1 设计模式的基本要素
- 模式名称(pattern name):助记名,如抽象工厂模式等;
- 问题(problem):描述了应该在何时使用模式;
- 解决方案(solution);
- 效果(consequences)。
2 设计模式怎样解决涉及问题
2.1 寻找合适的对象
面向对象程序设计由对象组成,对象包括数据和对数据进行操作的过程,过程通常称为方法或操作。对象在收到客户的请求(或消息)后,执行相应的操作。
客户请求是使对象执行操作的***唯一***方法,操作又是对象改变内部数据的**唯一方法。由于这些限制,对象的内部状态是被封装的,它不能被直接访问,它的表示对于对象外部是不可见的。
面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装、粒度、依赖关系、灵活性、性能、演化、复用等等,它们都影响着系统的分解,并且这些因素通常还是互相冲突的。
2.2 决定对象的粒度
对象大小和数目。
2.3 指定对象接口
- 对象声明的每一个操作指定操作名、作为参数的对象和返回值,这就是所谓的操作的型构(signature);
- 对象操作所定义的所有操作型构的集合被称为该对象的接口(interface);
- **类型(type)**是用来标识特定接口的一个名字:
- 当一个类型的接口包含另一个类型的接口时,我们就说它是另一个类型的子类型(subtype),另一个类型称之为它的超类型(supertype);
- 我们常说子类型继承了它的超类型的接口。
- 动态绑定(dynamic binding)
- 发送给对象的请求和它的相应操作在运行时刻的连接称之为动态绑定(dynamic binding);
- 动态绑定是指发送的请求直到运行时刻才受具体的实现的约束;
- 动态绑定允许你在运行时刻彼此替换有相同接口的对象,这种可替换性就称为多态(polymorphism);
- 多态允许客户对象仅要求其他对象支持特定接口,除此之外对其假设几近于无;
- 多态简化了客户的定义,使得对象间彼此独立,并可以在运行时刻动态改变它们相互的关系。
- 设计模式:
- 通过确定接口的主要组成成分及经接口发送的数据类型,来帮助你定义接口;
- 也许还会告诉你接口中不应包括哪些东西;
- 指定了接口之间的关系。特别地,它们经常要求一些类具有相似的接口;或它们对一些类的接口做了限制。
2.4 描述对象的实现
- 实例化:
- 对象通过实例化类来创建,此对象被称为该类的实例;
- 当实例化类时,要给对象的内部数据(有实例变量组成)分配存储空间,并将操作与这些数据联系起来。
- 下图虚箭头线表示***一个类实例化另一个类的对象,箭头指向被实例化的对象的类***:
- 类继承(class inheritance):
- 新的类可以由已存在的类通过**类继承(class inheritance)**来定义;
- 当*子类(subclass)继承父类(parent class)时,子类包含了父类定义的所有数据和操作。***子类的实例对象包含所有子类和父类定义的数据,且它们能完成子类和父类定义的所有操作;
- 以竖线和三角表示子类关系:
- 抽象类(abstract class):
- 抽象类的主要目的是为它的子类定义公共接口;
- 一个抽象类将把它的部分或全部操作的实现延迟到子类中,因此,一个抽象类不能被实例化;
- 在抽象类中定义却没有实现的操作被称为抽象操作(abstract operation);
- 非抽象类称为具体类(concrete class);
- 子类能够改进和重新定义它们父类的操作。这个过程称为重定义(override),重定义使得子类能接管父类对请求的处理操作;
- 抽象类图示:
- 抽象类的***类名***和***操作***均以斜体表示;
- 图中可以包括实现操作的伪代码,如果这样,则代码将出现在带有摺角的框中,并用虚线将该摺角框与代码所实现的操作相连:
- 混入类(mixin class):
- 混入类是给其他类提供可选择的接口或功能的类;
- 混入类与抽象类一样不能实例化;
- 混入类要求多继承;
- 图示如下:
-
类继承与接口继承的比较;
-
针对接口编程,而不是针对实现编程(面向对象设计的第一个原则);
2.5 运用复用机制
理解**对象、接口、类和继承之类的概念对大多数人来说并不难,问题的关键在于如何运用它们写出灵活的、可复用的软件。设计模式将告诉你怎样去做。
2.5.1 继承和组合的比较
面向对象系统中功能复用的两种最常用的技术是类继承和对象组合(object composition)。
- 类继承:
- 类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse);
- 术语 白箱 是相对可视性而言:在继承方式中,父类的内部细节对子类可见;
- ***【优缺点及解决方案】***:
- 【优点】类继承实在编译时刻静态定义的,且可直接使用;
- 【优点】类继承可以较方便地改变被复用的实现;
- 【缺点】因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现;
- 【缺点】父类通常至少定义了部分子类的具体表示,因为继承对子类揭示了其父类的实现细节,所以继承常被认为 破坏了封装性 ;
- 【缺点】子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。
- 【解决方法】只继承抽象类,因为抽象类通常提供较少的实现。
- 对象组合:
- 新的更复杂的功能可以通过组装或组合对象来获得;
- 对象组合要求被组合的对象具有良好定义的接口;
- 这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的,对象只以 黑箱 的形式出现。
- ***【优缺点及解决方案】***:
- 【优点】对象组合是通过获得对其他对象的引用而在运行时刻动态定义的;
- 【优点】组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用;
- 【优点】这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现时基于接口写的,所以实现上存在较少的依赖关系;
- 【优点】优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物;
- 【优点】基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。
因此:
优先使用对象组合,而不是类继承(面向对象设计的第二个原则);
2.5.2 委托
- 委托(delegation) 是一种组合方法,它使组合具有与继承同样的复用能力;
2.5.3 继承和参数化类型的比较
另一种功能复用技术(并非严格的面向对象技术)是参数化类型(parameterized type),也就是类属(generi)或模板(templates)(C++)。
2.6 关联运行时刻和编译时刻的结构
2.7 设计应支持变化
下面列举一些导致重新设计的一般原因,以及解决这些问题的设计模式:
原因 | 解决 |
---|---|
通过显示地指定一个类来创建对象 | Abstract Factory Factory Method Prototype |
对特殊操作的依赖 | Chain of Responsibility Command |
对硬件和软件平台的依赖 | Abstract Factory Bridge |
对对象表示或实现的依赖 | Abstract Factory Bridge Memento Proxy |
算法依赖 | Builder Iterator Strategy Template Method Visitor |
紧耦合 | Abstract Factory Command Facade Mediator Observer Chain of Responsibility |
通过生成子类来扩展功能 | Bridge Chain of Responsibility Composite Decorator Observer Strategy |
不能方便地对类进行修改 | Adapter Decorator Visitor |
2.8 怎样选择设计模式
3 参考资料
[1]《设计模式:可复用面向对象软件的基础》
[2]《大话设计模式》