迪米特法则(Law of Demeter):又称最小知识原则。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。
利用这个原则,能够帮我们实现代码的 高内聚、松耦合。
高内聚、松耦合
很多设计原则都以实现代码的 高内聚、松耦合 为目的,比如单一职责原则、基于接口而非实现编程等。
高内聚
所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。
松耦合
所谓松耦合是说,在代码中,类与类之间的依赖关系简单清晰。
即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。
两者之间的关系
高内聚、松耦合 是一个比较通用的设计思想,可以用来指导不同粒度代码的设计与开发,
比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。
- 高内聚 用来指导类本身的设计,松耦合用来指导类与类之间依赖关系的设计。
- 高内聚有助于松耦合,同理,低内聚也会导致紧耦合。
不该有直接依赖关系的类之间,不要有依赖
例:简化版的搜索引擎爬取网页功能
// 负责底层网络通信,根据请求获取数据
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(HtmlRequest htmlRequest) {
//...
}
}
// 用来通过 URL 获取网页
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
类的设计违背迪米特法则,依赖了不该有直接依赖关系的HtmlRequest
类。作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载 HTML,所以,我们不应该直接依赖太具体的发送对象
HtmlRequest
。
我们应该把address
和content
交给NetworkTransporter
,而非是直接把HtmlRequest
交给NetworkTransporter
。 -
Document
类构造函数中的downloader.downloadHtml()
逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。 -
Document
类中的HtmlDownloader
对象在构造函数中通过new
来创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性。 -
从业务含义上来讲,
Document
网页文档没必要依赖HtmlDownloader
类,违背了迪米特法则。
重构后的代码:
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(String address, Byte[] data) {
//...
}
}
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);
}
}
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);
}
}
有依赖关系的类之间,尽量只依赖必要的接口
在 单一职责中序列化与反序列化 的例子中,合并之后的类,在某些场景下违法了迪米特法则。
假设在我们的项目中,有些类只用到了序列化操作,而另一些类只用到反序列化操作。
那基于迪米特法则后半部分 有依赖关系的类之间,尽量只依赖必要的接口,只用到序列化操作的那部分类不应该依赖反序列化接口。
同理,只用到反序列化操作的那部分类不应该依赖序列化接口。
尽管拆分之后的代码更能满足迪米特法则,但却违背了高内聚的设计思想。
高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的地方不至于过于分散。
实际上,通过引入两个接口就能同时满足迪米特法则和高内聚设计思想:
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;
}
//...
}
上面的的代码实现思路,也体现了 基于接口而非实现编程 的设计原则,结合迪米特法则,可以总结出一条新的设计原则,那就是 基于最小接口而非最大实现编程。