Design Principles and Design Patterns
Table of Contents
1. 面向对象设计六大原则
参考:Uncle Bob 的经典巨作 Agile Software Development: Principles, Patterns, and Practices
中文名:《敏捷软件开发——原则、模式与实践》
1.1. 单一职责原则
英文名称是 The Single Responsibility Principle,简称 SRP。
原则的描述: 就一个类而言,应该仅有一个引起它变化的原因。
1.2. 开放——封闭原则
英文名称是 The Open-Close Principle,简称 OCP。
原则的描述: 软件实体(类、模块、函数等等)应该是可以扩展的,但是不可修改的。 它有两个含义,一是对于扩展是开放的,二是对于修改是封闭的。
1.3. 里氏替换原则
英文名称是 The Liskov Substitution Principle,简称 LSP。
原则的描述:子类型必须能够替换掉它们的基类型。
里氏替换原则是 Barbara Liskov 女士在 1988 年发表的,数学定义比较复杂,用白话表达出来就是: 把父类都替换成它的子类,程序的行为没有变化。 里氏替换原则使开放——封闭原则成为可能。如果违反了里氏替换原则,就潜在违反了开放——封闭原则。
1.3.1. Java 对里氏替换原则的支持
在编译时期,Java 语言编译器会检查一个程序是否符合里氏替换原则。
里氏替换原则要求凡是基类型使用的地方,子类型一定适用,因此子类型必须具备基类型的全部接口。举例而言,一个基类声明了一个 public 方法 method(),它的子类型将这个方法的访问权限从 public 改为 private,这破坏了里氏替换原则,Java 编译器会给出编译错误。
参考:《Java 与模式(阎宏著, 2002)》第 7 章
1.4. 依赖倒置原则
英文名称是 The Dependency Inversion Principle,简称 DIP。
原则的描述:
A、高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
B、抽象不应该依赖于细节。细节应该依赖于抽象。
举例说明如下:
我们做的项目大多要访问数据库,所以我们就把访问数据库的代码写成了函数,每次做好新项目时就去调用这些函数,这叫做高层模块依赖低层模块。这样不太好。做新项目时,业务逻辑的高层模块都是一样的,但客户却希望使用不同的数据库或存储信息方式,这时就出现麻烦了。我们想再次利用这些高层模块时,但是高层模块都是与低层的访问数据库绑定在一起的,没办法利用这些高层模块!如果高层模块和低层模块都依赖于抽象(即接口),则无论高层模块还是低层模块都可以很容易地被复用了!
备注: 依赖倒置原则是框架设计的核心原则。
1.4.1. 依赖倒置原则与设计模式
以抽象方式耦合是依赖倒置原则的关键。 很多设计模式的研究和应用是以依赖倒置原则为指导原则的。
参考:《Java 与模式(阎宏著, 2002)》第 8 章
1.4.1.1. 工厂模式
消费对象的客户端应当只依赖于对象的抽象类型,而不是它的具体类型。Java 中将一个具体的类实例化的时候,必须调用这个具体类的构造函数,所以无法做到只依赖于抽象类型。
设计模式给出了解决这个问题的可行方案,其中最典型的是工厂模式。工厂模式将创建一个类的实例的过程封装起来,消费这个对象的客户端仅仅得到实例化的结果,以及这个实例的抽象类型。当然,任何方法都无法回避 Java 语言中所要求的 new 关键安和直接调用具体类的构造函数的做法。简单工厂模式交过个违反“开放——封闭”原则以及依赖倒转原则的做法封装到了一个类里面,而工厂方法模式将这个违反原则的做法推迟到了具体工厂中,如图 1 所示。
Figure 1: 工厂方法模式和依赖倒置原则
这样,通过适当的封装,工厂模式可以净化大部分的结构,而将违反原则的做法孤立到易于控制的地方。
1.4.1.2. 模板方法模式
模板方法模式是依赖倒置原则的具体体现。
1.4.1.3. 迭代器模式
迭代器模式提供一个聚集的内部迭代功能,客户端得到的是一个 Iterator 抽象类型,并不知道迭代器的具体实现以及聚集对象的内部结构。聚集的内部结构的改变不会波及到客户端,从而实现了对抽象接口的依赖。
1.5. 接口隔离原则
英文名称是 The Interface Segregation Principle,简称 ISP。
原则的描述:不应该强迫客户依赖于它们不用的方法。
使用多个专门的接口比使用单一的总接口要好。一个类对另外一个类的依赖性应当是建立在最小的接口上的。从某种程度来讲, 接口隔离原则可以看做是接口层的单一职责原则。
1.6. 迪米特法则(最少知识原则)
迪米特法则(Law of Demeter)又称最少知识原则(Least Knowledge Principle),就是说, 一个对象应当对其他对象有尽可能少的了解。
迪米特法则最初是用来作为面向对象的系统设计风格的一种法则,于 1987 年秋天由 Ian Holland 在美国 Northeastern University 为一个叫做迪米特(Demeter)的项目设计提出的,因此叫做“迪米特法则”。
没有任何一个其他的 OO 设计原则像迪米特法则这样有如此之多的表述方式,下面给出的也只是众多的表述中较有代表性的几种:
- Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
- Each unit should only talk to its friends; don't talk to strangers.
- Only talk to your immediate friends.
参考:《Java 与模式(阎宏著, 2002)》第 11 章
1.6.1. 迪米特法则与设计模式
1.6.1.1. Facade 模式
Facade 模式(译作外观模式或门面模式)创造出一个门面对象,将客户端所涉及的属于一个子系统的协作伙伴的数目减到最少,使得客户端与子系统内部的对象的相互作用被门面对象所取代。显然,门面模式就是实现代码重构以便达到迪米特法则要求的一个强有力的武器。
1.6.1.2. 调停者模式
调停者模式创造出一个调停者对象,将系统中有关的对象所引用的其他对象数目减到最少,使得一个对象与其同事的相互作用被这个对象与调停者对象的相互作用所取代。显然,调停者模式也是迪米特法则的一个具体应用。
1.7. 总结
概括地讲,面向对象设计原则仍然是面向对象思想的体现。例如,单一职责原则与接口隔离原则体现了封装的思想,开放封闭原则体现了对象的封装与多态,而里氏替换原则是对对象继承的规范,至于依赖倒置原则,则是多态与抽象思想的体现。在充分理解面向对象思想的基础上,掌握基本的设计原则,并能够在项目设计中灵活运用这些原则,就能够改善我们的设计,尤其能够保证可重用性、可维护性与可扩展性等系统的质量属性。这些核心要素与设计原则,就是我们设计的对象法则,它们是理解和掌握设计模式的必备知识。
2. UML 中的四大关系
UML 中的四大关系是:依赖、关联、泛化、实现。它们在 UML 中的图例如图 2 所示。
Figure 2: UML 中的四大关系
参考:
UML 用户指南(第 2 版),2.2 节。
http://www.uml.org.cn/oobject/200911174.asp
http://blog.csdn.net/tianhai110/article/details/6339565
2.1. 依赖 dependency(“uses-a”关系)
依赖关系是两个模型元素间的语义关系,其中一个元素(独立元素)发生变化会影响另一个元素(依赖元素)的语义。
在 Java 中,依赖关系体现为局部变量、方法的参数,以及对静态方法的调用。
如一个类 A 的某一个局部变量的类型是另一个类 B,或一个类 A 中某个方法的参数是另一个类 B 的实例,或一个类 A 调用另一个类 B 的静态方法,那么类 A 依赖于类 B。
注意:如果类 B 出现在类 A 的字段(成员变量)中,那么类 A 与类 B 的关系就超越了依赖关系,而变成关联关系。
2.2. 关联 association
关联关系是类与类之间的联接,它使一个类知道另一个类的属性和方法。
在 Java 中,关联关系是使用类的字段(成员变量)实现的。
2.2.1. 聚合 aggregation(“has-a”关系)
聚合是一种强的关联关系,是整体和部分的关系,它描述了“has-a”关系。
如雁群和大雁之间是聚合关系。
2.2.2. 组合 composition(“has-a”关系)
组合是一种比聚合关系更强的关联关系。 它也是整体和部分的关系,它要求整体和部分的生命周期是一致的。
如鸟和它的翅膀之间是组合关系。
2.3. 泛化 generalization(“is-a”关系)
泛化关系是一种特殊和一般的关系,特殊元素(子元素)基于一般元素(父元素)而建立。
在 Java 中,父类与子类之间就是这种关系。
2.4. 实现 realization
实现关系是类目之间的语义关系,其中的一个类目指定了由另一个类目保证执行的合约。
在 Java 中,接口和实现之间就是这种关系。
3. 设计模式简介
In software engineering, a design pattern is a general reusable solution to a commonly occurring problem within a given context in software design.
A primary criticism of Design Patterns is that its patterns are simply workarounds for missing features in C++.
Peter Norvig demonstrates that 16 out of the 23 patterns in Design Patterns are simplified or eliminated (via direct language support) in Lisp or Dylan, please refer to http://www.norvig.com/design-patterns/
参考:
Design Patterns: Elements of Reusable Object-Oriented Software: https://en.wikipedia.org/wiki/Design_Patterns
Design Patterns (online): http://www.uml.org.cn/c%2B%2B/pdf/DesignPatterns.pdf
Design Patterns in Java Tutorial: http://www.tutorialspoint.com/design_pattern/
Patterns and Best Practices for Enterprise Integration: http://www.enterpriseintegrationpatterns.com/index.html
Refcardz: Design Patterns: https://dzone.com/refcardz/design-patterns
Design Patterns in Dynamic Programming: http://www.norvig.com/design-patterns/design-patterns.pdf
http://www.oodesign.com
4. GoF Patterns
GoF 在《设计模式——可复用面向对象软件的基础》中介绍了 23 种设计模式,并对其进行了分类,如图 3 所示。
Figure 3: Design pattern space
上面的分类是根据两个准则进行的。
- 目的准则,即模式是用来完成什么工作的。模式依据其目的可分为创建型、结构型和行为型三种。创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式对类或对象怎样交互和怎样分配职责进行描述。
- 范围准则,指定模式主要是用于类还是用于对象。类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时刻便确定下来了。对象模式处理对象间的关系,这些关系在运行时刻是可以变化的,更具动态性。从某种意义上来说,几乎所有模式都使用继承机制,所以“类模式”只批那些集中于处理类间关系的模式,而大部分模式都属于对象模式的范畴。
4.1. 创建型模式
4.1.1. 简单工厂模式
简单工厂模式不在 GoF 的 23 种设计模式内。简单工厂模式是工厂模式家庭中最简单实用的模式。
简单工厂模式就是由一个工厂类根据传入的参数决定创建出哪一种产生类的实例。
简单工厂模式有个缺点:如果需要添加新的类,就需要改变工厂类。这些缺点在工厂方法中得到一定的克服。
4.1.2. 工厂方法(Factory Method)
意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
适用场合:1)类不知道它所要创建的对象的类信息;2)类希望由它的子类来创建对象。
4.1.3. 抽象工厂(Abstract Factory)
意图:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
适用场合:1)系统不依赖于产品是如何实现的细节;2)系统的产品族大于 1,而在运行时刻只需要某一种产品族;3)属于同一个产品族的产品,必须绑在一起使用;4)所有的产品族,可以抽取公共接口。
4.1.4. 生成器(Builder)
意图:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
适用场合:1)需要创建的对象有复杂的内部结构;2)对象的属性之间相互依赖,创建时前后顺序需要指定。
4.1.5. 原型(Prototype)
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
适用场合:1)系统不关心对象创建的细节;2)要实例化的对象的类型是动态加载的;3)类在运行过程中的状态是有限的。
4.1.6. 单件模式(Singleton)
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
适用场合:各种“工厂类”。
4.2. 结构型模式
4.2.1. 适配器(Adapter)
意图:将一个类的接口换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适用场合:系统需要使用现有类的功能,但接口不匹配。
4.2.2. 桥接(Bridge)
意图:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
适用场合:1)系统需要在组件的抽象化角色与具体化角色之间增加更多的灵活;2)角色的任何变化都不应该影响客户端;3)组件有多个抽象化角色和具体化角色。
4.2.3. 组合(Composite)
意图:将对象组合成树形结构以表示“部分——整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
适用场合:1)系统中的对象之间是“部分——整体”的关系;2)用户不关心“部分”与“整体”之间的区别。
4.2.4. 装饰(Decorator)
意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
适用场合:1)需要添加对象职责;2)这些职责可以动态添加或者取消;3)添加的职责很多,从而不能用继承实现。
4.2.5. 外观(Facade)
意图:为子系统的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
适用场合:1)为一个复杂的接口提供一个简单界面;2)保持不同子系统的独立性;3)在分层设计中,定义每一层的入口。
4.2.6. 享元(Flyweight)
意图:运用共享技术有效地支持大量细粒度的对象。
适用场合:1)系统中有大量对象;2)这些对象占据大量内存;3)对象中的状态可以很好的区分为外部和内部;4)可以按照内部状态将对象分为不同的组;5)对系统来讲,同一个组内的对象是不可分辨的。
4.2.7. 代理(Proxy)
意图:为其他对象提供一种代理以控制对该对象的访问。
适用场合:对象无法直接访问。
4.3. 行为型模式
4.3.1. 职责链(Chain of Responsibility)
意图:使多个对象都有机会处理请求,不希望请求的发送者和接收者之间有耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
适用场合:1)输入对象需要经过一系列处理;2)这些处理需要在运行时指定;3)需要向多个操作发送处理请求;4)这些处理的顺序是可变的。
注:Servlet 中的 Filter 就是使用了责任链模式。
4.3.2. 命令(Command)
意图:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
适用场合:1)调用者同时和多个执行对象交互;2)需要控制调用本身的生命周期;3)调用可以取消。
4.3.3. 解释器(Interpreter)
意图:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
适用场合:1)当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时;2)在执行过程中,对效率要求不高,但对灵活性要求很高。
4.3.4. 迭代器(Iterator)
意图:提供一种方法,来顺序访问集合中的所有元素。
适用场合:1)访问一个聚合对象的内容,而不必暴露其内部实现;2)支持对聚合对象的多种遍历方式;3)为遍历不同的聚合对象提供一致的接口。
4.3.5. 中介者(Mediator)
意图:用一个中介对象来封装一系列的对象交互。中介者使得各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
适用场合:1)一组对象以定义良好但是复杂的方式进行通信,产生的相互依赖关系结构混乱且难以理解;2)想定制一个分布在多个类中的行为,而又不想生成太多的子类。
4.3.6. 备忘录(Memento)
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
适用场合:1)对象的属性比较多,但需要备份恢复的属性比较少;2)对象的状态是支持恢复的。
4.3.7. 观察者(Observer)
意图:定义对象之间一种“一对多”的依赖关系,当一个对象发生改变时,所有依赖于它的对象都会得到通知并自动更新。
适用场合:1)抽象模型有两部分,其中一部分依赖于另一部分;2)一个对象的改变会导致其他很多对象发生改变;3)当一个对象必须通知其它对象,而它又不能假定其它对象是谁。
4.3.8. 状态(State)
意图:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
适用场合:1)一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为;2)一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。
4.3.9. 策略(Strategy)
意图:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。Strategy 模式使得算法可独立于使用它的客户而变化。
适用场合:1)完成某项业务有多个算法;2)算法可提取公共接口。
4.3.10. 模板方法(Template Method)
意图:定义一个操作中的算法的骨架,而将一些细节的实现放到子类中。Template Method 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
适用场合:1)可以抽取方法骨架;2)控制子类的行为,只需要实现特定细节。
4.3.11. 访问者(Visitor)
意图:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
适用场合:1)一个类型需要依赖于多个不同接口的类型;2)需要经常为一个结构相对稳定的对象添加新操作;3)需要用一个独立的类型来组织一批不相干的操作,使用它的类型可以根据应用需要进行定制。
4.4. 怎么选择设计模式
Here are several different approaches to finding the design pattern that's right for your problem:
- Consider how design patterns solve design problems.
- Read through each pattern's intent.
- Study how patterns interrelate.
- Study patterns of like purpose.
- Examine a cause of redesign. Look at the patterns that help you avoid the causes of redesign.
- Consider what should be variable in your design. Table 1 lists the design aspect(s) that design patterns let you vary independently, thereby letting you change them without redesign.
Design Pattern | Aspect(s) That Can Vary |
---|---|
Abstract Factory | 产品对象家族 |
Builder | 如何创建一个组合对象 |
Factory Method | 被实例化的子类 |
Prototype | 被实例化的类 |
Singletion | 一个类的唯一实例 |
Adapter | 对象的接口 |
Bridge | 对象的实现 |
Composite | 一个对象的结构和组成 |
Decorator | 对象的职责,不生成子类 |
Facade | 一个子系统的接口 |
Flyweight | 对象的存储开销 |
Proxy | 如何访问一个对象;该对象的位置 |
Chain of Responsibility | 满足一个请求的对象 |
Command | 何时、怎样满足一个请求 |
Interpreter | 一个语言的文法及解释 |
Iterator | 如何遍历、访问一个聚合的各元素 |
Mediator | 对象间怎样交互、和谁交互 |
Memento | 一个对象中哪些私有信息存放在该对象之外,以及什么存储 |
Observer | 多个对象依赖于另外一个对象,而这些对象又如何保持一致 |
State | 对象的状态 |
Strategy | 算法 |
Template Method | 算法中的某些步骤 |
Visitor | 某些可作用于一个(组)对象上的操作,但不修改这些对象的类 |
参考:《设计模式——可复用面向对象软件的基础》1.7 节
5. Other Design Patterns
There are many other design patterns. For example: