设计模式专栏:http://t.csdnimg.cn/3a25S
目录
1.引用
2.何为"高内聚、低耦合"
3.LoD 的定义描述
4.定义解读与代码示例一
5.定义解读与代码示例二
1.引用
本节介绍最后一个设计原则:LoD(Law of Demeter,迪米特法则)。尽LoD不像SOLID、KISS和DRY原则那样被广大程序员熟知,但它非常实用。这条设计原能够帮助我们实现代码的“高内聚、低耦合”。
2.何为"高内聚、低耦合"
"高内聚、低耦合"是一个非常重要的设计思想,能够有效地提高代码的可读性和可性,能够缩小功能改动引起的代码改动范围。实际上,在之前,我们已经多次提这个设计思想。很多设计原则都以实现代码的“高内聚、低耦合”为目标,如单一职责原则基于接口而非实现编程等。
"高内聚、低耦合"是一个通用的设计思想,可以用来指导系统、模块、类和函数的计开发,也可以应用到微服务、框架、组件和类库等的设计开发中。为了讲解方便,我们“类”作为这个设计思想的应用对象,至于其他应用场景,读者可以自行类比。
"高内聚"用来指导类本身的设计,指的是相近的功能应该放到同一个类中,不相近的功能不要放到同一类中、相近的功能往往会被同时修改,如果放到同一个类中,那么代码可集中修改,也容易维护。单一职责原则是实现代码高内聚的有效的设计原则。
"低耦合"用来指导类之间依赖关系的设计,指的是在代码中,类之间的依赖关系要简单、清晰。即使两个类有依赖关系,一个类的代码的改动不会或很少导致依赖类的代码的改动。前者提到的依赖注入、接口隔离和基于接口而非实现编程,以及本节介绍的LoD,都是为了实现代码的低耦合。
注意,"内聚"和"耦合"并非完全独立,“高内聚”有助于“低耦合”。同理,“低内聚”会导致“高耦合”。例如,下图左边所示的代码结构呈现“高内聚、低耦合”,右边所示的代码结构呈现“低内聚、高耦合”。
在上面左边所示的代码结构中,每个类的职责单一,不同的功能被放到不同的类中,代码的内聚性高。因为职责单一,所以每个类被依赖的类就会比较少,代码的耦合度低,一个类的修改只会影响一个依赖类的代码的改动。在上图右边所示的代码结构中,类的职责不够单一功能大而全,不相近的功能放到了同一个类中,导致依赖关系复杂。在这种情况下,当我们需要修改某个类时,影响的类比较多。从上图我们可以看出,高内聚、低耦合的代码的结构更加简单、清晰,相应地,代码的可维护性和可读性更好。
3.LoD 的定义描述
单从"LoD"这个名字来看,我们完全猜不出这条设计原则讲的是什么。其实,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 strangers.”对应的中文为:每个模块(unit)只应该了解那些与它关系密切的模块(unit: only units“closely" related to the current unit)的有限知识(knowledge),或者说,每个模块只和自己的“朋友”“说话”(talk),不和“陌生人”“说话”。
大部分设计原则和设计思想都非常抽象,不同的人可能有不同的解读,如果我们想要将它们灵活地应用到实际开发中,那么需要实战经验支撑,LoD也不例外。于是,作者结合自己易理解和以往的经验,对LoD的定文进行了重新描述,不应该存在直接依赖关系的类之间不要有依赖,有依赖关系的类之间尽量只依赖必要的接口(也就是上面LoD定义描述中的“有限知识”),注意,为了讲解统一,作者把原定义描述中的“模块”替换成了“类”。
从上面作者给出的描述中,我们可以看出,LoD包含前后两部分,这两个部分讲的是两件事情,下面通过两个代码示例进行解读。
4.定义解读与代码示例一
我们先来看作者给出的LoD定义描述中的前半部分:应该存在直接依赖关系的类之间不要有依赖。我们通过一个简单的代码示例进行解读,在这个代码示例中,我们实现了简化的搜索引擎“爬取”网页的功能。这段代的包合3个类,其中,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 HtmlReyuest(url));
return new Html(rawHtml );
}
}
public class Document{
private Html html;
private String url;
public Document(String url){
this.url = url;
HtmlDownloader downloader = new HtmlDownloeder();
this.html = downloader.downloadHtml(url);
}
}
虽然上述代码能够实现基本功能,但存在较多设计缺陷。我们先来分析NetworkTransporter类。NetworkTransporter类作为一个底层网络通信类我们希望它的功能是通用的,而不只是服务于下载HTML网页,因此,它不应该直接依HtmlRequest类。从这一点上来讲,NetworkTransporter类的设计违反 LoD。
如何重构NetworkTransporter类才能满足LoD呢?我们举一个比较形象的例子,假如我们去商店买东西,在结账的时候,肯定不会直接把钱包给收营员,让收银员自己重里面拿钱,而类中的address和content(HtmlRequest类的定义在上面的代码中并为给出),它包含address类相当于收银员。我们应该把address和content交给NetworkTransporter类,而非直接把HtmlRequest类交给NetworkTransporter类,让NetworkTransporter自己取出address和content。根据这个思路,我们对NetworkTransporter类进行重构,重构后的代码如下所示:
public class Networkrransporter {
//...省略属性和其他方法..
public Bytel[] send(String address, Byte[] content){
...
}
}
我们再来分析 HtmlDownloader类。HtmlDownloader类原来的设计是没有问题的,不过我们修改了 NetworkTransporer 类中 sond()函数的定义,而 HtmlDownloader类调用了send()函数,因此,HtmlDownloader类也要做相应的修改。修改后的代码如下所示。
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类。Document类中存在下列3个问题。第一,构造函数中的downloader.downloadHtml()的逻辑比较复杂,执行耗时长,不方便测试,因此它不应该放到构造函数中。第二,HtmlDownloader 类的对象在构造函数中通过new创建,违反了基于接口面非实现编程的设计思想,也降低了代码的可测试性。第三,Document类依赖了不该依赖的HtmlDownloader类,违反了LoD。
虽然Document类中有3个问题,但修改一处即可解决所有问题。修改之后的代码如下所示。
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);
}
}
5.定义解读与代码示例二
现在,我们再来看一下作者给出LoD定义描述中后半部分:“有依赖关系的类之间尽量只依赖必要的接口”。我们还是结合一个代码示例进行讲解。下面这段代码中的Serialization 类负责对象的序列化和反序列化。
public class Serialization {
public String serialize(Object object){
String serializedResult = ...;
...
return serializedResult;
}
public object deserialize(String str){
Object deserializedResult = ...;
...
return deserializedResult;
}
}
单看 Serialization类的设计,一点问题都没有。不过,如果把 Serialization 类放到一定应用场景中,如有些类只用到了序列化操作,而另一些类只用到了反序列化操作,那么,于“有依赖关系的类之间尽量只依赖必要的接口”,只用到序列化操作的那些类不应该依赖反序列化接口,只用到反序列化操作的那些类不应该依赖序列化接口,因此,我们应该Serialization类拆分为两个更小粒度的类,一个类(Serializer类)只负责序列化,另一个类(Deserializer 类)只负责反序列化。拆分之后,使用序列化操作的类只需要依赖Serializar类使用反序列化操作的类只需要依赖 Deserializer类。拆分之后的代码如下所示。
public class Serializer{
public string serialize(0bject object){
String serializedResult = ...;
...
return serializedResult;
}
}
public class Deserializer {
public object deserialize(String str){
0bject deserializedResult = ...;
...
return deserializedResult;
}
}
不过,尽管拆分之后的代码满足LoD,但违反了高内聚的设计思想。高内聚要求相近的功能在同一个类中实现,当需要修改功能时,修改之处不会分散。对于上面的这个例子,如果修改了序列化的实现方式,如从JSON换成XML, 那么反序列化的实现方式也需要一并修改,也就是说,在Serialization类未拆分之前,只需要修改一个类,而在拆分之后,需要修改两个类。显然,拆分之后的代码的改动范围变大了。
如果我们既不想违反高内聚的设计思想,又不想违反LoD,那么怎么办呢?实际上,引入两个接口就能轻松解决这个问题。具体代码如下所示。
public interface serializable{
String serialize(0bject 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){
0bject deserializedResult = ...;
...
return deserializedResult;
}
public class DemoClass_l{
private Serializable serializer;
public Demo(Serializable serializer){
this.serializer = serializer;
}
...
}
public class Democlass_2{
private Deserializable deserializer;
public Demo(Deserializable deserializer){
this.deserializer = deserializer;
}
....
}
尽管我们还是需要向DemoClass_1类的构造函数中传入同时包含序列化和反序列化操作的Serialization类,但是,DemoClass_1类依赖的Seializable接口只包含序列化操作,因此DemoClass_1类无法使用Serialization类中的反序列化函数,即对反序列化操作无“感知”,这就符合了作者给出的LoD定义描述的后半部分“有依赖关系的类之间尽量只依赖必要的接口”的要求。
Serialization类包含序列化和反序列化两个操作,只使用序列化操作的使用者即便能够“感知”到另一个函数(反序列化函数),其实也是可以接受的,那么,为了满足LoD,将一个简单的类拆分成两个接口,是否是过度设计呢?
设计原则本身没有对错。判定设计模式的应用是否合理,我们结合应用场景,具体问题具体分析。
对于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){...}
}