Posts 设计原则
Post
Cancel

设计原则

前言

设计模式是什么?它是一套理论,由软件界的先辈们(The Gang of Four:包括Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)总结出的一套可以反复使用的经验,它可以提高代码的可重用性,增强系统的可维护性,以及解决一系列的复杂问题。做软件的人都知道需求是最难把握的,我们可以分析现有的需求,预测可能发生的变更,但是我们不能控制需求的变更。问题来了,既然需求的变更是不可控的,那如何拥抱变化呢?幸运的是,设计模式给了我们指导,专家们首先提出了6大设计原则,但这6大设计原则仅仅是一系列“口号”,真正付诸实施还需要有详尽的指导方法,于是23种设计模式出现了。

设计模式不是工具,它是软件开发的哲学,它能指导你如何去设计一个优秀的架构、编写一段健壮的代码、解决一个复杂的需求。因为它是软件行业的经验总结,因此它具有更广泛的适应性,不管你使用什么编程语言,不管你遇到什么业务类型,设计模式都可以自由地“侵入”。因为它不是工具,所以它没有一个可以具体测量的标尺,完全以你自己的理解为准,你认为自己多了解它,你就有可能产生多少的优秀代码和设计。因为它是指导思想,你可以在此基础上自由发挥,甚至是自己设计出一套设计模式。

六大设计原则:

  • 单一职责原则
  • 里氏替换原则
  • 依赖倒置原则
  • 迪米特法则
  • 开闭原则

单一职责原则

单一职责原则的英文名称是Single Responsibility Principle,简称是SRP。其定义是:应该有且仅有一个原因引起类的变更(There should never be more than one reason for a class to change.).

优点:

  • 类的复杂性降低,实现什么职责都有清晰明确的定义
  • 可读性提高,复杂性降低
  • 可维护性提高
  • 变更引起的风险降低:一个接口修改只对应相应的实现类有影响,对其他接口无影响

对于接口,我们在设计的时候一定要做到单一,但是对于实现类就需要多方面考虑了.生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性.比如:原本一个类可以实现的行为硬要拆分成两个类,然后再使用聚合或组合的方式耦合在一起,人为制造类系统的复杂性.

里氏替换原则

继承的优点:

  • 代码共享,减少创建类的工作量,每个字类都拥有父类的方法和属性
  • 提高代码的重用性
  • 子类可以形似父类,但又异于父类
  • 提高代码的可扩展性
  • 提高产品或项目的开放性

继承的缺点:

  • 继承是入侵性的.只要继承,就必须拥有父类的所有属性和方法.
  • 降低代码的灵活性.子类必须拥有父类的属性和方法.
  • 增强类耦合性.当父类的常量、变量和方法被修改时,需要考虑子类的修改.

什么是里氏替换原则: 所有引用基类的地方必须能透明的使用其子类的对象(Functions that use pointers or references to base classes mustbe able to use objects of derived classes without knowing it.) 通俗的讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类.但是,反过来就不行了,有子类出现的地方,父类未必就能适应.

里氏替换原则为良好的继承定义了一个规范:

  • 子类必须完全实现父类的方法: 接口/抽象类的应用
  • 子类可以有自己的个性.
  • 覆盖或实现父类的方法时输入参数可以被放大.
  • 覆盖或实现父类的方法时输出结果可以被缩小.

如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承.

采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑,非常完美!

依赖倒置原则

定义:

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

抽象是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点是可以被直接实例化,也就是可以加上一个关键字new产生一个对象.

简单说就是面向接口编程(OOD).

采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性. 两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)就可以独立开发了,而且项目之间的单元测试也可以独立地运行,而TDD(Test-Driven Development,测试驱动开发)开发模式就是依赖倒置原则的最高级应用。

依赖是可以传递的,A对象依赖B对象,B又依赖C,C又依赖D……生生不息,依赖不止,记住一点:只要做到抽象依赖,即使是多层的依赖传递也无所畏惧!

对象的依赖关系有三种方式来传递:

  • 构造函数传递依赖对象
  • Setter方法传递依赖对象
  • 接口声明依赖对象

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

  • 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备.
  • 变量的表面类型尽量是接口或者是抽象类.
  • 任何类都不应该从具体类派生.
  • 尽量不要覆写基类的方法.
  • 结合里氏替换原则使用.

