迪米特法则(迪米特法则的具体实现)

张工 2022-06-18 18:52:46 阅读:19
  

  设计模式目录

  

  设计模式是实现七大设计原则的具体方式。在实际开发中,设计模式不应该被机械地复制,而应该被灵活地应用,以反映七个设计原则。归根结底,设计模式是七个设计原则。

  在实践中,要根据需要选择合适的设计模式,不能为了使用而使用设计模式。建议如果一开始没有合适的设计模式,可以考虑不用设计模式,但是一定要遵守七大设计原则。还要避免过度设计,降低程序的复杂度非常重要,因为设计模式是不能强行移动的。

  你不仅可以通过学习经典的23种设计模式更深入地学习七大设计原则,还可以通过优秀的开源代码学习设计模式和七大设计原则,还可以在积累编程中积累开发经验。

七大设计原则

  

  七大设计原则的目的是提高系统的可重用性、可靠性、可读性和可扩展性,最终达到低耦合、高内聚。

  你觉得你在哪里见过它吗?

  是的,我也写了设计模式的目的。可以说,七大设计原则就是围绕这些目的制定的法规。

前言

  这段时间也比较自由,所以想学习提升自己。

  之前一直听说七大设计原则,有五大六大。其实都差不多,只是多多少少的问题。这七个设计原则是前人总结的面向对象开发的经验,也是23个设计模式的基础。我认为学习它们是非常必要的。相信学习了它们之后,对提高自己的编程水平和确定未来的方向会有很大的帮助。我之前一直不理会他们,而是先去学了设计模式,本末倒置。

  文末提供了我个人的采访答案,可以直接拉到最下面。

开放封闭原则

  

  开闭原理(OCP)是由贝特朗迈耶提出的。在他1988年的书《面向对象软件构造》(面向对象的软件构造)中,他提出软件实体应该对扩展开放,对修改关闭。

  软件实体:

  项目中划分的模块

  类接口

  方法

「概念」

  软件实体应该是「对扩展开放,对修改封闭」.这意味着软件实体应该通过扩展新代码来实现变化,而不是修改原始代码。

「目的」

  减少软件测试的影响。遵循打开和关闭的原则,在添加每个新代码后,只需要测试新扩展的代码。降低开发风险。每次添加新的需求或者系统发生变化,都可以通过添加不同的代码来完成,而不是修改原代码,出现错误时可以使用原代码。* *提高软件的可靠性和可扩展性。* *遵循开放和关闭原则,更易于扩展和维护。提高代码的「重用性」

「实现」

  如果你想用开闭原则,可以试试下面的规则:

t="1">
  • 「抽象约束,封闭变化」
  • 在Java(面向对象的语言)中想要实现开放封闭原则,可以通过“抽象约束,封闭变化”这条规则来实现。

    “抽象约束,封闭变化”具体的做法就是,定义一个「稳定(不再修改)的抽象类或者接口」,用于对扩展边界的限定,然后将「细节变化写到实现类中」,有新的变化就创建一个新的实体类,想要使用这些实现都要使用接口或者抽象类作为引用对象。


    「设计模式」设计模式之根:七大设计原则


      

    像这里,C类只访问接口的引用对象,这个应用对象的具体实现就由A类和B类来实现。

    这是针对「类与接口」的实体。

    1. 「根据需求分析」

    在模块设计时,应该考虑模块内部那些是不需要修改的,不需要修改的都是一些底层的代码,这些没什么好说的,好好写就行了;对短期内可能扩展的、或者是扩展成本低的就事先做好设计,增加可控制的「扩展点」,例如,通过配置文件扩展(例如想换另外一个处理类,就直接写一个新的处理类,然后修改配置文件)、通过传入参数进行扩展(通过参数调用不同的处理类)等等(这个跟个人开发水平有关,我这边也只是我的个人经验),尽量减少对原有代码的修改。

    这个针对「模块以及方法」

    「总结」

    其他的设计原则都是以开放封闭原则作为目标的,也就是实现了其他的原则,就实现了开放封闭原则。开放封闭原则也是所有软件设计的目标――「不修改代码就能够完成业务」,当然在软件没有开发完成之前还是需要修改以及新增代码的。

    里氏替换原则

      

    里氏替换原则(Liskov Substitution Principle,LSP)由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy)里提出来的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。

    继承:在面向对象语言中,继承就是子类能够继承父类中的成员属性、成员方法以及构造方法。

    继承的优点:

      实现代码共享,提高代码的重用性

      提高代码的可扩展性,子类可以有自己的特性

    继承的缺点:

      继承是侵入性的,子类拥有父类的属性和方法,子类在一定程度上被约束了,这降低了代码的灵活性

      增加了耦合度,一旦父类方法修改,那么子类方法就可能需要修改

      

    「概念」

    里氏替换原则主要阐述了有关「继承」的原则,通过学习里氏替换原则,也就能掌握什么时候该用继承,什么时候不该用继承。

      

    里氏替换原则通俗来讲就是:「子类可以扩展父类的功能,但不能改变父类原有的功能。」

      

    子类可以替换父类,且没有影响,也就是在父类出现的地方可以直接使用子类去替换,反过来就不一定可以。

    「目的」

    1. **提高代码的可重用性。**里氏替换原则克服了继承重写父类造成的可复用性变差的缺点。
    2. 里氏替换原则给出了「继承的正确使用方式」,不会因为类的扩展而给系统引入新的错误。
    3. 加强了程序的「可靠性、可维护性」

    「实现」

    里氏替换原则的使用规则:

    1. 「子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法」(公共方法)。
    2. 子类可以「增加自己特有的方法」
    3. 当子类重载父类的方法时,方法的「输入参数」要比父类的「更宽松」(宽松指的是可以使用接口来代替类)。
    4. 当子类实现父类的抽象方法时,方法的「返回值」要比父类的「更严格或相等」(严格是指使用具体的实现类来代替接口或抽象类)。

    这么多的使用规则其实就是为了满足,**程序中的父类能够被子类代替,而子类不一定能够被父类代替。**也是仅有符合以上四条规则才能够使用继承,不然就得考虑使用其他的方式来实现了。

    「总结」

    个人感觉,里氏替换原则「限制了多态的运用」,减少了不必要的多态,从而降低了使用继承带来的复杂程度。”子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。“我个人认为也是这一条规则比较重要,这条规则能够降低子类和父类的耦合程度,如果不遵守这条规则,那么子类和父类就跟两个没有关系的类一样了。

    依赖倒置原则

    「概念」

      

    依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;「抽象不应该依赖细节,细节应该依赖抽象」。其核心思想是:要面向接口编程,不要面向实现编程。

    低层模块一般是指被调用的那些模块,像连接数据库、发送网络请求、操作io这些模块;而高层次就是调用低层次去实现业务逻辑的模块。

      

    依赖倒置原则是在提倡「面向接口编程」

    目的

    1. 「降低类间的耦合度」。使用接口或抽象类作引用对象,可以很好地更换其他的子类。
    2. 减少并行开发的风险。使用接口或抽象类作引用对象,可以减少多人并行开发的矛盾。
    3. **提高代码的可读性。**我们在看到某个接口后就能知道这个接口引用的对象是干嘛的,可以不去深究它的底层。
    4. 提高了系统的「可扩展性」。使用接口作声明对象的话,就可以引用这个接口下的对象了。

    实现

    依赖倒置原则的使用规则:

    1. 每个类都要「实现接口或者抽象类」,或者两者都有。
    2. **变量的声明类型尽量使用接口或者抽象类。**不要直接把声明指向实现类,不方便以后想切换到其他类型对象。
    3. 使用继承时遵循「里氏替换原则」

    「总结」

    依赖倒置和里氏替换有点类似,依赖倒置是在提倡面向接口编程,而里氏替换是针对于继承的使用做出一些规定,依赖倒置使用到了里氏替换原则以及后面的接口隔离原则。

    个人感觉”变量的声明使用接口和抽象类“,这条规则可以使得我们的模块或者方法的适用范围更加的广泛,不单单只适用于一个实现类,降低了类之间的耦合程度。

    单一职责原则

      

    单一职责原则(Single Responsibility Principle,SRP)又称单一功能原则,由罗伯特C.马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(There should never be more than one reason for a class to change)。

      

    「概念」

    官方的定义就是,一个类应该有且仅有一个引起它变化的原因。

    换句话说就是,一个类的职责(做的事情)应该是单一的,不应该一个类同时有多个职责。这里的「职责划分」就非常重要了,我觉得这个原则的概念不需要再说了。

    「目的」

    1. 降低类的「复杂度」「耦合程度」,提高类的「内聚」「重用性」
    2. 提高类的「可读性」
    3. 提高代码的「可靠性」
    4. 降低修改代码引起的风险。

    实现

    单一职责原则说起来简单,但是实际运用起来却非常的难。想要使用单一职责原则,一句话就是「做好类的、模块的职责划分,控制职责的粒度」。而职责的划分需要设计人员需要有一定的经验和对需求的分析,尽量安排一个类只作某一方面的事。

    我按照我的经验举个例子,我们都需要连接数据库还有做业务逻辑,如果把这两件事情都安排到一个类上,那么连接数据库的操作也不能得到复用,每次修改业务逻辑也会变得非常麻烦。还有数据库、中间件这些分离也是为了保证每个东西只做一件事情,数据库只管理数据,中间件像kafka只做消息队列,这也让他们有很好的复用性,功能也非常的强大。

    总结

    个人感觉这单一职责原则是「让类或者方法以及模块内部更加独立」「降低因需求变化而修改类的可能」。但是想要用好单一职责原则,就得靠个人的开发经验了,在每次设计类的结构时,都得考虑怎么样能够让一个类做的事情都是在某一方面内的?这样做会不会增加开发难度?

    接口隔离原则

      

    2002 年罗伯特C.马丁给“接口隔离原则”的定义是:客户端不应该被迫依赖于它不使用的方法(Clients should not be forced to depend on methods they do not use)。该原则还有另外一个定义:一个类对另一个类的依赖应该建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。

    接口在java中是可以用来当作引用对象的,然后这个引用对象只会提供接口中声明的抽象方法。

      

    「概念」

    一个类对另一个类的依赖应该建立在最小的接口上,客户端程序不应该依赖它不需要的接口方法。简单点说就是,要为使用者提供它使用的方法,尽量屏蔽掉不会用到的方法。

    「目的」

    1. 降低接口的「复杂度」,提高接口的内聚,降低了系统的耦合程度。
    2. 提高类的「可读性、可扩展性、可重用性」
    3. 减少冗余代码。减少因接口过大而导致的被迫实现。

    「实现」

    使用接口隔离原则的规则:

    1. 接口尽量不要定义的太庞大,应该一个接口就定义做某一类型的方法。
    2. 定制接口,只提供调用者想要的方法。
    3. 提高内聚,使接口使用最少的方法就能够完成需求。

    尽量地将同一类型的操作赋给一个接口,一个接口不需要提供太多操作。

    这里说一下接口的分离方式:

    1. **使用委托分离接口。**这个我就不是很理解了,我想应该是通过在委托时判断该使用哪个接口去处理。
    2. **使用多重继承分离接口。**这个还挺好说的,就是把一个接口分成多个小接口,通过java的接口多继承合在一起。

    总结

    接口隔离原则和单一职责原则有点类似,接口隔离是针对与接口的,单一职责是针对类的,不过接口隔离在某种程度上也是对单一职责起到了作用,通过接口职责的划分,也能完成类职责的划分。「接口隔离是约束实现这个接口的类和系统的整体接口,而单一职责是约束类的实现内容。」

    总体来说,「接口隔离、里氏替换和单一职责」分别对「接口、继承和类的实现」做出了规定,「依赖倒置」则是提倡了「接口编程」,和这三个原则的作用是相互辅助的。

    迪米特法则

      

    迪米特法则(Law of Demeter,LoD)又叫作最少知识原则(Least Knowledge Principle,LKP),产生于 1987 年美国东北大学(Northeastern University)的一个名为迪米特(Demeter)的研究项目,由伊恩荷兰(Ian Holland)提出,被 UML 创始者之一的布奇(Booch)普及,后来又因为在经典著作《程序员修炼之道》(The Pragmatic Programmer)提及而广为人知。

      

    「概念」

    迪米特法则(Law of Demeter ):「talk only to your immediate friends(只和直接的朋友交流)」,一个对象应当对其他对象尽可能少的了解。不和陌生人说话。也就是说**,一个软件实体应当尽可能少地与其他实体发生相互作用**(调用其他实体的方法)。

    强调降低耦合,尽可能减少类之间的交互和依赖,这样也能够提高类的可重用性。

    目的

    1. 降低类之间的「耦合度」和依赖,提高类的独立程度,模块也适用。
    2. 提高类的「可重用性」和系统的「可扩展性」

    实现

    迪米特法则强调一个类应该和“朋友”说话,这里的「朋友指的是成员变量、方法的入参和出参」,而在方法体内部的就不是“朋友”了,可以说迪米特法则提倡不在方法体内创建对象。

    使用迪米特法则的规则:

    1. 在类的设计上,应该「优先创建一个不会再改变的类」
    2. 应该创建弱耦合的类,提高类的独立性。可以考虑运用「单一职责原则」
    3. 「降低类成员方法的访问权限」。只提供必要的方法给其他类使用。
    4. 「不暴露成员属性」,提供get、set方法。减少越过朋友直接和陌生人交流的情况。
    5. 「降低对其他对象的引用次数」

    总结

    迪米特法则的核心在于减少每个类对其它类的依赖,也就是说一个类明面上可能最多依赖几个“朋友”类,然后这几个“朋友”类会再去依赖它的“朋友”类,而不是一个类直接依赖多个类,这样能够让每个类形成一个小模块,有一定的独立性。

    过多的使用迪米特法则可能会导致中间类过多的情况,会导致系统变得复杂,降低模块间的通信效率,所以在使用的时候应该适当地考虑一下业务需求,在保证不会增加系统复杂性、会让模块间的关系更加清晰的情况下才去使用迪米特法则。

    合成复用原则

    「概念」

    合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,「要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现」

    目的

    1. 提高系统的封装性,「提高了内聚」。因为继承会把父类的成员变量和方法的实现都暴露给子类,一旦父类修改,子类也得修改。
    2. 「使系统更加灵活,降低类之间的耦合程度」。类之间的耦合关系都不高,要用就创建,不用就直接删掉,不需要改类的逻辑。
    3. 提高了类的「可重用性」

    实现

    合成复用原则没有太多的规则,只需要遵守「把已有的对象作为成员对象来使用」,即可达到复用,在创建成员对象时需要遵守依赖倒置原则。

    总结

    复用分为继承复用和合成复用。

    继承复用,就是通过继承的方式来复用父类中的方法以及成员属性;合成复用,就是通过直接调用已有对象来达到复用。

    「继承是一种侵入性很高的方式」,父类和子类的耦合程度会大大地提高,一旦父类发生改变子类也会发生改变,而且父类的方法以及成员属性都会暴露给子类,破坏了封装性和降低了灵活性。

    而合成复用就能够减少这种侵入,但是也并不是说就不要使用继承了,在某些情况下还是使用继承会比较合适,例如,某几个同样的方法都需要在某几个类中使用,可能成员属性也要用到,这几个类又是在同一模块内的,这种情况下采用继承的方式实现会更加方便,如果采用合成复用,可能会增加编程难度。但是在「使用继承的时候一定要遵守里氏替换原则」

    最后

    以上就是对七大设计原则的介绍,这些原则的终极目的就是「降低对象之间的耦合,提高类的内聚性,增加程序的可靠性、可读性、可扩展性」。23种设计模式也都是围绕着这七大设计原则进行的,在后来学习设计模式的时候,可以看到这七大原则更多的影子。

      

    这是我从网上看到的一个记忆口诀。

    记忆口诀:访问加限制,函数要节俭,依赖不允许,动态加接口,父类要抽象,扩展不更改。

      

    虽说在开发过程中遵守这七大原则是我们进行软件设计的基础,但是想要在实际开发过程中完全遵守这七大原则可能会给开发带来更大的成本,所以我们应该综合实际的开发场景,结合成本,不追求完美遵守这七大原则,有取有舍,才能够让这七大原则帮助我们设计出更加适合当下的、更加优美的代码结构。

    在这七大设计原则中,「里氏替换原则和依赖倒置原则是必须要遵守的」,这两个原则都不需要进行权衡,里氏替换是在使用继承时就要用到的,所以权衡的是是否需要使用继承,而不是是否使用这条原则。而其他原则都是需要一定的权衡和开发经验的积累,我觉得「开放封闭原则和接口隔离原则」还是最好要遵守的,不存在什么需要权衡的,不过具体的实现需要一定的开发经验;「单一职责原则、迪米特法则和合成复用原则」就需要根据具体的业务来判断是否要遵守、遵守的程度要如何,也是需要一定的开发经验。可以说除了里氏替换原则和依赖倒置,其他的都需要在日常的开发中积累经验,这需要我们在日常开发中留个心眼。

    我在这里列一个表,把七个原则对应的特性都列了出来,+代表符合,-代表不符合。

    原则 可重用性 可靠性 可读性 可扩展性 低耦合高内聚 复杂度 开放封闭原则 + + - + + + 里氏替换原则 + + - + - - 依赖倒置原则 + + + + + - 单一职责原则 + + + - + - 接口隔离原则 + + + + + - 迪米特法则 + - - + + + 合成复用原则 + + - + + +

    面试模板

    我在这里提供一个「面试的回答模板」,大家可以改一改就直接拿去用了。我大概面试了将近三十次吧,也就是遇到一次,大家也可以把这个当作一个总结来看待,这也只是我的个人回答,有更好的回答模板也可以发出来一起讨论一下。

    面试官问:你对七大设计原则有了解吗?

    答:

    我了解过的,七大设计原则的目的是「降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性」。可以说是面向对象程序设计必须要遵守的七大准则。

    七大设计原则有「开放封闭原则」,对扩展开放,对修改关闭,意思就是不修改已经写好的代码,只能写新的代码来扩展新的功能,它能够降低每次改动对系统带来的风险。「里氏替换原则」,子类可以扩展父类的功能,但不能改变父类原有的功能,具体的使用就是子类不要去重写父类中的方法,它规定了继承的使用方法,可以减少多态的滥用,也减少了父子类的耦合程度。「依赖倒置原则」,高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;简单来说就是它提倡我们使用接口编程,这样能够降低类之间的耦合程度,有利于代码的升级扩展。「单一职责原则」,一个类应该有且仅有一个引起它变化的原因,也就是一个类应该只做一件事,这样可以提高代码的阅读性,提高类的独立性。「接口隔离原则」,一个类对另一个类的依赖应该建立在最小的接口上;客户端程序不应该依赖它不需要的接口方法,也就是一个接口要精简单一,太复杂的接口应该拆分成小接口,它能够降低接口的复杂度,从而提高接口以及实现类的内聚。「迪米特法则」,又叫最少知识法则,一个软件实体应当尽可能少地与其他实体发生相互作用,每个类应该都只依赖于成员属性、方法的入参和返回值,其他的类都不应该直接产生依赖。「合成复用原则」,它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现,因为继承复用是会增加两个类的耦合程度的,而合成复用是不会增加耦合程度的,更加的灵活,有利于降低代码的耦合程度。(把前面的介绍说个七七八八就可以了,确实是需要背一些东西)

    虽然在开发的过程中最好遵守这七大设计原则,但是实际的运用还是要根据业务需求和实际场景来确定。

    最后的最后,我是没想到七大设计原则花了我将近五天的时间,可能内容确实有点多,加上也是很久没有写文章了,所以进度稍微有点慢了,这有点超出我的预期了,在后面学习设计模式的时候,我也会加快速度,争取在一个月内搞定,然后就可以开启我的源码之旅了。

    二维码