赞
踩
“高内聚、松耦合” 是一个非常重要的设计思想,能够有效地提高代码地可读性和可维护性,缩小功能改动导致的代码改动范围。很多设计原则都以实现代码的“高内聚、松耦合”为目的,比如单一职责、基于接口而非实现编程等。
“高内聚、松耦合” 可以用来指导不同粒度代码的设计与开发,“高内聚” 用来指导类本身的设计,“松耦合” 用来指导类与类之间依赖关系的设计。不过,这两者并非完全独立不相干,高内聚有助于松耦合,松耦合又需要高内聚的支持。
所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
所谓松耦合,就是类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或很少导致依赖类的代码改动。比如依赖注入、接口隔离、基于接口而非实现编程,还有待会要讲的迪米特法则,都是为了实现代码的松耦合。
“高内聚” 有助于 “松耦合”,同理,”低内聚“ 也会导致 ”紧耦合“。上图左边是 ”高内聚、松耦合“ 的代码结构,右边是 ”低内聚、紧耦合“。
左边部分的代码设计中,类的粒度比较小,每个类的职责都比较单一,相近的功能都放到了一个类中,不相近的功能被分割到了多个类中,这样类更加独立,代码的内聚性更好。因为职责单一,所以每个类被依赖的类就会比较少,代码低耦合。一个类的修改,只会影响到一个依赖类的代码改动。我们只需要测试这一个依赖类是否还能正常工作就行。高内聚低耦合的代码结构更加简单、清晰,可维护性和可读性上要好很多。
右边部分的代码设计中,类粒度比较大,类功能大而全,不相近的功能放到了一个类中,导致很多其他类依赖这个类。当要修改这个类的某一个功能时,”牵一发而动全身“会影响依赖它的多个类。我们需要测试这三个依赖类是否能正常工作。
迪米特法则(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); } // ... }
这段代码虽然 “能用”,但是它不够 “好用”。
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); } }
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); } }
迪米特法则的后半部分:有依赖关系的类之间,尽量只依赖必要的接口。同样举例说明。
public class Serialization {
public String serialize(Object object) {
String serializedResult = ...;
// ...
return serializeResult;
}
public Object deserialize(String url) {
Object deserializedResult = ...;
// ...
return deserializedResult;
}
}
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;
}
}
尽管拆分之后的代码更能满足迪米特法则,但却违背了高内聚的设计思想。高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的地方不至于过于分散。
如果我们既不想违背高内聚的设计思想,也不想违背迪米特法则,如何解决这个问题呢?实际上,通过引入两个接口就能轻松解决这个问题:
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);
尽管我们还是要往 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、如何理解 ”迪米特法则“?
不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。
不能因为要应用设计原则而应用设计原则,要考虑实际的开发场景具体问题具体分析。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。