关于设计——基础原则和设计模式总结

摘要:本文主要总结经典设计模式的概念以及应用场景。知道常用模式和基本的面向对象概念并不会让人马上变成好的面向对象设计者,但这是个基本的学习过程。真正的设计大师脑子记的不会是固定的模式,他们关心的是建立弹性的设计,可以维护,可以应付改变。

一 基础

  • 抽象
  • 封装
  • 多态
  • 继承

二 原则

  • 单一职责原则:一个类负责一项职责。“职责”没有标准量化,在实际开发中,需要因地制宜。
  • 里式替换原则:继承与派生原则。子类型必须能够替换掉他们的父类型
  • 依赖倒置原则:高层模块不应该依赖底层模块,二者都应该依赖其抽象;针对接口编程而不要针对实现编程
  • 接口隔离原则:建立单一的接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少
  • 迪米特法则:低耦合,高内聚
  • 开闭原则:一个软件实体如类,模块和函数应该对扩展开放,对修改关闭
  • 组合&聚合服用原则:尽量使用组合和聚合,少使用继承关系来达到复用
  • 封装变化
  • 为交互对象的松耦合设计而努力

三 模式

3.1 策略模式:

定义

定义算法族,分别封装起来,让他们之间可以互相替换。此模式让算法的变化独立于使用算法的客户。本质是分离算法,选择实现

结构图

策略模式结构图

时序图

策略模式时序图

策略模式时序图

使用情景

  • 出现许多相关的类,仅仅是行为上的差别
  • 出现同一种算法,有很多不同的实现
  • 通过多个if-else语句来选择不同行为的情况,可以使用策略模式来代替这些条件语句

对比

  • 策略模式和状态模式:状态模式根据状态的变化来选择不同的行为,不同的状态对应不同的类,在实现功能的同时还会维护数据的变化,这些实现状态对应的功能类是不能相互替换的。策略模式是根据需要选择相应的实现类,各个实现类是平等的,是可以相互替换的。
  • 策略模式和模板方法模式:模板方法重在封装算法骨架,而策略模式重在分离并封装算法实现。二者常常组合使用。

3.2 观察者模式

在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖他的对象都会收到通知并自动更新。重心在于触发联动。

  • 观察者模式定义了对象之间一对多的关系
  • 被观察者用统一的接口来更新观察者
  • 被观察者不知道观察者的细节,只知道他们都实现了观察者的接口

使用情景

  • 当一个抽象模型的一方面依赖于另一方面
  • 更改一个对象的时候,需要修改一系列其他的对象
  • 当一个对象必须通知其他对象,而你希望这个对象和其他对象是松耦合的

结构图

观察者模式结构图

时序图

准备阶段时序图

观察者模式时序图

运行阶段时序图

观察者模式时序图

3.3 装饰模式

动态地将责任附加到对象上。想要实现扩展,装饰者提供了比继承更加弹性的替代方案。

使用情景

  • 需要在不影响其它对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式,这几乎就是装饰模式的主要功能
  • 不合适使用子类来进行扩展的时候,可以考虑使用装饰模式,因为装饰模式是使用的“对象组合”的方式。所谓不适合用子类扩展的方式,比如:扩展功能需要的子类太多,造成子类数目呈爆炸性增长。

结构图

装饰模式结构图

比较

  • 装饰模式和组合模式:装饰模式是为了动态地给对象增加功能;组合模式是想要管理组合和叶子对象,为他们提供一个一致性的操作接口给客户端,方便客户端使用。
  • 装饰模式和策略模式:装饰器改变的是经过前一个装饰器装饰后的对象,是在之前的基础上做进一步修改;而策略模式是直接替换之前的算法,改变的是内核。

3.4 工厂方法

定义了一个创建对象的接口,但由子类决定实例化的类是哪一个。工厂方法把类的实例化推迟到子类。

使用情景

  • 一个类需要创建对象的接口,但是又不知道具体的实现,这种情况可以选用工厂方法模式,把创建对象的工作延迟到子类去实现。
  • 如果一个类希望由子类来创建所需要的对象,应该使用工厂方法模式

结构图

工厂方法模式结构图

时序图

客户端使用由Creator创建出来的对象情况的调用顺序示意图

工厂方法模式时序图

客户端使用Creator对象时候的调用顺序示意图

工厂方法模式时序图

比较

  • 工厂方法模式和模板方法模式:两个模式的外观相似,都是有一个抽象类,然后由子类来提供一些实现,但是工厂方法模式的子类专注的是创建产品对象,而模板方法的子类专注的是为固定的算法骨架提供某些步骤的实现。

3.5 抽象工厂方法

提供一个接口,用来创建相关的或依赖对象的家族,而不需要明确指定具体类。 抽象工厂方法的每一个创建接口,其实是一个工厂方法。

使用情景

  • 希望一个系统只知道产品的接口,而不关心产品的实现的时候
  • 希望可以动态切换产品族的时候

结构图

抽象工厂方法模式结构图

时序图

抽象工厂方法模式结构图

比较

  • 抽象工厂模式和工厂方法模式:工厂方法模式一般是针对单独的产品对象的创建,而抽象工厂模式注重产品簇对象的创建,这是它们的区别。抽象工厂可以退化成工厂方法,而工厂方法又可以退化成简单工厂,这是它们的联系

3.6 单例模式

确保一个类只有一个实例,并提供全局的访问点。

使用情景

  • 需要控制一个类的实例只能有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式,这些功能恰好是单例模式要解决的问题。

结构图

单例模式结构图

时序图

延迟加载式时序图

单例模式时序图

提前加载式时序图

单例模式时序图

3.7 命令模式

