这边文章主要来源极客时间的设计模式之美,非常棒的一个教程,大家一定要买这个课程!一定要!
什么是高内聚
所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。 相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
什么是松耦合
所谓松耦合是说,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一
个类的代码改动不会或者很少导致依赖类的代码改动。
内聚和耦合的关系
“高内聚”有助于“松耦合”,同理,“低内聚”也会导致“紧耦合”
什么是迪米特法则?
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 类满足迪米特法则呢?我这里有个形象 的比喻。假如你现在要去商店买东西,你肯定不会直接把钱包给收银员,让收银员自己从里面拿钱,而是你从钱包里把钱拿出来交给收银员。这里的 HtmlRequest 对象就相当于钱 包,HtmlRequest 里的 address 和 content 对象就相当于钱。我们应该把 address 和 content 交给 NetworkTransporter,而非是直接把 HtmlRequest 交给NetworkTransporter。
send(String address, Byte[] data)
HtmlDownloader
这个类没什么问题,只需要把入参修改一下
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;
}
}
//工厂对象
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 serializedResult;
}
public Object deserialize(String str) {
Object deserializedResult = ...;
//...
return deserializedResult;
}
}
&emsp单看这个类的设计,没有一点问题。不过,如果我们把它放到一定的应用场景里,那就还有继续优化的空间。假设在我们的项目中,有些类只用到了序列化操作,而另一些类只用到反序列化操作。那基于迪米特法则后半部分“有依赖关系的类之间,尽量只依赖必要的接口”,只用到序列化操作的那部分类不应该依赖反序列化接口。同理,只用到反序列化操作的那部分类不应该依赖序列化接口。
&emsp根据这个思路,我们应该将 Serialization 类拆分为两个更小粒度的类,一个只负责序列化 (Serializer 类),一个只负责反序列化(Deserializer 类)。拆分之后,使用序列化操作 的类只需要依赖 Serializer 类,使用反序列化操作的类只需要依赖 Deserializer 类。拆分 之后的代码如下所示:
public class Serializer{
public String serialize(Object object) {
String serializedResult = ...;
//...
return serializedResult;
}
}
public class Deserializer{
public Object deserialize(String str) {
Object deserializedResult = ...;
//...
return deserializedResult;
}
}
&emsp不知道你有没有看出来,尽管拆分之后的代码更能满足迪米特法则,但却违背了高内聚的设 计思想。高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的 地方不至于过于分散。对于刚刚这个例子来说,如果我们修改了序列化的实现方式,比如从 JSON 换成了 XML,那反序列化的实现逻辑也需要一并修改。在未拆分的情况下,我们只需要修改一个类即可。在拆分之后,我们需要修改两个类。显然,这种设计思路的代码改动范围变大了。
&emsp如果我们既不想违背高内聚的设计思想,也不想违背迪米特法则,那我们该如何解决这个问 题呢?实际上,通过引入两个接口就能轻松解决这个问题,具体的代码如下所示。实际上, 我们在第 18 节课中讲到“接口隔离原则”的时候,第三个例子就使用了类似的实现思 路,你可以结合着一块儿来看。