C#版得墨忒耳定律(迪米特法则)

4 篇文章 0 订阅

最近在度娘搜代码优化,看到了Edison Zhou写的《代码整洁之道》(Clean Code)- 读书笔记,里面有个整理很清楚的思维导图,发现有得墨忒耳定律,就想到面试题里的简单经典的问题:什么是对象?对象的三大特征是什么?非常感慨,面试者往往看看而背诵。当然也包括我。单就对象的属性和方法也是一言而过,看到这个得墨忒耳定律,有想起前几天同事说的充血模式、贫血模式。感慨自己对对象的理解和使用之浅薄,实在令人发指!!!

降低耦合度,提高封装……

得墨忒耳定律(迪米特法则)(Law of Demeter,缩写LoD),其实就是“最少知识原则(Principle of Least Knowledge)”,是一种软件开发的设计指导原则,在面向对象软件设计种用的较多,效果很好。得墨忒耳定律是松耦合的一种具体案例。使得软件有更好的可维护性与适应性。因为对象较少依赖其它对象的内部结构,可以改变对象容器(container)而不用改变它的调用者(caller)。

这个就好像一个企业里最有效率的团队成员之间各自努力干着自己的事情:

不相关的不扯淡,弱相关的少扯蛋,尽量减少强相关。避免不必要的干扰和恢复中断之前状态所需要的成本。

这里包括心情成本,时间成本。。。

软件的各个模块之间也是同样的道理,尤其是越大的系统越要注意。

不仅仅是软件, 硬件也遵循着同样的道理。

 

附件:维基百科对“得墨忒耳定律”的解释

得墨忒耳定律(Law of Demeter,缩写LoD)也叫做“最少知识原则”,是一种开发软件的设计原理,特别是面向对象的程序设计,得墨忒耳定律是松耦合的一种特殊情况。该指导原则是1987年末在美国东北大学发明的,该原则可以简单地概括为以下方式之一:

1每个单元对于其他的单元只能拥有有限的知识:只是与当前单元紧密联系的单元;

2每个单元只能和它的朋友交谈:不能和陌生单元交谈;

3只和自己直接的朋友交谈。 这个原理的名称来源于希腊神话中的农业女神,孤独的得墨忒耳。

 

编写“羞怯内向型”的代码:

包含两层意思,一个是不向别人暴露你自己,不会没必要的向其他模块暴露任何事情;另一个是不与太多人打交道,不依赖于其他模块实现的模块。

不与太多人打交道,说的就是要降低与别人的耦合,比如你的模块A依赖于一个模块B的功能,那么你就仅仅调用这个模块B的功能,而不要调用这个模块的实现中出现的模块C的功能,因为,一旦B的模块实现方式改变,那么C可能不存在,或者C出现了变动,那么它的影响就不仅仅是B,还有A也受到了影响。而如果A只调用B,则即使B的实现去掉了C模块,那么只要B的接口不变,那么A是不受影响的,或者如果C变了,那么由于A只调用B,则C的变动影响的只会是B,而不会影响A。因此,耦合性强的系统,改变代码将会变得非常困难,牵一发而动全身。

函数的得墨忒耳法则试图使任何给定程序中的模块之间的耦合减至最少,它设法阻止你为了获得对第三个对象的方法的访问而进入某个对象。

法则规定:某个对象的任何方法都应该只调用属于以下情况的方法:

1.这个对象自己拥有的方法;

2.传入该方法的参数的方法;

3.该方法创建的对象的方法;

4.该对象直接拥有的对象的方法;

代码如下:

class A{

        private B b = new B();

        private void methodE(){}

        public void methodA(C c){

                D d = new D();

                methodE(); //1这个对象自己拥有的方法,可调用

               c.print(); //2传入该方法的参数的方法,可调用

               d.invert(); //3该方法创建的对象的方法,可调用

               b.kill(); //4该对象直接拥有的对象的方法,可调用

               F f = b.getF();

               f.rock(); //5 该对象依赖对象的实现的模块,不可调用。

         }

}

 当然,得墨忒耳定律中,由于不建议调用依赖模块B的实现的模块C,因此,你不得不编写大量的包装方法,这些方法只是把请求转发给被委托者。存在运行时代价和空间开销,如果你很在乎这些,那么你不得不在性能和可维护性灵活性之间做个平衡,比如为了性能而使某些模块紧密耦合。呵呵,有时为了性能不得不‘反规范化’,就像在数据库设计中,为了提高访问速度,避免联合查询,而对某些字段采用冗余一样。