依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑,而编写程序需要的是对现实世界的事物进行抽象,抽象的结果就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的。

简单点说,抽象间的依赖就是倒置,实物间的依赖是正置.

在一个大中型项目中,采用依赖倒置原则有非常多的优点,特别是规避一些非技术因素引起的问题。项目越大,需求变化的概率也越大,通过采用依赖倒置原则设计的接口或抽象类对实现类进行约束,可以减少需求变化引起的工作量剧增的情况。人员的变动在大中型项目中也是时常存在的,如果设计优良、代码结构清晰,人员变化对项目的影响基本为零。大中型项目的维护周期一般都很长,采用依赖倒置原则可以让维护人员轻松地扩展和维护。

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

接口隔离原则

定义:

  • 客户端不应该依赖它不需要的接口.
  • 类间的依赖关系应该建立在最小的接口上.

客户端应该依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就是需要对接口进行细化,保证其纯洁性.

类间的依赖关系需要接口细化,接口纯洁.

简单点说就是:接口尽量细化,同时接口中的方法尽量少.

接口隔离原则是对接口进行规范约束:

  • 接口要尽量小,不能无限小,违背单一职责.
  • 接口要高内聚,接口中尽量少公布public方法
  • 定制服务:对系统之间或模块之间的接口采用定制服务.不同访问对象,提供不同的接口
  • 接口设计是有限度的,适度设计,过于灵活的同时也会带来结构的复杂化

接口隔离原则是对接口的定义,同时也是对类的定义,接口和类尽量使用原子接口或原子类来组装。但是,这个原子该怎么划分是设计模式中的一大难题,在实践中可以根据以下几个规则来衡量:

  • 一个接口只服务于一个子模块或业务逻辑
  • 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
  • 已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;
  • 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑,最好的接口设计就出自你的手中!

迪米特法则

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

迪米特法则对类的低耦合提出了明确的要求:

  • 只和朋友交流
  • 朋友间也是有距离的
  • 是自己的就是自己的

朋友类的定义:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类.

一个类只和朋友交流,不与陌生交流,类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象.

一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。因此,为了保持朋友类间的距离,在设计时需要反复衡量:是否还可以再减少public方法和属性,是否可以修改为private、package-private(包类型,在类、方法、变量前不加访问权限,则默认为包类型)、protected等访问权限,是否可以加上final关键字等。

在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。

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

开闭原则

开闭原则定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭(Software entities like classes,modules and functions should be open forextension but closed for modifications.)

开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。

需求变化是不可避免的,如何应对一个需求变化:

  • 修改接口: 接口应该是稳定可靠的,不应该经常发生变化
  • 修改实现类:
  • 通过扩展实现变化: 修改少,风险也小

可以把变化归纳为:

  • 逻辑变化
  • 子模块变化
  • 可见视图变化

一个项目的基本路径应该是这样的:项目开发、重构、测试、投产、运维,其中的重构可以对原有的设计和代码进行修改,运维尽量减少对原有代码的修改,保持历史代码的纯洁性,提高系统的稳定性。

开闭原则是最基础的一个原则,前五个的原则都是开闭原则的具体形态,也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。

开闭原则的重要性:

  • 开闭原则对测试的影响
  • 开闭原则可以提高复用性
  • 开闭原则可以提高可维护性
  • 面向对象开发的要求

所有已经投产的代码都是有意义的,并且都受系统规则的约束,这样的代码都要经过“千锤百炼”的测试过程,不仅保证逻辑是正确的,还要保证苛刻条件(高压力、异常、错误)下不产生“有毒代码”(Poisonous Code),因此有变化提出时,我们就需要考虑一下,原有的健壮代码是否可以不修改,仅仅通过扩展实现变化呢?否则,就需要把原有的测试过程回笼一遍,需要进行单元测试、功能测试、集成测试甚至是验收测试,现在虽然在大力提倡自动化测试工具,但是仍然代替不了人工的测试工作。

所以,我们需要通过扩展来实现业务逻辑的变化,而不是修改。新增加的类,新增加的测试方法,只要保证新增加类是正确的就可以了。

