6大设计原则解析

一、6大设计原则解读

1.1  6大设计原则简介

6大设计原则包括:单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则和开闭原则。

1.2 单一职责原则

        单一职责原则(single responsibility principle),这个原则存在着争议,争议之处就在于对职责的定义,什么是类的职责,以及怎么划分类的职责。先举例分析一下:

       只要做过项目,肯定要接触到用户、机构、角色这些模块,基本上使用的都是RBAC模式(Role-Based Access Control,基于角色来完成用户权限的授予和取消,是动作主体(用户)与资源的行为(权限)分离),确实是一个很好的解决方法。我们在这里要讲的是用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等,用户有这么多的信息和行为要维护,我们就把这些写到一个接口中,都是用户管理类。

        单一职责原则的好处:

      类的复杂性降低,实现什么职责都有清晰明确的定义:

      可读性提高,复杂性降低,那当然可读性提高了:

     可维护性提高,可读性提高,那当然更容易维护了:

    变更引起的风险降低,变更时必不可少的,如果接口的单一职责做得好,一个接口修改支队相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

  注意:单一职责原则提出了一个编写程序的标准,用“职责“或者“变化原因”来衡量接口或类设计是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

     对于单一职责原则,建议是接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

1.3 里氏替换原则

在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,优点如下

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 提高代码的重用性;
  • 子类可以形似父类,但由异于父类。
  • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过集成父类来完成的。
  • 提高产品或项目的开放性。

继承的缺点:

  • 继承是有侵入性的。只要继承,就必须拥有父类的所有属性和方法。
  • 降低代码的灵活性,子类必须拥有父类的属性和方法,让子类自由世界中多了些束缚;
  • 增加了耦合性。

java中使用extends关键字来实现继承,它采用了单一继承的规则,C++则采用了多重继承的规则,一个子类可以继承多个父类。从整体上来看,利大于弊,怎么才能让“利”的元素发挥最大的作用,同时减少“弊”带来的麻烦呢?解决方案是引入里氏替换原则(liskov substitute principle LSP),

里氏替换定义:

第一种定义(也是最正宗的定义):if for each object 01 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 substitue for o2 then S is  a subtype of T(如果对每一个类型为S 的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o2被替换为o1时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)

第二种定义:所有引用积累的地方必须能透明地使用其子类的对象。

第二个定义式最清晰明确的,通俗讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或者异常,使用者可能根本就不需要知道是父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必就能适应。

里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。

  1. 子类必须完全实现父类方法
  2. 子类可以有自己的个性
  3. 覆盖或实现父类的方法使输入参数可以被放大
  4. 覆写或实现父类的方法时输出结果可以被缩小

在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性“被抹杀——委屈了点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准

1.4 依赖倒置原则

依赖倒置原则(dependence inversion principle)包含三层含义:

  • 高层模块不应该依赖底层模块,两者都应该依赖其抽象
  • 抽象不应该依赖细节
  • 细节应该依赖抽象

高层模块和底层模块容易理解,每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是底层模块,原子逻辑的在组装就是高层模块。那什么是抽象?什么是细节?在java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的:细节就是实现类,实现接口或集成抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字new产生一个对象。依赖倒置原则在java语言中的表现就是:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的
  • 接口或抽象类不依赖与实现类
  • 实现类依赖接口或抽象类

更加精简的定义就是“面向接口编程”——OOD(Object-oriented design,面向对象设计)的精髓之一。

依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合,我们怎么在项目中使用这个规则呢?只要遵循以下的几个规则就可以:

  • 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备:这是依赖倒置的基本要求,接口和抽象类都是属于抽象类的,有了抽象才可能依赖倒置。
  • 变量的表面类型尽量是接口或者抽象类:并不是变量的类型一定要是接口或者抽象类,比如一个工具类,xxxutils一般是不需要接口或是抽象类。还有,如果使用类clone方法,就必须使用实现类,这个是JDK提供的一个规范。
  • 任何类都不应该从具体类派生:如果一个项目处于开发状态,确实不应该有从具体类派生出子类的情况,但人总会犯错,因此不超出两层的继承都是可以忍受的。
  • 尽量不要覆写基类的方法:如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。
  • 综合里氏替换原则使用。

依赖倒置原则时6个设计原则中最难以实现的原则,它是实现开闭原则的重要途径。依赖倒原则没有实现,就别想实现对拓展开放,对修改关闭。在项目中,大家只要记住是“面向接口编程”就基本上抓住了依赖倒置原则的核心。

1.5 接口隔离原则

接口隔离原则要求接口的纯洁性,接口隔离原则时对接口进行规范约束,包含4层含义:

  • 接口要尽量小:这是几口隔离原则的核心定义,不出现臃肿的接口(fat interface),但是“小”是有限度的,首先就是不能违反单一职责原则。
  • 接口要高内聚:就是提高接口,类、模块的处理能力,减少对外的交互。
  • 定制服务:一个系统或系统内的模块之间必然会有耦合,有耦合就要有互相访问的接口(但不一定就是java中定义的interface,也可能是一个类或单纯的数据交换),我们设计时就需要为各个访问者(即客户端)定制服务,什么是定制服务?定制服务就是单独为一个个体提供优良的服务。我们在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。采用定制服务就必然有一个要求:只提供访问者需要的方法。
  • 接口设计时有限度的:接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化,开发难度增加。可维护性降低,这不是一个项目或产品所期望看到的,所以接口设计一定要注意湿度。

实践中根据以下几个规则来衡量:

  • 一个接口只服务于一个子模块或业务逻辑;
  • 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法。
  • 已经被污染的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。
  • 了解环境,拒绝盲从。

1.6 迪米特法则

迪米特法则(law of demeter,LoD)也成为最少知识原则(least knowledge principle),虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没有关系,那是你的事情,我就知道提供的这么多public方法,我就调用这么多,其他的我一概不关心。

迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转和跳转类,导致系统的复杂性提高,同时也为维护带来了难度。需要反复权衡,既做到结构清晰,又做到高内聚低耦合。

1.7 开闭原则

开闭原则定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

开闭原则的的重要性:

  • 开闭原则对测试的影响:测试时出错,测试失败时。通过扩展来实现业务逻辑的变化,而不是修改。
  • 开闭原则可以提高服用性:在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样的代码才可以复用,粒度越小,被复用的可能性越大。
  • 开闭原则可以提高可维护性

如何使用开闭原则?

开闭原则时一个很虚的原则,前面5个原则都是对开闭原则的具体解释。但是开闭原则并不局限于这么多,它“虚”地没有边界。

如何在实际工作中应用这个“虚”的像口号的原则:

  1. 抽象约束:抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,包含三层含义:第一,通过接口或抽象类约束扩展,对扩展进行辩解限定,不允许出现在接口或抽象类中不存在的public方法;第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;第三,抽象层尽量保持稳定,一旦确定即不允许修改。
  2. 元数据(metadata)控制模块行为:编程是一个很苦很累的活,那怎么才能减轻我们的压力呢?答案是尽量使用元数据来控制程序的行为,减少重复开发。什么是元数据:用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。
  3. 制定项目章程:在一个团队中,建立项目章程是非常重要的,因为章程中制定了所有人员都必须遵守的约定,对项目来说,约定优于配置。
  4. 封装变化:对变化的封装包含两层含义:第一,将相同的变化封装到一个接口或抽象类中,第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中,封装变化也就是受保护的变化(protected variations),找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口,准确的讲是粉状可能的变化。

 

 

本文内容参考《设计模式之禅(第二版)》作者:琴小波

 

 

 

 

你可能感兴趣的:(设计模式和设计原则)