得墨忒耳定律--对象 O 的 M 方法,可以访问/调用如下的:

  1. 对象 O 本身
  2. M 方法的传入参数
  3. M 方法中创建或实例化的任意对象
  4. 对象 O 直接的组件对象
  5. 在M范围内,可被O访问的全局变量

 

 这些都是很简单的规则。

换言之:每个单元(对象或方法)应当对其他单元只拥有有限的了解。

一些比喻

最常见的比喻是:不要和陌生人说话

看看这个:假设我在便利店购物。付款时,我是应该将钱包交给收银员,让她打开并取出钱?还是我直接将钱递给她?

再做一个比喻:人可以命令一条狗行走(walk),但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。

为什么要遵循这个规则?

  • 我们可以更改一个类,而无需因连锁反应再去改许多其他的(类)。
  • 我们可以改变调用的方法,而无需改变其他任何东西。
  • 遵从LOD,让测试更容易被构建。我们不必为了模拟而写很多的’when’和各种return。
  • 提高了封装和抽象(下文将举例说明)。
  • 基本上,我们隐藏了“xx是如何工作的”。
  • 让代码更少的耦合。主叫方法只耦合一个对象,而并非所有的内部依赖。
  • 它通常会更好地模拟现实世界。想想钱包与付款的那个比喻。

数数那些“.”?

虽然在代码中充斥着许多“.”意味着Lod定律被违反了,但有时“合并这些点”并没有任何意义。比如我们把下面这样代码,

getEmployee().getChildren().getBirthdays()

 重构成这样子:

getEmployeeChildrenBirthdays()

这样真的好么,我不确定。

太多包装类

这是不遵从LOD的另一个结果。在这种情况下,我坚信这类的设计需要被重新处理。

所以,在编码、清理或重构的过程中,我们要遵循某些常识性的规律。

一个范例

假设有一个ItemFirst类,它包含多个属性。每个属性都有一个名称和值。(这是一个多值属性)

那最简单的实现就是使用Dictionary。

public class ItemFirst
    {
        private readonly Dictionary<string, List<string>> attributes;
        public ItemFirst()
        {
            this.attributes = new Dictionary<string, List<string>>();

        }

        public Dictionary<string, List<string>> GetAttributes()
        {
            return attributes;
        }
    }

 java类:

public class Item {
  private final Map<String, Set<String>> attributes;

  public Item(Map<String, Set<String>> attributes) {
    this.attributes = attributes;
  }

  public Map<String, Set<String>> getAttributes() {
    return attributes;
  }
}

 现在,有一个ItemsSaver类,将使用到Item和其属性:

public class ItemSaver
    {
        private string valueToSave;
        public ItemSaver(string valueToSave)
        {
            this.valueToSave = valueToSave;
        }

        public void DoSomething(string attributeName, ItemFirst item)
        {
            List<string> attributeValues = item.GetAttributes()[attributeName];
            foreach (var value in attributeValues)
            {
                if (value == valueToSave)
                {
                    DoSomethingElse();
                }
            }
        }

        private void DoSomethingElse()
        {
        }
    }

java类: 

public class ItemSaver {
  private String valueToSave;
  public ItemSaver(String valueToSave) {
    this.valueToSave = valueToSave;
  }

  public void doSomething(String attributeName, Item item) {
    Set<String> attributeValues = item.getAttributes().get(attributeName);
    for (String value : attributeValues) {
      if (value.equals(valueToSave)) {
        doSomethingElse();
      }
    }
  }

  private void doSomethingElse() {
  }
}

 

 我想获取某一个具体属性的时候:

List<string> attributeValues = item.GetAttributes()[attributeName];

var singleValue = attributeValues.FirstOrDefault(); 

很明显,我们遇到一个问题。每当使用Item类的属性时,我们知道了它如何工作,它的内部实现。同时,这也让测试代码难以维护。

我的宇宙第一IDE建单元测试一直在崩溃,我也崩溃了。就借用java的测试来说吧 

看一下这个测试用例(测试框架 Mockito):你可以想象到这是多么难以变更和维护。

    Item item = mock(Item.class);
    Map<String, Set<String>> attributes = mock(Map.class);
    Set<String> values = mock(Set.class);
    Iterator<String> iterator = mock(Iterator.class);
    when(iterator.next()).thenReturn("the single value");
    when(values.iterator()).thenReturn(iterator);
    when(attributes.containsKey("the-key")).thenReturn(true);
    when(attributes.get("the-key")).thenReturn(values);
    when(item.getAttributes()).thenReturn(attributes);

 可以用真正的Item替代模拟的,但这仍需要创建大量的测试前数据。

让我们来回顾一下:

  • 我们暴漏了内部实现--Item类怎样保存它的属性
  • 为了使用属性,我们需要从item对象中拿到属性和它的内部实现相关对象(如测试代码中的Set集合values)。
  • 如果想改变属性的实现,我们需要更改所有使用Item和其属性的类。这很可能波及多个类。
  • 构建的测试繁琐、易错,且需要很多维护。

 

