女朋友嫌我写代码烂,被分手,学习设计原则的第四天。感觉马上也要失去工作了。祸不单行呀!
1.迪米特法则?
迪米特法则在《代码整洁之道》里学习过,当时那个本书翻译的是在是太烂了,我还是在新华书店官网买的,在网上搜了一个PDF的都比那本书翻译的好,不知道搁哪里搞的印刷版,一点追求也没有。
迪米特法则(Law of Demeter,LoD),也被称为最少知识原则(Principle of Least Knowledge,PoLK)或者"不和陌生人说话"原则,是面向对象编程中的一条设计原则。它强调一个对象应该对其他对象有最少的了解,即对象之间的耦合应该尽可能地减少。简言之,一个对象不应该直接与许多其他对象发生相互作用,而是通过与少数几个对象发生交互来完成任务。
具体来说,迪米特法则可以归纳为以下几个关键点:
- 只与朋友通信: 一个对象只应该调用其成员、自己创建的对象、方法的参数等,而不应该调用一个陌生的对象的方法。
- 减少对象之间的耦合: 对象之间的直接交互越少,系统越容易维护和扩展。
- 尽量降低类之间的耦合度: 类与类之间的关系越简单,系统越稳定。每个类应该对自己需要的类知之甚少。
在工作中听到最多的关于迪米特法则的就是“高内聚,低耦合”,那么什么是高低内聚低耦合呢?
// 不符合迪米特法则的例子
class Teacher {
public void instructStudent(Student student) {
// 教导学生的逻辑
}
}
class Student {
public void listenTo(Teacher teacher) {
// 听老师讲课的逻辑
}
}
// 符合迪米特法则的例子
class Teacher {
public void instructStudent() {
// 教导学生的逻辑
}
}
class Student {
public void listenTo() {
// 听老师讲课的逻辑
}
}
在符合迪米特法则的例子中,Teacher 和 Student 类之间的耦合更低,每个类都只关注自己的职责,而不是直接调用对方的方法,
概括说:“高内聚”用来指导类本身的设计,“低耦合”用来指导类与类之间依赖关系的设计。
实际上,“高内聚、松耦合”是一个比较通用的设计思想,可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,
提起高内聚,就是指功能相近的要放在一起,这个跟单一职责的主旨是一样的;
低耦合:是指类之间的关系尽可能少,改动本身,不会对其他的类造成影响,为了实现这种低耦合,对应产生的设计思想有,面向接口编程,依赖注入,接口隔离。都是抽象出不变的接口进行交互。
单一职责是从自身提供的功能出发,迪米特法则是从关系出发,针对接口而非实现编程是使用者的角度
案例 1 :
不该有直接依赖关系的类之间,不要有依赖
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);
}
//...
}
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(HtmlRequest htmlRequest) {
//...
}
}
作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载 HTML,所以,我们不应该直接依赖太具体的发送对象HtmlRequest,比如说浏览器访问网址只需要有rul就可以了,你给我一个文案,让我去文案里把Url提取出来。从这一点上讲,NetworkTransporter 类的设计违背迪米特法则,依赖了不该有直接依赖关系的 HtmlRequest 类。
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(String address, Byte[] data) {
//...
}
}
我们再来看 HtmlDownloader 类。这个类的设计没有问题。不过,我们修改了 NetworkTransporter 的 send() 函数的定义,而这个类用到了 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);
}
}
案例 2 :
有依赖关系的类之间,尽量只依赖必要的接口
public class Serialization {
public String serialize(Object object) {
String serializedResult = ...;
//...
return serializedResult;
}
public Object deserialize(String str) {
Object deserializedResult = ...;
//...
return deserializedResult;
}
}
根据前面的面向接口编程,接口隔离原则时,第一印象就是抽象出来。
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;
//避免直接new 对象,通过组合传入
public Demo(Serializable serializer) {
this.serializer = serializer;
}
//...
}
public class DemoClass_2 {
private Deserializable deserializer;
public Demo(Deserializable deserializer) {
this.deserializer = deserializer;
}
//...
}
“单一职责原则”“接口隔离原则”“基于接口而非实现编程”“迪米特法则”都是为了 实现 “高内聚、低耦合”的手段。做到了接口隔离,一般情况下职责也比较单一,基于接口而非实现编程,往往也会降低耦合性。有的时候使用了迪米特法则或者单一职责原则,可能会破坏高内聚原则,这种情况就要具体分析场景,以及使用接口来实现