当前位置:   article > 正文

迪米特法则(LOD)_lod法则

lod法则

何为“高内聚、松耦合”?

“高内聚、松耦合” 是一个非常重要的设计思想,能够有效地提高代码地可读性和可维护性,缩小功能改动导致的代码改动范围。很多设计原则都以实现代码的“高内聚、松耦合”为目的,比如单一职责、基于接口而非实现编程等。

“高内聚、松耦合” 可以用来指导不同粒度代码的设计与开发,“高内聚” 用来指导类本身的设计,“松耦合” 用来指导类与类之间依赖关系的设计。不过,这两者并非完全独立不相干,高内聚有助于松耦合,松耦合又需要高内聚的支持。

什么是 “高内聚”?

所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。

什么是 “松耦合”?

所谓松耦合,就是类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或很少导致依赖类的代码改动。比如依赖注入、接口隔离、基于接口而非实现编程,还有待会要讲的迪米特法则,都是为了实现代码的松耦合。

“内聚” 和 “耦合” 的关系

在这里插入图片描述

“高内聚” 有助于 “松耦合”,同理,”低内聚“ 也会导致 ”紧耦合“。上图左边是 ”高内聚、松耦合“ 的代码结构,右边是 ”低内聚、紧耦合“。

左边部分的代码设计中,类的粒度比较小,每个类的职责都比较单一,相近的功能都放到了一个类中,不相近的功能被分割到了多个类中,这样类更加独立,代码的内聚性更好。因为职责单一,所以每个类被依赖的类就会比较少,代码低耦合。一个类的修改,只会影响到一个依赖类的代码改动。我们只需要测试这一个依赖类是否还能正常工作就行。高内聚低耦合的代码结构更加简单、清晰,可维护性和可读性上要好很多。

右边部分的代码设计中,类粒度比较大,类功能大而全,不相近的功能放到了一个类中,导致很多其他类依赖这个类。当要修改这个类的某一个功能时,”牵一发而动全身“会影响依赖它的多个类。我们需要测试这三个依赖类是否能正常工作。

迪米特法则

迪米特法则(Law of Demeter,LOD),又名为最小知识原则(The Least Knowledge Principle),它的英文定义如下:

Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.
每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友 “说话”(talk),不和陌生人 “说话”(talk)

上面的定义描述比较抽象,简单理解就是,不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)

不该有直接依赖关系的类之间,不要有依赖

迪米特法则的前部分:不该有直接依赖关系的类之间,不要有依赖。举个例子解释一下。

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);
	}
	// ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

这段代码虽然 “能用”,但是它不够 “好用”。

NetworkTransporter 作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载 html,所以,我们不应该直接依赖太具体的发送对象 HtmlRequest。从这一点上讲,NetworkTransporter 类的设计违背迪米特法则,依赖了不该有直接依赖关系的 HtmlRequest。

NetworkTransporter 类如何满足迪米特法则呢?很简单,只需要传递 NetworkTransporter 所需的参数就行,而不是强依赖 HtmlRequest。

public class NetworkTransporter {
	// 省略属性和其他方法...
	public Byte[] send(String address, Byte[] data) {
		// ...
	}
}

public class HtmlDownloader {
	private NetworkTransporter transporter; // 通过构造函数或IOC注入

