设计模式探秘-3-设计模式简介

设计模式简介

Christopher Alexander 说:“模式是在某一背景下某个问题的一种解决方案。”它们绝不只是解决某人个别问题的模板。它们是描述动机的一种方式,不仅包括我们要得到的效果,也包括困扰我们的问题。

设计模式的关键特征

项目 描述
意图 每个模式都有唯一的用于标识的名称
问题 模式的目的
解决方案 模式能够为问题提供合适其所处环境的一个解决方案
参与者和协作者 模式所涉及的实体
效果 使用模式的效果,研究模式中起作用的各种因素
实现 模式的实现方式。实现只是模式的具体体现,而不能视为模式本身
一般性结构 显示模式典型结构的标准图

一个好的设计模式的论述应该覆盖使用这个模式的优点和缺点

学习设计模式的好处

  1. 对不断重复出现问题,复用既有的、高质量的解决方案。
  2. 确立通用的术语,改善团队内的沟通。
  3. 提升思考层次。
  4. 判断设计是否正确,而不仅仅是能够奏效。
  5. 改善个人学习和团队学习。
  6. 提高代码的可修改性和可维护性。
  7. 采用更佳设计方案,即使没有明确使用模式。
  8. 发现巨型继承层次结构的替代方案。

设计模式分类

根据目的,设计模式可分为创建型、结构型和行为型三种。

  1. 创建型主要用于创建对象
  2. 结构型用于处理类和对象的组合
  3. 行为型用于描述类和对象如何交互和怎样分配职责

根据范围,即模式主要用于处理类之间关系还是对象之间的关系可分为类模式和对象模式两种

  1. 类模式处理类和子类之间的关系,通常通过继承建立,编译时刻确定下来,属于静态的
  2. 对象模式处理对象间关系,运行时时刻变化,具有动态性
范围\目的 创建型模式 结构型模式 行为型模式
类模式 工厂方法模式 (类)适配器模式 解释器模式
模版方法模式
对象模式 抽象工厂模式
建造者模式
原型模式
单例模式
(对象)适配器模式
桥接模式
组合模式
装饰模式
享元模式
外观模式
代理模式
职责模式
命令模式
迭代器模式
中介者模式
备忘录模式
观察者模式
状态模式
策略模式
访问者模式

基本设计模式概述

  1. Abstract Factory(抽象工厂模式):提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
  2. Adapter(适配器模式):将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
  3. Bridge(桥接模式):将抽象部分与它的实现部分分离,使它们都可以独立地变化。
  4. Builder(建造者模式):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
  5. Chain of Responsibility(责任链模式):为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。
  6. Command(命令模式):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。
  7. Composite(组合模式):将对象组合成树形结构以表示“部分-整体”的层次结构。它使得客户对单个对象和复合对象的使用具有一致性。
  8. Decorator(装饰模式):动态地给一个对象添加一些额外的职责。就扩展功能而言, 它比生成子类方式更为灵活。
  9. Facade(外观模式):为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
  10. Factory Method(工厂模式):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使一个类的实例化延迟到其子类。
  11. Flyweight(享元模式):运用共享技术有效地支持大量细粒度的对象。
  12. Interpreter(解析器模式):给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。
  13. Iterator(迭代器模式):提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。
  14. Mediator(中介模式):用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
  15. Memento(备忘录模式):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
  16. Observer(观察者模式):定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
  17. Prototype(原型模式):用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
  18. Proxy(代理模式):为其他对象提供一个代理以控制对这个对象的访问。
  19. Singleton(单例模式):保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  20. State(状态模式):允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
  21. Strategy(策略模式):定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法的变化可独立于使用它的客户。
  22. Template Method(模板方法模式):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
  23. Visitor(访问者模式):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

软件设计六大原则

  1. 单一职责原则
  2. 里氏替换原则
  3. 依赖导致原则
  4. 接口隔离原则
  5. 迪米特法则
  6. 开闭原则

单一职责原则(Single Responsibility Principle)简称SRP

单一职责原则定义:应该有且仅有一个原因引起类的变更
There should never be more than one reason for a class to change.

单一职责原则的好处:

  1. 降低类的复杂性,实现的职责都有清晰明确的定义
  2. 可读性提高
  3. 可维护性提高
  4. 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其它接口无影响,这对系统的扩展性、维护性都有非常大的帮助

单一职责原则最难划分的就是职责。
单一职责原则适用于接口、类,也适用于方法。
对于接口和方法一定要做到单一职责,对于类的设计尽量做到只有一个原因引起变化。

里氏替换原则(Liskov Substitution Principle)简称LSP

继承优点:

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

继承缺点:

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

两种里氏替换原则的定义:

  1. 最正宗的,如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有对象o1替换为o2时,程序P的行为没有发生变化,那么类型S时类型T的子类型。
  2. 所有引用基类的地方必须能透明的使用其子类的对象。

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

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

  1. 子类必须完全实现父类的方法
    1. 在类中调用其它类时务必使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
    2. 如果子类不能完整的实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚合、组合等关系替代继承。
  2. 子类可以有自己的方法和属性
  3. 覆盖或实现父类的方法时输入参数可以被放大
    • 子类中方法的前置条件必须与超类中被覆盖的方法的前置条件相同或者更宽松。
  4. 覆盖或实现父类的方法时输出结果可以被缩小
    • 父类方法的返回值是一个类型T,子类的相同方法(重写或覆盖)的返回值为S,里氏替换原则要求S必须小于等于T,即S和T是同一类型,或S是T的子类。

