专栏地址:xiaozhuanlan.com/fullstack编程
做为开发人员,或多或少都会熟悉或了解一些设计模式,如单例模式、工厂模式、观察者模式等等。但并不是都能理解这些设计模式背后的本质,从而可能会致使对模式单纯的套用或滥用的状况出现。不要为了模式而模式,要明白使用模式的目的,要正确理解模式背后的设计原理,要理解背后的基本设计原则。后端
首先,咱们要明白使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。那么,若是咱们开发的应用并非为了这些目的,其实就不必使用设计模式,好比 Solidity 智能合约目前就不太适合直接套用设计模式。设计模式
其次,要理解设计模式背后一些重要的设计原则,全部设计模式基本都是基于这些设计原则总结出来的,这才是设计模式的本质和精髓所在。架构
人们总结出来的设计原则也不少,而从源头开始,GoF(Gang of Four)在《设计模式》一书中只提到两个设计原则:框架
后来的人们给上面两个设计原则分别起了专业的名字:依赖倒置原则和合成复用原则。并且,还总结出了其余设计原则,主要包括里氏替换原则、单一职责原则、接口隔离原则、迪米特法则、开闭原则等。接下来就详细阐述下这几个设计原则。前后端分离
依赖倒置原则(Dependence Inversion Principle,DIP),其原始定义为:函数
High level modules should not depend upon low level modules, Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstracts.翻译
翻译过来就是:设计
所谓抽象,就是指接口或抽象类;所谓细节,就是指实现了接口或继承了抽象类的具体实现类。上面内容便是说,模块之间的依赖关系,应该经过接口或抽象类而产生,模块的实现类之间不要发生直接的依赖关系;并且接口或抽象类不该该依赖于实现类,实现类应该依赖于接口或抽象类。其核心思想也是 GoF 所提的针对接口编程,而不是针对实现编程。cdn
咱们知道,具体实现类是颇有可能常常发生变动的,但接口或抽象类则不多会改变。所以,依赖于抽象,能够大大减低模块间的耦合度,以及能够提升模块的可复用性和程序的稳定性。不过,相应地,也会增长代码量。
不少设计模式都遵循了该原则,好比工厂类模式、观察者模式、适配器模式、策略模式等等。
在咱们平时的实际开发中,若是想提升代码的可重用性、扩展性,那就应该尽可能遵循该原则。可是,也不要陷入另外一个误区,就是每个类都抽象出一个对应的接口。
合成复用原则(Composite Reuse Principle,CRP),也称为组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP),该原则提出:优先使用合成复用,而不是继承复用。
咱们知道,类的复用有两种方式:合成与继承。合成便是组合或聚合。为何要优先使用合成复用呢?这是由于继承复用主要有两个缺陷:
使用合成复用则能够将已有对象(也称为成员对象)归入到新对象中,使之成为新对象的一部分,新对象能够调用已有对象的功能。所以,已有对象的内部实现细节对新对象就是不可见的,这就是黑箱复用,不会破坏类的封装性,其耦合度也相对较低,所以能够提升扩展性。
所以,须要复用时,咱们要优先考虑能不能使用合成,实在不合适才考虑继承。而使用继承时,还须要遵循另外一个设计原则:里氏替换原则。关于这个原则,后面再讲。
另外,使用合成复用时,还能够再结合上面的依赖倒置原则,让新对象和已有对象的交互经过接口或抽象类进行,从而能够更进一步减低耦合度。
里氏替换原则(Liskov Substitution Principle,LSP)主要用来规范如何正确地使用继承,其定义有两种:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
翻译:若是对每个类型为 S 的对象 o1,都有一个类型为 T 的对象 o2,使得以 T 定义的全部程序 P 在全部的对象 o1 都替换成 o2 时,程序 P 的行为没有发生变化,那么类型 S 是类型 T 的子类型。
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
翻译:全部引用基类的地方必须能透明地使用其子类的对象。
很明显,第二种定义更通俗易懂,其实就是说,只要基类出现的地方,均可以替换为子类,并且程序的功能不会发生变化。
注意最后一点很关键,要保证替换为子类以后,程序功能不会发生变化,那么,子类不能重写(覆盖)父类已实现的方法。若是子类重写了父类已实现的方法,那极可能就会获得不同的结果。由于替换成子类对象以后,调用该对象的方法时,实际上就会调用子类的方法,那结果就和调用父类方法不同了。
虽然子类不能重写父类已实现的方法,但能够重载父类已实现的方法,但要求重载的方法形参要比父类方法的输入参数更宽松。好比,父类有一个方法为 func(HashMap map),那子类方法能够为 func(Map map),由于 Map 比 HashMap 更宽松。假设父类实例为 fa,子类实例为 su,那 fa.func(HashMap或其子类) 和 su.func(HashMap或其子类) 所调用的都是父类的方法 func(HashMap map),这样,替换以后的结果就能保证一致。而若是反过来,父类的形参为 Map,子类的形参为 HashMap,那调用 su.func(HashMap或其子类) 时就会优先调用子类的方法了,那结果和调用父类方法可能就不同了,所以,这是违背里氏替换原则的。
通常来讲,程序中的父类大可能是抽象类,只定义了一个框架,具体功能须要子类来实现。并且父类中已实现的代码自己已经足够好,子类只须要进行扩展便可,尽可能避免对其已经实现的方法再去重写。
单一职责原则(Single Responsibility Principle,SRP)是你们最熟悉、也最容易理解的一个设计原则了,其定义也是很是简单:
There should never be more than one reason for a class to change.
意思就是,致使类变动的缘由不能超过一个。换句话说就是,一个类只负责一个职责。类的职责单一,类的复杂度就会下降,代码维护起来天然也更容易。咱们都知道,若是一个类包含了不少职责,那这个类就会变得很是臃肿,很差维护。
其实,单一职责原则不仅是适用于类,对于接口和方法也适用。
虽然单一职责原则很是简单,也很是好理解,但若是应用到实际开发中,其实又不是那么容易。要应用好单一职责原则,核心在于如何能作好职责的划分,如何定义职责的粒度大小,缺少设计经验的人很容易将一个类的职责粒度定义得过粗或过细。因此,能把该设计原则应用得好,实际上是须要很强的分析设计能力的。
若是再延伸出去,单一职责原则其实还普遍应用到架构中,如先后端分离、读写分离、架构分层、数据模型与业务逻辑分离等等,其实都是将大粒度的职责进行拆解分离。所谓大道至简,因此不要小看一个简单的单一职责原则。
接口隔离原则(Interface Segregation Principle,ISP)也有两个定义:
Clients should not be forced to depend upon interfaces that they don`t use.
客户端不该该依赖它不须要的接口。
The dependency of one class to another one should depend on the smallest possible.
一个类对另外一个类的依赖应该创建在最小的接口上。
咱们知道,一个类若是要实现一个接口,就必须实现这个接口所要求的全部方法。那么,若是这个接口里包含了这个类不须要的方法,这其实就会形成接口污染。要避免接口污染,就须要将这个接口拆分,只提取出这个类须要的方法,组成一个新的接口,而后让这个类去实现这个新接口,这就是接口隔离原则。
所谓接口隔离,隔离的其实就是多余的方法。遵循接口隔离原则,就能够避免创建庞大臃肿的接口,避免形成接口污染,可提升程序的灵活性和可维护性。
在具体的应用中,咱们应该尽可能细化接口,让接口中的方法尽可能少。尽可能为不一样的类创建不一样的专用接口,避免创建一个综合性的接口供多个不一样需求的类调用。
不过,细化的程度也不是越细越好,若是过分细化,则会形成接口数量过多,反而使得程序复杂化,因此,细化接口也要适度。
另外,不少人都会发觉接口隔离原则跟单一职责原则很类似,其实二者的关注的角度不一样。单一职责原则的关注点是业务逻辑上的职责划分,而接口隔离原则关注的则是接口数量要小。实际上,咱们在平时设计接口时,应该两个原则都要遵循。
迪米特法则(Law of Demeter,LoD)又叫做最少知识原则(Least Knowledge Principle,LKP),其定义也很是好理解:
Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
每一个单元对其余单元只拥有有限的知识,只了解与当前单元紧密联系的单元。
第一句话比较容易作到,只要尽可能减小一个类对外暴露的方法便可。而第二句话,等同于下面这句对迪米特法则的另外一个更直白的定义:
Only talk to your immediate friends.
只与直接的朋友通讯。
所谓直接的朋友,就是指在逻辑上有直接耦合关系的对象和类。通常来讲,出如今成员变量、方法参数、方法返回值中的类为直接的朋友,而出如今局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要做为局部变量的形式出如今类的内部。
迪米特法则的初衷在于下降类之间的耦合,让每一个类尽可能减小对其余类的依赖,才能提升代码的复用率。
你会发觉,迪米特法则也正好应对了高内聚低耦合的设计思想。减小一个类对外暴露的方法,从而让其余类减小对它的了解,这就是高内聚;只与直接的朋友通讯,减小对其余类的依赖,这就是低耦合。
开闭原则(Open Closed Principle,OCP)是咱们今天要讲的最后一个原则,也是其余设计原则的基石,能够说,其余设计原则都只是实现开闭原则的一些手段。先来看看开闭原则的定义:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
软件实体(类、模块、函数等)应对扩展开放,但对修改封闭。
意思就是说,当咱们的软件实体须要变化时,要尽可能经过扩展软件实体的行为来实现变化,而不是经过修改已有的代码。
咱们知道,全部软件系统都不会一成不变,若是一个需求变化会致使多个依赖的模块都发生级联式的改动,说明程序已经呈现出“坏设计(Bad Design)”的特质了。这样的程序就会相应地变得脆弱、僵化、没法预期和没法重用。开闭原则的产生就是为了解决这些问题,它可以指导咱们如何创建稳定灵活的系统,它推崇的是已经设计完成的模块应该从不改变。当需求变化时,能够经过添加新代码扩展这个模块的行为,而别去更改那些能够工做的旧代码。
那么,如何作到对扩展开放、对修改封闭呢?其实,抽象是关键。咱们都知道,抽象的灵活性好、适应性广,只要抽象定义合理,基本能够保持软件架构的稳定,因此咱们能够用抽象来构建框架。而易变的细节,咱们用从抽象派生的实现类来进行扩展,当软件须要发生变化时,咱们只须要根据需求从新派生一个实现类来扩展就能够了。固然前提是咱们的抽象要合理,要对需求的变动有前瞻性和预见性才行。那么,总结为一句话就是:用抽象构建框架,用实现扩展细节。
本文总共讲解了七个主要的设计原则:依赖倒置原则让咱们针对接口编程,只依赖于抽象,不依赖实现,由于依赖抽象易于扩展;合成复用原则建议咱们优先使用组合或聚合来实现代码的复用,也是由于合成复用耦合度低,能够提升扩展性;里氏替换原则指导咱们如何正确地使用继承,所以扩展的时候才不会产生不一致的结果;单一职责原则强调一个类只负责一个职责,以提升类的扩展性和可维护性;接口隔离原则强调接口的设计要精简,避免接口污染;迪米特法则告诉咱们要尽可能作到高内聚低耦合;开闭原则推崇对扩展开放,对修改封闭,是其余设计原则的总纲。
扫描如下二维码便可关注订阅号。