	public Html downloadHtml(String url) {
		// 同步需要修改处理
		HtmlRequest htmlRequest = new HtmlRequest(url);
		Byte[] rawHtml = transporter.send(htmlRequest.getAddress(), htmlRequest.getContent().getBytes());
		return new Html(rawHtml);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

Document 类的问题比较多,主要有三点:

  • 构造函数的 downloader.downloadHtml() 逻辑复杂,耗时长,不应该放到构造函数中,会影响代码可测试性

  • HtmlDownloader 在构造函数中通过 new 创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性

  • Document 没必要依赖 HtmlDownloader,违背了迪米特法则

虽然问题很多,但是修改起来还是比较简单:

public class Document {
	private Html html;
	private String url;
	
	// 改动1:去除了在构造函数创建 HtmlDownloader
	public Document(String url, Html html) {
		this.html = html;
		this.url = url;
	}
	// ...
}

// 改动2:通过工厂方法创建 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);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

有依赖关系的类之间,尽量只依赖必要的接口

迪米特法则的后半部分:有依赖关系的类之间,尽量只依赖必要的接口。同样举例说明。

public class Serialization {
	
	public String serialize(Object object) {
		String serializedResult = ...;
		// ...
		return serializeResult;
	}
	
	public Object deserialize(String url) {
		Object deserializedResult = ...;
		// ...
		return deserializedResult;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

Serialization 负责对象的序列化和反序列化。但考虑某些场景,有些类只用到了序列化操作,而另一些类只用到了反序列化操作,那基于迪米特法则的后半部分 “有依赖关系的类之间,尽量只依赖必要的接口”,只用到序列化操作的那部分类不应该依赖反序列化接口,同理,只用到反序列化操作的那部分类不应该依赖序列化接口。

根据这个思路,我们应该将 Serialization 拆分成两个更小粒度的类,一个只负责序列化,一个只负责反序列化:

public class Serializer {
	public String serialize(Object object) {
		String serializedResult = ...;
		// ...
		return serializedResult;
	}
}

public class Deserializer {
	public Object deserialize(String str) {
		Object deserializedResult = ...;
		// ...
		return deserializedResult;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

尽管拆分之后的代码更能满足迪米特法则,但却违背了高内聚的设计思想。高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的地方不至于过于分散。

如果我们既不想违背高内聚的设计思想,也不想违背迪米特法则,如何解决这个问题呢?实际上,通过引入两个接口就能轻松解决这个问题:

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 DemoClass_1(Serializable serializer) {
		this.serializer = serializer;
	}
	// ...
}

public class DemoClass_2 {
	private Deserializable deserializer; // 只需要反序列化

	public DemoClass_2(Deserializable deserializer) {
		this.deserializer = deserializer;
	}
	// ...
}

Serialization serialization = new Serialization();
DemoClass_1 demoClass_1 = new DemoClass_1(serialization);
DemoClass_2 demoClass_2 = new DemoClass_2(serialization);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

尽管我们还是要往 DemoClass_1 的构造函数中传入包含序列化和反序列化的 Serialization,但是,我们依赖的 Serializable 接口只包含序列化操作,DemoClass_1 无法使用 Serialization 中的反序列化接口,对反序列化操作无感知,这也就符合了迪米特法则后半部分说的”依赖有限接口“的要求。

辩证思考和灵活应用

对于 Serialization 序列化和反序列化,第一种方式是全部写在 Serialization 类中,第二种是使用接口拆分。那为了满足迪米特法则,我们将一个非常简单的类拆分出两个接口,是否有点过度设计的意思呢?

设计原则本身没有对错,只有能否用对之说。不要为了应用设计原则而应用设计原则,我们在应用设计原则的时候,一定要具体问题具体分析。

对于刚刚的 Serialization 来说,只包含两个操作,确实没太大必要拆分成两个接口。但是,如果我们对 Serialization 添加更多的功能,实现更多更好用的序列化、反序列化函数,就需要重新考虑一下这个问题:

public class Serializer {
	public String serialize(Object object) { //... }
	public String serializeMap(Map map) { //... }
	public String serializeList(List list) { //... }

	public Object deserialize(String objectString) { //... }
	public Map deserializeMap(String mapString) { //... }
	public List deserializeList(String listString) { //... }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这种场景下,将序列化和反序列化拆分为两个接口的方式会更好些,这样能够减少耦合和测试工作量,将序列化和反序列化的功能隔离开来。

总结

1、如何理解 ”高内聚、松耦合“?

”高内聚、松耦合“ 是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。

”高内聚“ 用来指导类本身的设计,”松耦合“ 用来指导类与类之间依赖关系的设计。

高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。

松耦合,指的是在代码中,类与类之间的依赖关系简单清晰。

2、如何理解 ”迪米特法则“?

不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。

不能因为要应用设计原则而应用设计原则,要考虑实际的开发场景具体问题具体分析。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/488486
推荐阅读
相关标签
  

闽ICP备14008679号