改进

第一个改进是,把对属性的各种操作,委托给Item类本身。

public class Item {
  private final Map<String, Set<String>> attributes;
  public Item(Map<String, Set<String>> attributes) {
    this.attributes = attributes;
  }

  public boolean attributeExists(String attributeName) {
    return attributes.containsKey(attributeName);
  }

  public Set<String> values(String attributeName) {
    return attributes.get(attributeName);
  }

  public String getSingleValue(String attributeName) {
    return values(attributeName).iterator().next();
  }
}

这样,测试便容易多了:

Item item = mock(Item.class);
    when(item.getSingleValue("the-key")).thenReturn("the single value");

我们几乎将属性相关操作的实现都隐藏了。

使用到Item的类,并不知道其内部实现。除了以下两个情形:

  1. Item本身仍知道属性被怎样构建。
  2. 创建Item的类,也知道属性怎样被实现。

以上两点意味着,如果我们改变属性的实现(比如变更为不使用map),至少两个其他的类将需要改变。这是高耦合的一个好例子。

进一步改进

上面的解决方案有时候(通常?)足够。作为务实的程序员,我们需要知道何时停止。然而,来看如何进一步改进第一种方案。

创建一个Attributes类:

public class Attributes
    {
        private readonly Dictionary<string, List<string>> attributes;
        public Attributes()
        {
            this.attributes = new Dictionary<string, List<string>>();

        }

        public bool AttributeExists(string attributeName)
        {
            return attributes.ContainsKey(attributeName);
        }


        public List<string> Values(string attributeName)
        {
            return attributes[attributeName];
        }

        public string GetSingleValue(string attributeName)
        {
            return Values(attributeName).FirstOrDefault();
        }

        public Attributes AddAttribute(string attributeName, ICollection<string> values)
        {
            this.attributes.Add(attributeName, (List<string>)values);

            return this;
        }
    }
public class Attributes {
  private final Map<String, Set<String>> attributes;

  public Attributes() {
    this.attributes = new HashMap<>();
  }

  public boolean attributeExists(String attributeName) {
    return attributes.containsKey(attributeName);
  }

  public Set<String> values(String attributeName) {
    return attributes.get(attributeName);
  }

  public String getSingleValue(String attributeName) {
    return values(attributeName).iterator().next();
  }

  public Attributes addAttribute(String attributeName, Collection<String> values) {
    this.attributes.put(attributeName, new HashSet<>(values));
    return this;
  }
}

然后Item类这样去用它:

 public class Item
    {
        private readonly Attributes attributes;

        public Item(Attributes attributes)
        {
            this.attributes = attributes;
        }

        public bool AttributeExists(string attributeName)
        {
            return attributes.AttributeExists(attributeName);
        }

        public List<string> Values(string attributeName)
        {
            return attributes.Values(attributeName);
        }

        public string GetSingleValue(string attributeName)
        {
            return attributes.GetSingleValue(attributeName);
        }

        public Attributes AddAttribute(string attributeName, List<string> values)
        {
            return attributes.AddAttribute(attributeName, values);
        }
    }
public class Item {
  private final Attributes attributes;

  public Item(Attributes attributes) {
    this.attributes = attributes;
  }

  public boolean attributeExists(String attributeName) {
    return attributes.attributeExists(attributeName);
  }

  public Set<String> values(String attributeName) {
    return attributes.values(attributeName);
  }

  public String getSingleValue(String attributeName) {
    return attributes.getSingleValue(attributeName);
  }
}

 

(注意到了么?Item内属性项的实现被改变,但测试代码无需变更。这是由前面委托的更改导致的。)

在第二种解决方案中,我们改进了属性的封装。现在,连Item类自己都不知道它(属性)是如何工作的。

现在,我们可以变更属性的实现,而不必修改任何一个其他类。而且可以将属性变为如下任一类实现方式:

  • 用Set集合保存一组Value
  • 用List列表保存一组Value
  • 用你能想到的某种完全不同的数据结构
  • 只要所有测试都通过,就一切OK。

我们获得了什么?

  • 代码更容易维护。
  • 测试用例更简单且易于维护。
  • 代码更加灵活。我们可以随意更改属性项的实现(map, set, list或其他)
  • 属性项的修改,不会影响其他代码。甚至不会影响直接使用它(的代码)。
  • 模块化和代码重用。可以在其他代码中重用Attributes类。

 本文是想把java的知识点用C#描述,奈何我水平有限,掺杂着整理

本文内容出自: 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值