在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。那为什么要复用呢?减少代码量,避免相同的逻辑分散在多个角落,避免日后的维护人员为了修改一个微小的缺陷或增加新功能而要在整个项目中到处查找相关的代码,然后发出对开发人员“极度失望”的感慨。那怎么才能提高复用率呢?缩小逻辑粒度,直到一个逻辑不可再拆分为止。

一款软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能要对程序进行扩展,维护人员最乐意做的事情就是扩展一个类,而不是修改一个类,甭管原有的代码写得多么优秀还是多么糟糕,让维护人员读懂原有的代码,然后再修改,是一件很痛苦的事情,不要让他在原有的代码海洋里游弋完毕后再修改,那是对维护人员的一种折磨和摧残。

万物皆对象,我们需要把所有的事物都抽象成对象,然后针对对象进行操作,但是万物皆运动,有运动就有变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。

如何应用开闭原则:

  • 抽象约束
  • 元数据控制模块行为
  • 制定项目章程
  • 封装变化

抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:第一,通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;第三,抽象层尽量保持稳定,一旦确定即不允许修改。

接口或抽象类一旦定义,就应该立即执行,不能有修改接口的思想,除非是彻底的大返工。 所以,要实现对扩展开放,首要的前提条件就是抽象约束。

什么是元数据?用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。

在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。相信大家都做过项目,会发现一个项目会产生非常多的配置文件。然而,如果你在项目中指定这样一个章程:所有的Bean都自动注入,使用Annotation进行装配,进行扩展时,甚至只用写一个子类,然后由持久层生成对象,其他的都不需要修改,这就需要项目内约束,每个项目成员都必须遵守,该方法需要一个团队有较高的自觉性,需要一个较长时间的磨合,一旦项目成员都熟悉这样的规则,比通过接口或抽象类进行约束效率更高,而且扩展性一点也没有减少。

对变化的封装包含两层含义:第一,将相同的变化封装到一个接口或抽象类中;第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。封装变化,也就是受保护的变化(protectedvariations),找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口,准确地讲是封装可能发生的变化,一旦预测到或“第六感”发觉有变化,就可以进行封装,23个设计模式都是从各个不同的角度对变化进行封装的.

使用开闭原则时应注意:

  • 开闭原则也只是一个原则
  • 项目规章非常重要
  • 预知变化

开闭原则只是精神口号,实现拥抱变化的方法非常多,并不局限于这6大设计原则,但是遵循这6大设计原则基本上可以应对大多数变化。因此,我们在项目中应尽量采用这6大原则,适当时候可以进行扩充,例如通过类文件替换的方式完全可以解决系统中的一些缺陷。通过修改原有实现类的方式就可以解决这个问题,前提条件是:类必须做到高内聚、低耦合,否则类文件的替换会引起不可预料的故障。

如果你是一位项目经理或架构师,应尽量让自己的项目成员稳定,稳定后才能建立高效的团队文化,章程是一个团队所有成员共同的知识结晶,也是所有成员必须遵守的约定。优秀的章程能带给项目带来非常多的好处,如提高开发效率、降低缺陷率、提高团队士气、提高技术成员水平,等等。

在实践中过程中,架构师或项目经理一旦发现有发生变化的可能,或者变化曾经发生过,则需要考虑现有的架构是否可以轻松地实现这一变化。架构师设计一套系统不仅要符合现有的需求,还要适应可能发生的变化,这才是一个优良的架构。

总结

六大设计原则的核心思想是:降低类之间的耦合,提高代码的可重用性,增强系统的可维护性.

单一职责原则从类的职责方面进行约束,也适用于方法与接口.降低类的复杂性,提高稳定性.

里氏替换原则从类的继承关系方面进行约束,增强程序的健壮性.

依赖倒置原则的核心就是面向接口编程,是实现开闭原则的重要途径.

接口隔离原则要求接口设计尽量小,纯洁.

迪米特法则要求对象之间的关系应当保持一定的距离,核心点是类之间的弱耦合.

开闭原则的核心点是通过扩展的方式进行变化的实现.

This post is licensed under CC BY 4.0 by the author.