何为高内聚、低耦合
“高内聚、松耦合”是一个比较通用的设计思想,可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。
比如设计类,高内聚用来指导类本身的设计,送耦合指导类与类之间的关系设计,它们并非独立不相干。
高内聚
指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。实际上,我们前面讲过的单一职责原则是实现代码高内聚非常有效的设计原则。
松耦合
在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。如:依赖注入、接口隔离、基于接口而非实现编程、LOD 法则。
左边高内聚低耦合:
类的粒度比较小,每个类的职责都比较单一。相近的功能都放到了一个类中,不相近的功能被分割到了多个类中。这样类更加独立,代码的内聚性更好。因为职责单一,所以每个类被依赖的类就会比较少,代码低耦合
右边低内聚高耦合:
类粒度比较大,低内聚,功能大而全,不相近的功能放到了一个类中。这就导致很多其他类都依赖这个类。
LOD--Law Of Demeter
中文:迪米特法则,又叫最小知识原则
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.
不要和陌生人说话。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。
不该有直接依赖关系的类之间,不要有依赖
一个网页爬虫例子,使用 HtmlDownloader 下载网页内容,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 是通用的网络通信类,不应该直接依赖太具体的发送对象 HtmlRequest。就像去买东西,HtmlRequest 对象就相当于钱包,HtmlRequest 里的 address 和 content 对象就相当于钱。我们应该把 address 和 content 交给 NetworkTransporter,而非是直接把 HtmlRequest 交给 NetworkTransporter
Document 类:
第一,构造函数中的 downloader.downloadHtml() 逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。
第二,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;
public Document(Html html) {
this.html = html;
}
//...
}
// 通过一个工厂方法来创建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(html);
}
}
复制代码
有依赖关系的类之间,尽量只依赖必要的接口
下面有一个序列号反序列化实现:
public class Serialization {
public String serialize(Object object) {
String serializedResult = ...;
//...
return serializedResult;
}
public Object deserialize(String str) {
Object deserializedResult = ...;
//...
return deserializedResult;
}
}
复制代码
根据 LOD 原则后半部份,只用到序列化操作的那部分类不应该依赖反序列化接口。同理,只用到反序列化操作的那部分类不应该依赖序列化接口。那么我们将这个类拆分成 2 个:
public class Serializer {
public String serialize(Object object) {
String serializedResult = ...;
...
return serializedResult;
}
}
public class Deserializer {
public Object deserialize(String str) {
Object deserializedResult = ...;
...
return deserializedResult;
}
}
复制代码
这时虽然满足了 LOD,但是却违反了高内聚的设计思想,为什么?因为假如我需要修改序列化的规则,那么反序列化的实现也需要一并修改,未拆分时需要修改一个类即可,拆分后需要同时修改两个类。
那如何鱼与熊掌兼得呢?通过定义公共接口如下:
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;
}
//...
}
复制代码
只需要序列的类,只需要实现序列化接口;反序列化也一样。体现了基于接口而非实现编程的思想,结合 LOD,可以得出:基于最小接口而非最大实现编程。
讲道理,当前的功能确实没太大必要拆成两个接口;但是当 Serialization 承载更多功能时,就更能体现这个设计的好处:
/**
* 给旧的实现添加添加map、list功能
*/
public class Serializer { // 参看JSON的接口定义
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) { //... }
}
/**
* 新实现,添加map的序列号反序列化功能
*/
public class MapSerialization implements Serializable, Deserializable {
@Override
public String serialize(Object object) {
String serializedResult = ...;
...
return serializedResult;
}
@Override
public Object deserialize(String str) {
Object deserializedResult = ...;
...
return deserializedResult;
}
}
复制代码
对于只需要用到序列化的功能的使用者,没必要了解反序列化的“知识”,而修改之后的 Serialization 类,反序列化的“知识”,从一个函数变成了三个。一旦任一反序列化操作有代码改动,我们都需要检查、测试所有依赖 Serialization 类的代码是否还能正常工作。为了减少耦合和测试工作量,我们应该按照迪米特法则,将反序列化和序列化的功能隔离开来。
问题
“高内聚、松耦合”“单一职责原则”“接口隔离原则”“基于接口而非实现编程”“迪米特法则”,总结下它们之间的区别和联系?
1.单一职责原则 适用对象:模块,类,接口 侧重点:高内聚,低耦合 思考角度:自身
2.接口隔离原则 适用对象:接口,函数 侧重点:低耦合 思考角度:调用者
3.基于接口而非实现编程 适用对象:接口,抽象类 侧重点:低耦合 思考角度:调用者
4.迪米特法则 适用对象:模块,类 侧重点:低耦合 思考角度:类关系
评论