3.8 迪米特法则
3.8.1 何为高内聚、低耦合
-
高内聚:
相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
-
低耦合:
类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。
3.8.2 何为迪米特法则(LOD)?
- 迪米特法则的英文翻译是:Law of Demeter,缩写是 LOD
- 另一个更加达意的名字,叫作最小知识原则,英文翻译为:The Least Knowledge Principle
- 每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。
- 不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。
3.8.3 代码实战一:理解“不该有直接依赖关系的类之间,不要有依赖“
-
简版搜索引擎爬取网页的功能。代码中包含三个主要的类。
- NetworkTransporter 类负责底层网络通信,根据请求获取数据;
- HtmlDownloader 类用来通过 URL 获取网页;
- Document 表示网页文档,后续的网页内容抽取、分词、索引都是以此为处理对象。
具体的代码实现如下所示:
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(HtmlRequest htmlRequest) {
//...
}
}
public class HtmlDownloader {
private NetworkTransporter transporter;//通过构造函数或IOC注入
public Html downloadHtml(String url) {
Byte[] rawHtml = transporter.send(new HtmlRequest(url));
return new Html(rawHtml);
}
}
public class Document {
private Html html;
private String url;
public Document(String url) {
this.url = url;
HtmlDownloader downloader = new HtmlDownloader();
this.html = downloader.downloadHtml(url);
}
//...
}
以上代码有比较多的设计缺陷:
-
NetworkTransporter 类。作为一个底层网络通信类,它的功能应尽可能通用,而不只是服务于下载 HTML,所以,不应该直接依赖太具体的发送对象 HtmlRequest。重构如下:
public class NetworkTransporter { // 省略属性和其他方法... public Byte[] send(String address, Byte[] data) { //... } }
-
HtmlDownloader 类。类的设计没有问题。修改了 NetworkTransporter 的 send() 函数的定义,需要对它做相应的修改,修改后的代码如下所示:
public class HtmlDownloader { private NetworkTransporter transporter;//通过构造函数或IOC注入 // HtmlDownloader这里也要有相应的修改 public Html downloadHtml(String url) { HtmlRequest htmlRequest = new HtmlRequest(url); Byte[] rawHtml = transporter.send( htmlRequest.getAddress(), htmlRequest.getContent().getBytes()); return new Html(rawHtml); } }
-
Document 类。这个类的问题有三点:
- 构造函数中的 downloader.downloadHtml() 逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。
- HtmlDownloader 对象在构造函数中通过 new 来创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性。
- 从业务含义上来讲,Document 网页文档没必要依赖 HtmlDownloader 类,违背了迪米特法则。
使用工厂方法解耦,重构如下:
public class Document { private Html html; private String url; public Document(String url, Html html) { this.html = html; this.url = url; } //... } // 通过一个工厂方法来创建Document public class DocumentFactory { private HtmlDownloader downloader; public DocumentFactory(HtmlDownloader downloader) { this.downloader = downloader; } public Document createDocument(String url) { Html html = downloader.downloadHtml(url); return new Document(url, html); } }
3.8.4 代码实战二:理解“有依赖关系的类之间,尽量只依赖必要的接口”
-
Serialization 类负责对象的序列化和反序列化:
public class Serialization { public String serialize(Object object) { String serializedResult = ...; //... return serializedResult; } public Object deserialize(String str) { Object deserializedResult = ...; //... return deserializedResult; } }
- 单看这个类的设计,没有一点问题。
- 在特定的应用场景里,那就还有继续优化的空间。假设在项目中,有些类只用到了序列化操作,而另一些类只用到反序列化操作。那基于迪米特法则后半部分“有依赖关系的类之间,尽量只依赖必要的接口”,只用到序列化操作的那部分类不应该依赖反序列化接口。同理,只用到反序列化操作的那部分类不应该依赖序列化接口。重构如下:
public class Serializer { public String serialize(Object object) { String serializedResult = ...; ... return serializedResult; } } public class Deserializer { public Object deserialize(String str) { Object deserializedResult = ...; ... return deserializedResult; } }
-
新的问题:能满足迪米特法则,但却违背了高内聚的设计思想。
-
高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的地方不至于过于分散。
- 如果修改了序列化的实现方式,比如从 JSON 换成了 XML,那反序列化的实现逻辑也需要一并修改。在未拆分的情况下,只需要修改一个类即可。在拆分之后,需要修改两个类。
-
既不想违背高内聚的设计思想,也不想违背迪米特法则,如何做?
public interface Serializable { String serialize(Object object); } public interface Deserializable { Object deserialize(String text); } public class Serialization implements Serializable, Deserializable { @Override public String serialize(Object object) { String serializedResult = ...; ... return serializedResult; } @Override public Object deserialize(String str) { Object deserializedResult = ...; ... return deserializedResult; } } public class DemoClass_1 { private Serializable serializer; public Demo(Serializable serializer) { this.serializer = serializer; } //... } public class DemoClass_2 { private Deserializable deserializer; public Demo(Deserializable deserializer) { this.deserializer = deserializer; } //... }
以上代码,尽管还是要往 DemoClass_1 的构造函数中,传入包含序列化和反序列化的 Serialization 实现类,但是,我们依赖的 Serializable 接口只包含序列化操作,DemoClass_1 无法使用 Serialization 类中的反序列化接口,对反序列化操作无感知,这也就符合了迪米特法则后半部分所说的“依赖有限接口”的要求。
3.8.5 总结
设计原则 | 适用对象 | 侧重点 | 思考角度 |
---|---|---|---|
单一职责 | 模块、类、接口 | 高内聚、低耦合 | 自身 |
接口隔离 | 接口、函数 | 低耦合 | 调用者 |
基于接口而非实现编程 | 接口、抽象类 | 低耦合 | 调用者 |
迪米特法则 | 模块、类 | 低耦合 | 类关系 |