迪米特法则 又称为 最少知识原则:就是说,一个对象应当对其它对象有尽可能少的了解。
迪米特法则的初衷在于降低类之间的耦合。由于类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块独立,相互之间不存在或存在很少的依赖关系。
众多表述
迪米特法则 有多种表述,下面是众多的表述中较有代表性的几种:
- 只与你直接的朋友通信(Olny talk to your immediate friends)
- 不要跟 “陌生人” 说话(Don't talk to strangers)
- 每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
朋友圈定义
- 当前对象本身(this)
- 以参量形式传入到当前对象方法中的
- 当前对象的实例变量直接引用的对象
- 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
- 当前对象所创建的对象
任何一个对象,如果满足上面的条件之一,就是当前对象的 “朋友”;否则就是 “陌生人”。
设计示例
一个示例,有 Friend,Someone,Stranger 三个类,其中 Someone 与 Friend 是朋友,Friend 对象作为 Someone 构造方法的传入参数。
迪米特法则的改造示例:
public class Someone {
/**
* 【不符合迪米特法则】
* Someone 接受 Friend 入参, 两者是朋友关系
* operation方法不满足迪米特法则, 该方法引用了 Stranger 对象,
* 而 Stranger的对象不是 Someone 的朋友
*
* @param friend
*/
public void operation(Friend friend) {
Stranger stranger = friend.provider();
stranger.process();
}
/**
* 针对上面方法改造
* forward 叫作转发方法, 将具体细节隐藏在 Friend 内部
* 从而使 Some 与 Stranger 之间的直接联系被省略掉。
* 使系统内部耦合度降低。在系统的某一个类需要修改时,
* 仅仅会直接影响到这个类的 “朋友” 们,
* 而不会直接影响到其余部分
*
* @param friend
*/
public void operation1(Friend friend) {
friend.forward();
}
}
public class Friend {
private Stranger stranger = new Stranger();
public Friend(Stranger stranger) {
this.stranger = stranger;
}
public Stranger provider() {
return stranger;
}
/**
* @desc: 转发方法
* @param: []
*/
public void forward() {
stranger.process();
}
}
public class Stranger {
public void process() {
}
}
狭义的迪米特法则的缺点
遵循狭义的迪米特法则会产生一个明显的缺点:会在系统里造出大量的小方法,散落在系统的各个角落。这些方法仅仅是传递间接的调用,与系统的业务逻辑无法。容易造成迷惑和困扰。
遵循类之间的迪米特法则会使一个系统的局部设计简化,但也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。
与依赖倒置原则互补使用
为了克服狭义的迪米特法则的缺点,可以使用依赖倒置原则,引入一个抽象的类型引用 “抽象随生人” 对象,使 “抽象陌生人” 变成 “某人” 的朋友。
/**
* 陌生人抽象
*/
public interface AbstractStranger {
abstract void process();
}
/**
* 陌生人抽象
*/
public class Stranger implements AbstractStranger {
@Override
public void process() {
}
}
/**
* 朋友
*/
public class Friend {
private AbstractStranger stranger = new Stranger();
public Friend(AbstractStranger stranger) {
this.stranger = stranger;
}
public AbstractStranger provider() {
return stranger;
}
/**
* 转发方法
*/
public void forward() {
stranger.process();
}
}
/**
* 某些人
*/
public class Someone {
/**
* Friend 提供的是抽象类型
*/
public void operation2(Friend friend) {
AbstractStranger provider = friend.provider();
provider.process();
}
}
广义的迪米特法则
迪米特法则所谈论的,实际是对象之间的信息流量,流向以及信息的影响控制。
在软件系统中,一个模块设计得好不好的最主要,最重的标志,就是该模块在多大的程度上将自己的内部数据和其他与实现有关的细节陷藏起来。
一个设计很好的模块可以将它所有的实现细节隐藏起来,彻底地将提供给外界的 API 和自己的实现分隔开来。使模块与模块之间可以仅仅通过彼此的 API 相互通信,而不理会模块内部的工作细节。这一概念就是 信息的隐藏 或者叫做 封装,也就是大家熟悉的面向对象软件设计特征之一。
信息的隐藏(封装)非常重要的原因在于,它可以使各个子系统之间脱耦,从而允许它们独立地被开发,优化,使用,阅读以及修改。
这种脱耦化可以有效地加快系统的开发过程,因为可以独立地同时开发各个模块。它可以使维护过程变得容易,因为所有模块都容易读懂,特别是不必担心对其他模块的影响。
信息的隐藏并不能带来更好的性能,但它可以使性能的有效调整变得容易。一旦确认某一个模块是性能的障碍时,设计人员可以针对这个模块本身进行优化,而不必担心影响到其他的模块。
信息的隐藏可以促进软件的复用。由于每一个模块都不依赖于其他模块而存在,因此每一个模块都可以独立地在其他的地方使用。一个系统的规模越大,信息的隐藏就越是重要,而信息隐藏的威力也就越明显。
迪米特法则运用注意事项
迪米特法则的主要用意是控制信息的过载。在将迪米特法则运用到系统设计中时,要注意下面的几点:
在类的划分上,应当创建有弱耦合的类。
类之间的耦合越弱,就越有利于复用。一个处在弱耦合中的类一旦被修改,不会对有关系的类造成波及。
在类的结构设计上,每一个类都应当尽量降低成员的访问权限(Accessibility)。
换而言之,一个类包装好自己的 private 状态。一个类不应当 public 自己的属性,应当提供取值或赋值方法让外界间接访问自己的属性。
在类的设计上,只要有可能,一个类应当设计成不变类。
- 在对其他类的引用上,一个对象对其对象的引应当降到最低。
广义迪米特法则在类的设计上的体现
优先考虑将一个类设置成不变类
Java 提供了很多不变类,如:String,BigInteger,BigDecimal 等封装类都是不变类。不变类易于设计,实现和使用。
在设计任何一个类的时候,首先考虑这个类的状态是否需要改变。即便一个类必须是可变类,在给它的属性设置赋值方法的时候,也要保持吝啬的态度。除非真的需要,否则不要为一个属性设置赋值方法。
尽量降低一个类的访问权限
在满足一个系统对这个类的需求的同时,应当尽量降低这个类的访问权限(accessibility)。对于项级(top-level)的类来说,只有两个可能的访问权限等级:
package-privage:default,包路径下可访问,这是默认的访问权限,即只能从当前库访问。
package-private 的好处是,一旦这个类发生修改,那么受影响的客户端必定是在这个库内部。由于一个软件包往往有它自己的库结构,因此一个访问权限为 package-private 的类是不会被客户应用程序使用的,这意味着软件提供商可以自由地决定修改这个类,而不必担心对客户的承诺。
public:public 修饰的类,可以从当前库和其他库访问它。客户应用就有可能会使用这个类,一旦这个类有更新或删除,就可能造成客户的程序停止运行的情况。
如果一个类可以设置成为 package-private 的,那么就不应当将它设置成为 public 。
谨慎使用 Serializable 接口
一个类如果实现了 Serializable 接口的话,客户端就可能将这个类的实例序列化,然后再反序列化。
由于 序列化 和 反序列化涉及到类的内部结构。如果这个类的内部 private 结构在一个新版本中发生变化的话,那么客户端可能会根据新版本的结构试图将一个老版本的 序列化 结果 反序列化,这会导致失败。
也就是说,为防止这种情况发生,软件提供商一旦将一个类设置成为 Serializable 的,就不能再在新版本中修改这个类的内部结构,包括 private 的 方法和句段。因此,除非必要,不要使用 Serializable。
尽量降低成员的访问权限
类的成员包括:属性、方法、嵌套类,嵌套接口等。一个类的成员可以有四种不同的访问权限
访问权限 | 同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 | 描述 |
---|---|---|---|---|---|
public | √ | √ | √ | √ | 开放的访问权限 |
protected | √ | √ | √ | 保护访问权限,为继承而设计 当前类和子类可访问 |
|
default(默认) | √ | √ | 默认的,没有权限修饰符 又称 package-private 同包内私有,同包的类可以访问 |
||
private | √ | 私有权限,只允许本类内访问 |
注:其中 private 和 protected 不能修饰的类接口,否则编译就会报“modifier private not allowed here”
作为一条指导原则,应将访问权限控制在有限的范围内,并且是根据需要逐步开放,而不是一次性全部对外开放。一旦设置成 public,那么就可能被任何类访问。这对一个软件提供商来说,意味着客户程序可能使用该方法,因此在所有以后的版本中都要承诺不改变这个方法的特征,就变成了对外的一种承诺,这个成本可能是非常高昴的。
因此,将 private 或 package-private 方法改为 protected 或者 public,必须慎之又慎。
广义迪米特法则在代码层次上的实现
Java 语言允许一个变量在任何地方声明,即任何可以有语句的地方都可以声明变量,相对于 C语言要求所有局域变量都在一个程序块的开头声明,这样做的意义深远,是被很多人忽略的。
在需要一个变量的时候才声明它,可以有效地限制局域变量的有效范围。一个变量如果仅仅在块的内部使用的话,就应当将这个变量在程序块的内部使用它的地方声明,而不是放到块的外部或者块的开头声明。这样做有两个好处:
程序员可以很容易读懂程序。
否则,程序员需要反复对照使用变量的语句和变量的声明语句才能将变量的使用与声明对上号。并且局部变量随语句块的的修改是可见的,而不会散落在其它地方,当不再被使用时也没有人留意将其删除。
如果一个变量是在需要它的程序块的外部声明的,那么当这个块还没有被执行时,这个变量就已经被分配内存了;而这个程序块已经执行完毕后,这个变量所占据的内存空间还没释放,这显然是不好的。
注意:本文归作者所有,未经作者允许,不得转载