1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书,在书中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。 这 4 位作者在软件开发领域里也以他们的“四人组”(Gang of Four,GoF)匿名著称。
设计模式就是用来解决软件设计过程中高频问题的一系列套路,能够使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,提高代码的可重用性、可读性、可靠性、灵活性和可维护性。
根据模式是用来完成什么工作来划分。
- 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。
- GoF 中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
- 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构。
- GoF 中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
- 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。
- GoF 中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
根据模式是主要用于类上还是主要用于对象上来划分。
- 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。
- GoF 中的工厂方法、(类)适配器、模板方法、解释器属于该模式。
- 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。
- GoF 中除了以上 4 种,其他的都是对象模式。
前面说明了 GoF 的 23 种设计模式的分类,现在对各个模式的功能进行介绍。
- 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
- 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
- 工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
- 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
- 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
- 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
- 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
- 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
- 装饰(Decorator)模式:动态的给对象增加一些职责,即增加其额外的功能。
- 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
- 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
- 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
- 模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
- 策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算的客户。
- 命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
- 职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
- 状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
- 观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
- 中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
- 迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
- 访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
- 备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
- 解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification)。
- 迭代后只需对新增代码进行测试
- 提高代码可复用性
- 易于扩展和维护
可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。
继承必须确保超类所拥有的性质在子类中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。
- 实现开闭原则的重要方式之一
- 克服继承父类造成的可重用性变差的缺点
- 确保扩展不会给已有系统引入新的错误
子类可以扩展父类的功能,但不能改变父类原有的功能。
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象(High level modules shouldnot depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions)。其核心思想是:要面向接口编程,不要面向实现编程。
- 降低类间耦合度
- 提高系统稳定性
- 减少并行开发引起的风险
- 提高可读性和可维护性
- 每个类尽量提供接口或抽象类,或二者兼备
- 变量的声明类型尽量是接口或者是抽象类(避免调用具体类的属性或方法)
- 任何类都不应该从具体类派生
- 使用继承时尽量遵循历史替换原则
单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(There should never be more than one reason for a class to change)。
- 降低类的复杂度
- 提高类的可读性
- 提高系统的可维护性
- 降低变更引起的风险
发现类的不同职责并将其分离,再封装到不同的类或模块中。
客户端不应该被迫依赖于它不使用的方法(Clients should not be forced to depend on methods they do not use)。 一个类对另一个类的依赖应该建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。
含义:要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义
- 能减少项目工程中的代码冗余
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。
- 降低了类之间的耦合度,提高了模块的相对独立性
- 由于耦合度降低,从而提高了类的可复用率和系统的扩展性
但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。
- 在类的划分上,应该创建弱耦合的类
- 在类的结构设计上,尽量降低类成员的访问权限
- 在类的设计上,优先考虑将一个类设置成不变类
- 在对其他类的引用上,将引用其他对象的次数降到最低
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)
- 谨慎使用序列化(Serializable)功能
在软件复用时,尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。 如果要使用继承关系,则必须严格遵循里氏替换原则。
通常类的复用分为继承复用和合成复用。
继承复用缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化
合成复用优点:
- 维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用
- 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象
通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。