LoD:如何实现代码的“高内聚、低耦合“

设计模式专栏: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){...}
}
  • 13
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值