代码世界中类间的耦合关系会直接影响代码可复用性、可读性、可扩展性等。这种耦合关系就如同人之间朋友关系一样,志不同道不合不应相于谋,否则最终只会落得个互相伤害的下场。代码组织时也应如此,应按照一定的原则处理好类之间的关系,否则就会导致恶性耦合只会使得项目代码越写越烂,难以维护。这个原则就是迪米特原则。
一、迪米特原则概念
迪米特法则(Law of Demeter, LOD),也称之为最少知识原则,指的一个类应该对自己需要耦合或调用的类知道的最少。相应一些英文定义:
- Each unit should have only limited knowledge about other units: only units “closely” related to the current unit.
(每个单元对于其他的单元只能拥有有限的知识:只是与当前单元紧密联系的单元)- Each unit should only talk to its friends; don’t talk to strangers. (每个单元只能和它的朋友交谈:不能和陌生单元交谈)
- Only talk to your immediate friends. (只和自己直接的朋友交谈) (来自维基百科)
不论是中文定义还是英文定义,我想根据自己的理解换一种说法,即“一个类应该与自己应该依赖的类产生依赖,而不应该与自己不该依赖的类产生依赖”。这样的说法似乎更容易被理解一些,那继而来的问题就是,哪些是这个类应该依赖的?哪些是不应该以来的呢?这些应该依赖的类在英文定义中称之为"朋友类"。
在“朋友类”的定义和判断上,一些参考资料给出的是出现在类成员变量、类方法的输入输出参数中的类称为朋友类,而出现在方法体内部的类不属于朋友类。个人对这个定义不是很赞同,这个定义可以用来评价现有代码是否符合迪米特原则时判断朋友类,但是不适用代码设计阶段。即,我们在设计代码时,哪些类应该是朋友类这个定义不能很好的给我们回答。我个人认为,在单一职责原则基础上,应该强耦合的朋友类之间所对应的业务逻辑也是强耦合的。所有原则、代码设计均是以业务逻辑为基础,只要这样才能更好的服务于业务。所以,判断类之间是否为朋友类,就看类所负责的业务之间是否强耦合,对于非强耦合业务类不要产生依赖,这也满足了类对自己调用的类知道的最少【业务都强耦合,代码类上必须依赖呀,没法子不依赖哈】。
举个栗子,在RPC服务中大概可以有接口层、业务层、数据层等,根据业务逻辑的耦合关系来看,接口层会依赖业务层提供业务能力,返回业务数据,但是不能够依赖数据层直接获取存储数据,这就不满足迪米特原则了。再举一个其他资料的案例,公司类(Company)应该仅依赖部门(Department),不应该直接依赖员工(Employee)。另外,我们常熟知的DO、BO、DTO等这种分层规范也是应用迪米特原则的一种体现。
二、应用实践
前面已经基本说清楚迪米特法则的基本概念了,这个章节将通过一个非常简单的案例演示下迪米特法则的应用。我们平常在零碎的时间里,喜欢看一些书籍,一般都是电子书,现在我们看书的操作是这样的:唤醒手机,打开阅读软件,选择书籍,然后阅读。总共 3 个步骤,涉及了 3 样东西:手机、软件、书籍。现在使用代码来描述下这个过程。
2.1 未应用迪米特法则
在未应用迪米特法则时,我们可能会写出下面这样的代码:
① 书籍
public class Book {
private String title;
public Book(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
② 软件应用
public class App {
public void read(Book book) {
System.out.println(book.getTitle());
}
}
③ Phone
public class Phone {
App app = new App();
Book book = new Book("设计模式");
public void readBook() {
app.read(book);
}
}
④ 客户端
public class Client {
public static void main(String[] args) {
Phone phone = new Phone();
phone.readBook();
}
}
从类图上来看,首先电子书应用是由电子书籍构成,因此APP与Book的关系不应该是依赖关系,更应该是强依赖的组合关系。再者,手机和电子书之前不应该存在耦合(依赖)关系,没有电子书,手机一样能够作为他用。如果在之后的业务逻辑中,如果书籍业务扩增,如添加类型、版权等都会影响到本不该影响的手机Phone业务。由此可以看出,Phone依赖了不该依赖的类Book,这种依赖业务的变化十分不可控,对依赖方的影响也不可控,继而导致系统稳定性、可扩展性降低。
2.2 应用迪米特法则
基于以上问题,如果应用迪米特法则,那么Phone类应该就只能够依赖App类,而App类也只依赖Book类。代码修改如下:
① 书籍
书籍和$2.1节相同,不重复补充。
② 软件应用
public class App {
private Book book;
public App(Book book) {
this.book = book;
}
public void read() {
System.out.println(book.getTitle());
}
}
③ Phone
public class Phone {
private App app;
public Phone(App app ) {
this.app = app;
}
public void readBook() {
app.read();
}
}
④ 客户端
public class Client {
public static void main(String[] args) {
App app = new App(new Book("设计模式"));
Phone phone = new Phone(app);
phone.readBook();
}
}
这样的代码类耦合关系看起来就十分舒服了,书籍相关业务变动只可能会影响其和App之间的关系,即便重构也不会影响到Phone相关业务,明显提高了代码稳定性和扩展性。
这里可能有人会有疑问,前后两个方案对比,那Client类不是依赖的更多了吗?本来只需要依赖一个Phone类,然而改造后依赖了三个类。没错,确实Client依赖更多,但是这是符合业务逻辑的,“唤醒手机,打开阅读软件,选择书籍”,作为执行者执行这一套动作,肯定需要知道什么书籍(Book具体对象),什么应用(App对象),什么手机(Phone对象),因此这种依赖是符合业务逻辑的。此外,之前文章中也说过,Client不属于设计模式考虑范围之内,Client中是处理类对象行为过程,非代码设计讨论范围之内的。
【参考资料】
- 迪米特法则-知乎【推荐理由:符合作者个人理解】