定义
迪米特法则(LoD,Law of Demeter)也称最少知识法则(LKP,Least Knowledge Principle): 一个类应该要对自己耦合或调用的类知道的最少。也就是说主调类只关注被调类或被耦合类暴露出来的可访问方法(如public修饰),并且只关注自己使用的方法,其他的不需要知道。这可以通过减少类间不必要的依赖、降低耦合提高内聚来实现。
关键点:迪米特法则对类的低耦合提出了明确要求
秦小波老师用拟人的手法对听起来难以理解的迪米特进行了详细的解读。
1、只和直接朋友交流(Only talk to your immediate friends)
何为直接朋友?两个类间发生耦合就可以称之为朋友关系,如组合、聚合、依赖(相关概念详细了解可查看UML);体现在代码上就是:出现在成员变量、方法的输入输出参数中的类称之为朋友类。
示例:
体育老师想让班长清点到场的女生。假设这里涉及到三个类:老师、班长、女生。
先来看看可能的一种代码实现(相关代码功能见名知意,砍掉相关继承关系与接口设计等)
class Girl{}
class Monitor{
public void countGirls(List<Girl> girlsList) {
System.out.println("girl number:"+girlsList.size());
}
}
class Teacher{
public void command(Monitor monitor) {
//initial girl list
List<Girl> girlsList=new ArrayList(10);
for(int i=0;i<10;i++) {
girlsList.add(new Girl());
}
//count by Monitor
monitor.countGirls(girlsList);
}
}
public class Client {
public static void main(String[] args) {
//driver
new Teacher().command(new Monitor());
}
}
很明显,这个代码结构是有问题的!
类图:(tool:EA)
我们可以看到:
1、Girl是Monitor的朋友类(在countGirls方法传参中使用到了Girl类)。
2、Monitor是Teacher的朋友类(同上)。但在command方法中却使用了Teacher的非朋友类Girl。
问题导火索出现在command方法中:command方法与一个陌生类Girl有了交流,破坏了Teacher类的健壮性,违背了迪米特法则。
那这里该怎么修改呢?把girlsList放在Teacher的成员变量或利用传参?还记得前面说到过的吗:这里的Teacher其实并不需要关注Girl类长什么样子,仅仅是需要通过Monitor得知Girl的数量。Teacher如果什么都需要知道的话,那要班长干嘛?也就是说在上述业务需求下,Girl根本不需要出现在Teacher类中。
综上对代码做出修改如下:
class Girl {}
class Monitor {
List<Girl> girlsList;
// DIP
public Monitor(List<Girl> girlsList) {
this.girlsList = girlsList;
}
public void countGirls() {
System.out.println("girl number:" + girlsList.size());
}
}
class Teacher {
public void command(Monitor monitor) {
monitor.countGirls();
}
}
public class Client {
public static void main(String[] args) {
// driver
// initial girl list
List<Girl> girlsList = new ArrayList(10);
for (int i = 0; i < 10; i++) {
girlsList.add(new Girl());
}
// count by Monitor
new Teacher().command(new Monitor(girlsList));
}
}
类图
此时类间关系就改变了,Teacher不再需要关注Girl。在驱动类中对girlsList进行初始化,Monitor中利用依赖注入传入girlsList,实现了Teacher与Girl的解耦,增强了系统健壮性。
在某些场景下可能会出现通过一个类的方法获取到另一个类对象的情况,也就是说这种情况下类间关系通过方法建立。如B.getA();有点类似Spark中的Builder类。一般情况下我们建立类间关系不会基于方法,而是基于类的层面上去建立。如利用成员变量、接口依赖注入。
2、朋友间也是有距离的
假设还是以上述代码为例,班长在上体育课时不仅需要清点人数,还要根据当时的天气情况、训练项目通知同学们去相应的场所上课,清点人数时还要知道哪些人缺席了、哪些人请假了。
这样一来老师需要班长做的就不仅仅是countGirls了
如下:(其他代码不变)
class Monitor {
List<Girl> girlsList;
// DIP
public Monitor(List<Girl> girlsList) {
this.girlsList = girlsList;
}
// count
public void countGirls() {
System.out.println("girl number:" + girlsList.size());
}
// tip
public void tipClass() {
System.out.println("the location for current PE is stadium NO.1, please");
}
// report
public void reportClassDetailAboutAbsent() {
System.out.println("all of Absent number:2. there are tony and mark");
}
public void reportClassDetailAboutLeave() {
System.out.println("all of Leave number:1. who named kitty");
}
}
class Teacher {
public void command(Monitor monitor) {
monitor.tipClass();
monitor.countGirls();
monitor.reportClassDetailAboutAbsent();
monitor.reportClassDetailAboutLeave();
}
}
类图
在这里仅写了最简单的方法体,并没有涉及具体的业务逻辑。Teacher调用了Monitor的四个方法,可以说Monitor在Teacher面前几乎没有任何秘密存在了,他们之间的关系过于紧密了。假设上课前的流程发生改变,直观的就涉及了到两个类代码的修改,而这里还只是最简单的流程体现而已;因为他们的紧耦合,变更风险的扩散变得难以掌控。两个刺猬取暖,靠的太近就容易刺伤对方。
实际上对于课前的很多事情,体育老师并不需要过于关心,只需要在学期的第一节体育课时和大家有个约定并且让班长监督就ok了。面对许多突发状况,班长需要自己去解决,并不需要事事请示体育老师。
在此情况下,Teacher只需要发布上课的信息就足够了。
变更后代码如下:
class Monitor {
List<Girl> girlsList;
// DIP
public Monitor(List<Girl> girlsList) {
this.girlsList = girlsList;
}
// count
private void countGirls() {
System.out.println("girl number:" + girlsList.size());
}
// tip
private void tipClass() {
System.out.println("the location for current PE is stadium NO.1, please");
}
// report
private void reportClassDetailAboutAbsent() {
System.out.println("all of Absent number:2. there are tony and mark");
}
private void reportClassDetailAboutLeave() {
System.out.println("all of Leave number:1. who named kitty");
}
public void PE() {
tipClass();
countGirls();
reportClassDetailAboutAbsent();
reportClassDetailAboutLeave();
}
}
class Teacher {
public void command(Monitor monitor) {
monitor.PE();
}
}
类图:(注意查看访问权限)
可以看到此时Monitor对于具体的工作方法变为了private,新增一个公共方法PE对外提供服务。此时若是上课流程发生改变,找Monitor就ok了。我们发现在之前的代码中提供了太多的public方法,这种对外提供服务的方法越多,变更扩散的风险也就越大。在实际的开发中,能够缩小方法的访问范围就尽量的缩小;能用private就不要用protected!
3、是自己的就是自己的
怎么理解这句话呢?在实际开发中,可能会遇到具体细节职责不明确的问题,就比如说上述的countGirls,班长可以做老师也可以做呀(在此假设Teacher已经引入Girl的依赖关系)!那这个方法放在哪里比较合适呢?秦老师这里提到一条原则:如果一个方法放在本类中,不增加类间关系也不产生负面影响,那就放在本类中。
4、谨慎使用Serializable
一开始看到这个标题以为是关于远程调用(RPC, Remote Procedure Call)中的全序列化问题,这里常常涉及到系统优化开发问题。另一个是大数据分布式开发中必须要记住的一点:你要知道你的代码运行在哪个角色里,是否要跨网络传输,哪些对象是不可序列化的(比如说数据库的connection)。不过这里好像并不是这么一回事,更多的是涉及到项目变更管理的问题。
最佳实践
从依赖关系的引入控制到类间解耦,我们慢慢向系统级开发的高内聚、低耦合目标迈进。不知道有没有想过这样的一个问题:解耦要解到什么样的一个程度呢?事务都还有个ACID标准呢!那解耦的粒度有一个通用标准吗?很遗憾也很庆幸,至少以目前软件工程的发展水平来看:解耦有可遵循的原则,但并没有必须执行的标准。在代码层面,我们可以引入第三方类降低类间耦合,但也因此增加了第三方类,类的增加往往意味着系统复杂度的增加,牵一发而动全身。我们需要在各种开发因素里面进行一个权衡(trade off),以找到一个至少在当前适用环境下的最优解决方案。
参考文献
秦小波《设计模式之禅 》第二版