设计六大原则总结

1、单一职责原则(SRP)

定义:就一个类而言,应该仅有一个引起它变化的原因
为什么需要单一职责呢?如果一个类承担的职责过多,就等于把这些职责耦合在一起了,一个职责的变化可能会引起其它职责的变化,当变化发生时,设计会遭到意想不到的变化。
我们看看下面简单的类图,UserDiscount类具有两个方法,一个是获取等级类型,一个是计算折扣价格。

有两个不同的类在使用UserDiscount,Order需要获取用户等级和计算价格;User只需要获取用户等级,但不需要计算价格,这个设计违反类SRP,如果其中一个使用类的改变导致UserDiscount改变,这样会导致其它使用类也需要变更、测试、部署等问题。我们需要拆分两个职责类,如下图:

但是,如果类的变化总是导致这两个职责的同时变化,那么就不必分离它们,实际上,分离它们可能会导致复杂性增加。或者说,变化的轴线仅当变化实际发生时才具有真正意义。如果没有征兆,那么去应用SRP或者其它原则都是不可取的。
结论:SRP是最简单的职责之一,但是也比较难正确运用的职责,在开发中,会自然地把职责结合在一起,毕竟有些职责需要耦合在一起的,而难以拆分并增加复杂性。

2、开放封闭原则(OCP)

定义:软件实体(类、模块、函数等等)应该是可以扩展的,但是不可以修改的

  • 对于扩展是开放的:模块行为是可以扩展的,当应用需求改变时,我们可以对模块进行扩展,使其满足那些改变的行为。
  • 对于修改是封闭的:对模块扩展时,不必改动模块的源代码

下面来看个播放MP3的例子,MP3和Player都是具体类,MP3直接使用Player播放音乐,但是如果需要播放音频,那么就需要重新修改Player而导致MP3也需要修改。

下面我们修改下例子而遵循OCP原则


这个设计中,IPlayer是一个接口,MP3和Video继承该接口,今后想增加其它类型的播放只需要继承IPlayer就行,无需修改MP3或Video类。
但实际开放中,无论模块多么封闭,都会存在一些无法对之封闭的现象,那就需要有策略的去对待这个问题,模块应该对哪种变化封闭而做出选型,必须先猜测最有可能发生变化的情况,然后构造出抽象来隔离。
结论:遵循OOP可以带来灵活性、可重用性、以及可维护性。然而,对于应用程序中每个部分都肆意的进行抽象同样是不行的,这样属于不成熟抽象,我们只需要把频繁变化的部分进行抽象就行。

3、Liskov替换原则(LSP)

定义:子类型必须能够替换掉它们的基类型
举个例子,函数a使用的参数是基类B,但是C类继承基类B,但把C做为参数传给了函数a而导致其发生错误,这样就是违反了LSP原则。主要体现在下面四个方面:

  • 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
  • 子类中可以增加自己特有的方法。
  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

下面来看下简单类图,违反来SRP原则,定义了一个Rectangle和一个继承自Rectangle的Square,看着是非常符合逻辑的,但是我们重新设置Rectangle的宽度,会导致Square的宽度也会变动,导致Square出错。

改变一下不符合SRP,我们再定义一个他们共同的父类Graphics,然后让Rectangle和Square都继承自这个父类。在基类Graphics类中没有赋值方法,因此重设宽高不可能适用于Graphics类型,而只能适用于不同的具体子类Rectangle和Aquare,因此里氏替换原则不可能被破坏。并且下面的设计也符合OCP原则。

结论:使用LSP,使得程序具有更多的可维护性、可重用性以及健壮性。而LSP是使OCP成为可能的主要原则之一,子类型的可替换性才使得基类类型的模块在无需修改的情况下可以扩展。

4、依赖倒置原则(DIP)

定义:高层模块不应该依赖于低层模块,二者应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
下面来看下简单例子,用户有多个用户等级类UseOrdinary和UserDiamond,而UserTypeService使用等级类进行相关的逻辑处理,今后如果增强其它用户等级,就需要修改UserTypeService,这样违反类DIP,高层策略没有和低层实现分离,抽象没有和具体细节分离,没有这种分离,高层策略就自动地依赖于低层策略,抽象就自动地依赖于具体细节。

我们变更下具体的实现方式,抽象出UseType接口,UseOrdinary和UserDiamond继承该接口,而UserTypeService使用了UseType,不管今后增加什么用户等级都无需修改UserTypeService,并对于具体的实现类我们是不管的,只要接口的行为不发生变化,增加新的用户等级后,上层服务不用做任何的修改。这样设计降低了层与层之间的耦合,能很好地适应需求的变化,大大提高了代码的可维护性。

结论:设置倒置的依赖关系结构,使得细节和策略都依赖于抽象,属于面向对象设计;如果依赖关系不倒置,属于过程化设计。

5、接口隔离原则(ISP)

定义:不应该强迫继承类依赖于它们不使用的接口方法,类间的依赖关系应该建立在最小的接口上
使用者依赖了那些它们不使用的方法,就面临着这些未使用的方法改变而带来的变更,无意中导致了它们之间的耦合,下面来看下简单示例,MatchingHandler是一个匹配接口,包含匹配系统ID(handleSystemId)和处理联赛ID(detectLeagueId),MatchMatching和LeagueMatching继承了该接口,但MatchMatching不需要处理处理联赛ID,也继承了该方法,这样方法改变而带来的变更。

我们在来看下变更后的简单类图,新增了LeagueMatchingHandler(detectLeagueId),LeagueMatching继承了该接口,detectLeagueId方法的变更不会导致MatchMatching也需要变更,只会影响到LeagueMatching。

结论:胖类是这个类过于臃肿,可能会导致使用者产生不正常的耦合关系,该类的修改也会导致使用者的修改。使用接口分解,使用者只需要使用特定的接口,并解除了和胖类的耦合关系。

6、迪米特原则(LOD)

定义:类之间尽可能少与其他实体发生相互作用
在开发中,我们经常提到高内聚低耦合,使各个模块之间的耦合尽量的低,才能提高代码的复用率,耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为低耦合,而出现在局部变量中的类则高耦合。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。下面我们来看下例子,定义了Match,Team和Player,Match都引用了Team和Player,Team又引用了Player,这样违反了LOD,导致了Match跟Player耦合增加。

下面我们来变更下引用,Match只需要引用了Team就行,无需在引用Palyer,因为Team已经引用了Player。这样Match可以打印出相关选手了。

结论:LOD的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系,但这样必须会产生一个中介类,由这个中介类来处理类之间的通信,过多的中介类会导致系统复杂度增大而难以维护。设计的时候需要权衡,保持结构清晰和高内聚低耦合

posted @ 2019-08-30 12:03 fomin 阅读(...) 评论(...) 编辑 收藏