将请求封装成对象,这可以让你使用不同的请求,队列或者日志请求来参数化其他对象。命令模式也可以支持撤销操作。

使用情景

  • 需要抽象出执行的动作,并参数化这些对象
  • 需要在不同的时刻指定,排列和执行请求
  • 需要支持取消操作
  • 需要执行重复执行
  • 需要事务

结构图

命令模式结构图

时序图

组装过程

命令模式时序图

调用过程

命令模式时序图

3.8 适配器模式

将一个类的接口转换成用户期望的另一个的接口。

使用情景

  • 想要使用一个已经存在的类,但是它的接口不符合你的需求,这种情况可以使用适配器模式,来把已有的实现转换成你需要的接口
  • 想创建一个可以复用的类,这个类可能和一些不兼容的类一起工作,这种情况可以使用适配器模式,到时候需要什么就适配什么
  • 想使用一些已经存在的子类,但是不可能对每一个子类都进行适配,这种情况可以选用对象适配器,直接适配这些子类的父类就可以了

结构图

适配器模式结构图

时序图

适配器模式时序图

3.9 外观模式

提供一个统一的接口,用来访问子系统中的一群接口。外观模式定义了一个高层接口,让子系统更容易使用。

  • 辨:适配器将一个对象包起来以改变其接口;装饰器将对象包起来以增加新的行为和责任;而外观将一群对象包装起来以简化其接口。

使用情景

  • 想要减少外部与系统内多个模块的交互,松耦散合

结构图

外观模式结构图

时序图

外观模式时序图

3.10 模板方法模式

在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得在不改变算法结构的情况下,重新定义算法的某些步骤。

使用情景

  • 多个类的某一个算法的步骤相同

结构图

模板方法模式结构图

3.11 迭代器模式

提供一种方法顺序访问一个聚合对象的各个元素,而又不暴露其内部的表现。

使用情景

  • 希望提供访问一个聚合对象的内容,但是又不想暴露它的内部表示的时候,可以使用迭代器模式来提供迭代器接口,从而让客户端只是通过迭代器的接口来访问聚合对象,而无需关心聚合对象内部实现
  • 希望有多种遍历方式可以访问聚合对象,可以使用迭代器模式
  • 希望为遍历不同的聚合对象提供一个统一的接口,可以使用迭代器模式

结构图

迭代器模式结构图

3.12 组合模式

允许你将对象组成树形结构来表现“整体/部分”的层次结构。组合能让客户以一致的方式处理个别对象和对象组合。

使用情景

  • 想表示对象的部分-整体层次结构,可以选用组合模式,把整体和部分的操作统一起来,使得层次结构实现更简单,从外部来使用这个层次结构也简单
  • 希望统一的使用组合结构中的所有对象,可以选用组合模式,这正是组合模式提供的主要功能

结构图

组合模式结构图

3.13 状态模式

允许对象在内部状态改变时改变他的行为,对象看起来好像修改了他的类。

使用情景

  • 一个对象的行为取决于它的状态,而且它必须在运行时刻根据状态来改变它的行为。可以使用状态模式,来把状态和行为分离开,虽然分离开了,但状态和行为是有对应关系的,可以在运行期间,通过改变状态,就能够调用到该状态对应的状态处理对象上去,从而改变对象的行为。
  • 一个操作中含有庞大的多分支语句,而且这些分支依赖于该对象的状态。可以使用状态模式,把各个分支的处理分散包装到单独的对象处理类里面,这样,这些分支对应的对象就可以不依赖于其它对象而独立变化了。

结构图

状态模式结构图

时序图

状态模式时序图

3.14 代理模式

为另一个对象提供一个替身或占位符以访问这个对象。

  • 虚代理:根据需要来创建开销很大的对象(该对象只在真正需要的时候被创建)
  • 远程代理:用来在不同的空间地址上代表同一个对象
  • copy-on-write代理:拷贝一个大的对象是很消耗资源的,如果这个被拷贝的对象从上次操作以来,根本就没有被修改过,那么再拷贝这个对象是没有必要的,白白消耗资源而已。那么就可以使用代理来延迟拷贝的过程,可以等到对象被修改的时候才真的对它进行拷贝。
  • cache代理:为那些昂贵的操作结果提供临时的存储空间,以便多个客户端可以共享这些结果
  • 防火墙代理:保护对象不被恶意的用户访问和操作
  • 同步代理:使多个用户可以同时访问目标对象而没有冲突
  • 智能指引:在访问对象的时候增加一些附加操作

使用情景

  • 为一个对象在不同的地址空间提供局部代表的时候,可以使用远程代理
  • 需要按照需要创建开销很大的对象的时候,可以使用虚代理
  • 需要控制对原始对象的访问的时候,可以使用保护代理
  • 需要在访问对象的时候执行一些附加操作的时候,可以使用智能指引代理;

结构图

代理模式结构图

时序图

代理模式时序图

3.15 解释器模式

给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

使用情景

  • 当有一个语言需要解释执行,并且可以将该语言中的句子表示为一个抽象语法树的时候,可以考虑使用解释器模式。
  • 在使用解释器模式的时候,还有两个特点需要考虑,一个是语法相对应该比较简单,太复杂的语法不合适使用解释器模式;另一个是效率要求不是很高,对效率要求很高的情况下,不适合使用解释器模式。

结构图

解释器模式结构图

时序图

解释器模式时序图

四 要点

  • 应该以设计为本,以用最简单的方案解决问题为本
  • 良好的OO设计必须具备可复用,可扩充,可维护三个特性
  • 经常把系统中会变化的部分抽出来封装

五 书籍

  • 《设计模式》——四人组
  • 《The Timeless Way of Building》
  • 《A Pattern Language》
  • 《Head First —— 设计模式》