采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。

采用里氏替换原则时,应尽量避免子类的“个性”,一旦子类有“个性”,该子类和父类的关系就很难调和了,把子类当做父类,子类的“个性“被抹杀,将子类作为一个业务来使用,会让代码的耦合关系变得扑朔迷离——缺乏类替换的标准。

依赖倒置原则(Dependence Inversion Principle)简称DIP

依赖倒置原则,要依赖于抽象,不要依赖于具体:

  1. 高层模块不应该依赖底层模块,两者都应该依赖于其抽象
  2. 抽象不应该依赖于细节
  3. 细节应该依赖于抽象

即”面向接口编程”——OOD(Object-Oriented Design,面向对象设计)的精髓之一。
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并发开发引起的风险,提高代码的可读性和可维护性。
依赖时可以传递的,A依赖于B对象,B又依赖于C,C又依赖于D…… 但只要做到抽象依赖,即使是多层的依赖也无所畏惧。
依赖的三种写法:

  1. 构造函数传递依赖对象(构造函数注入)
  2. Setter方法传递依赖对象
  3. 接口声明依赖对象(接口注入)

使用依赖倒置原则的方法:

  1. 每个类尽量都有接口或抽象类、或抽象类和接口都有
  2. 类型的表面尽量是接口或者抽象类
  3. 任何类都不应该从具体类派生
  4. 尽量不去覆写基类的方法
  5. 结合里氏替换原则使用

接口隔离原则

接口分为两种:

  1. 实例接口
  2. 类接口

隔离的定义:

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

即建立单一接口,不要建立臃肿庞大的接口。通俗说:接口尽量细化,同时接口中方法尽量少。

接口隔离原则是对接口进行规范约束,包含4层含义:

  1. 接口尽量小
    • 根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
  2. 接口要高内聚
    • 高内聚就是提高接口、类、模块的处理能力,减少对外的交互。
  3. 定制服务
    • 定制服务就是单独为一个个体提供优良的服务。定制服务必然要求:只提供访问者需要的接口。
  4. 接口设计是有限度的,不能无限细化,否则虽然提高了灵活性,但也带来了结构的复杂性,开发难度增加,可维护性降低,所以接口设计要注意适度。

接口隔离原则的原子划分:

  1. 一个接口只服务于一个子模块或业务逻辑
  2. 通过业务逻辑压缩接口中的public方法,接口设计时常去回顾,让接口更加精简
  3. 已经污染的接口尽量去修改,若变更的风险更大,则采取适配器模式进行转化
  4. 了解环境,拒绝盲从。环境不同,接口拆分的标准就不同,深入了解业务逻辑,设计更好的接口

迪米特法则(Law of Demeter)简称LoD

迪米特法则也称为最少知识原则(Least Knowledge Principle,LKP),规则:一个对象应该对其它对象有更少的了解。

迪米特法则对类的低耦合提出明确要求包含4层含义:

  1. 只和朋友交流
    • 两个对象之间有耦合关系即为耦合,如:组合、聚合、依赖等。
  2. 朋友之间也是有距离的
    • 类尽量不要公布太多的public方法和非静态的public变量,尽量内敛,多使用private和protected等访问权限
  3. 是自己的就是自己的
    • 如果一个方法放在本类中可以,放在其它类中也可以,则坚持这个原则:如果一个方法放在本类中,即不增加类间关系,也对本类不产生负面影响,就放置在本类中。
  4. 谨慎使用Serializable(序列化)

迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合之后,类的复用率才可以提高。但会产生大量的中转或调转类,导致系统复杂性提高,使维护困难。使用迪米特法则需反复权衡,既做到结构清晰,同时做到高内聚低耦合。

开闭原则(Open-ClosePrinciple)简称OCP

开闭原则即一个软件实体(类、模块、函数等)应该对扩展开放,对修改关闭
开闭原则指出应尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化,开闭原则是为软件实体的未来事件而定制的对现行开发设计进行约束的一个原则。
开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无缘的代码片段。

变化:

  1. 逻辑变化
  2. 子模块变化
  3. 可见视图变化

开闭原则的重要性:

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

开闭原则是一个非常虚的原则,前五个原则是对开闭原则的具体解释,但开闭原则并不局限于这么多。开闭原则在工作中的运用:

  1. 抽象约束
    1. 通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现接口或抽象类中不存在的public方法
    2. 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类
    3. 抽象类尽量保持稳定,一旦确定即不允许修改
  2. 元数据控制模块行为
    元数据即描述环境和数据的数据,通俗的来说就是配置参数。
  3. 指定项目章程
  4. 封装变化
    1. 将相同的变化封装到一个接口或抽象类中
    2. 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

参考资料