日期
Apr 20, 2022
Tags
设计模式
导语
在上一篇设计模式辨析中,针对继承在编译时静态决定类,可扩展性差等问题,装饰模式提供了比继承更弹性的方法来为类添加功能函数,这篇文章中进一步讨论了继承的潜在问题,重点着眼于类和对象之间的关系。桥接模式、组合模式和享元模式都属于「结构型设计模式」,而中介者模式属于「行为型设计模式」,为了更好的几种模式对比而放在一篇文章中,尽管关系篇的命名可能不够准确,但还是比较能概括这几种设计模式的共同点。
继承
继承作为面向对象的三个基本特征之一,是最常见的一种类关系,作用如下:
- 子类通过继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法
- 或子类从父类继承方法,使得子类具有父类相同的行为。
优点:继承可以使用子类重写父类方法的方式进行扩展,提高了代码的复用性,并获得了一定的可扩展性
缺点:继承的过度使用会导致类的结构过于复杂,对象之间关系太多,代码难以维护,扩展性差。
继承关系的潜在问题
- 对象的继承关系在编译时静态定义好的,无法在运行时改变从父类继承的实现。
- 子类的实现与父类有紧密的依赖关系,父类实现中的任何变化都会导致子类的变化
- 复用子类时继承的实现可能不适合解决新的问题,这时父类必须重写或替换,这种依赖关系限制了灵活性和复用性
子类和父类之间的关系是一种高耦合关系。
桥接模式(Bridge Pattern)
一个父类可以通过子类实现多种变化。在类变化较为复杂的情况下,只使用继承会造成大量的类增加,不能满足开放-封闭原则。继承是一种强耦合的结构,我们的设计目标是找到一个弱耦合、松耦合的结构来表示抽象类和实现之间的关系,这个设计模式就是桥接模式。
桥接模式通过解耦类不同方向的变化,使用对象组合的方式,把两个角色之间继承关系改为组合关系,从而使得两者应对各自独立的变化。
什么是桥接模式?
顾名思义,「桥」起到连接作用,在桥接模式中,通过「桥」连接的抽象和实现两个独立的结构,这两个部分可以以继承的方式独立扩展、变化而不相互影响。
定义:桥接模式将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- 抽象与实现分离,不是说抽象类和派生类分离
- 实现指的是抽象类和它的派生类用来实现自己的对象,实现可以有多角度(方向)分类
桥接模式的核心意图:将每一种分类的实现分离出来,让他们独立变化,减少它们之间的耦合,每种实现的变化不会影响其他实现,从而达到应对变化的目的。
桥接模式如何应对变化?
- 找出变化封装之
- 优先使用对象聚集,而不是继承,即合成/聚合复用原则
合成/聚合复用原则(CARP)
尽量使用合成/聚合,尽量不要使用类继承
合成(Composition):表示一种强“拥有”关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样
聚合(Aggregation):表示一种弱“拥有”关系,体现A对象包含B对象,但B对象不是A对象的一部分
合成和聚类都是关联的特殊种类。
好处:
- 有限使用对象的合成/聚合将有助于保持每个类被封装,并被集中在单个任务上。
- 这样类和类继承层次都可以保持较小规模,不至于增长过多,而不可控制。
桥接模式示意图
假设现在有一个书本生产系统,生产的书本有Book1、Book2两种类型,黄色和绿色两种不同颜色,现有抽象类Book,若仅使用继承,那么就需要如下图示的四种子类才可以满足需求。
若要添加书本类型或颜色类型,均需要声明对子类进行新的扩展,需要的子类数将呈现爆炸性增长,代码复杂度将不断增加。
![仅使用继承](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F514ca662-fe5c-4e72-bef2-58fc275cc427%2FUntitled.png?table=block&id=e76b4060-d70d-46e2-984e-649387cb8b61&t=e76b4060-d70d-46e2-984e-649387cb8b61&width=1149&cache=v2)
而使用桥接模式,可以将书本类型(抽象)和颜色类型(实现)两部分分离开来,改继承为组合,扩展和变化将在两个独立角度中进行,不再需要通过添加子类的方式实现扩展。
![桥接模式](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F58dafdcf-d84d-4ae3-979e-949936c31842%2FUntitled.png?table=block&id=42d63411-b73e-457b-9247-c36a8a0f714b&t=42d63411-b73e-457b-9247-c36a8a0f714b&width=1027&cache=v2)
颜色相关的代码将被抽取到颜色类中,并通过在书本类中添加指向颜色对象的成员变量,将书本和颜色两部分连接起来。使用桥接模式后新增颜色将不会对现有的形状类产生影响,反之新增形状也不会对现有的颜色类产生影响,新增功能或方法都只需要扩展类即可,不需要修改现有类,符合“开放-封闭”原则。
桥接模式代码实例[3]:
桥接模式的优缺点
优点:
- 实现了抽象和实现的分离,减少了它们之间的耦合
- 多角度分类实现对象可以降低项目的复杂度和类之间的关系,不同角度之间不会相互影响
- 新增功能或方法都只需要扩展类即可,不需要修改现有类,符合“开放-封闭”原则,提高了类的可扩展性
缺点:
- 桥接模式的引入会增加系统的复杂度,增加了代码的理解和设计难度
- 桥接模式的使用范围有一定局限性,需要识别出系统中两个或多个独立变化的维度才能使用。
组合模式(Composite Pattern)
什么是组合模式?
定义:将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式能让用户以一致的方法处理单个对象和组合对象。
- 使用树形结构管理对象
- 能够在单个对象之间和组合对象之间游走
- 基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,代码中的所有使用基本对象都可以使用组合对象
组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。任何用到基本对象的地方都可以使用组合对象,客户可以一致地使用组合结构和单个对象。
组合、组件、叶节点、枝节点
组合由组件构成,组合结构中的任意对象称为组件,组件有两种类型:
- 叶节点元素
- 枝节点元素(组合类)
组合的树形结构:
根是顶层的组合,往下是枝节点,最末端是叶节点
- 在组合模式中所有的对象都属于同一个类,整体与部分可以被一致对待
- 使用组合结构,我们能把相同的操作应用到组合和个别对象上,在大多数情况下,我们可以忽略对象组合和单个对象之间的差别
组合模式的分类
透明方式
在组件接口中声明所有用来管理子对象的方法,使子对象(叶节点和枝节点)对于外界没有区别,具备完全一致的行为接口。
透明性:组合模式以单一责任设计原则换取“透明性”,组件的接口同时包含一些管理子节点和叶节点的操作,无论是子节点还是叶节点对用户而言都是透明的
存在的问题:
- 存在叶节点类不具有相关的方法,实现没有意义的方法的问题,违背了单一职责原则,即最少知识原则
- 用户有可能做一些不恰当或没有意义的操作,失去一些安全性
安全方式
在组合对象中实现所有用来管理子对象的方法,组件类中不声明相关方法,客户端调用时需要做出相应的判断
安全性:将责任进行区分,放在不同的接口上,代码需要通过条件语句等判断处理不同类型的节点,失去了透明性,但这样的设计比较安全
安全方式符合设计模式的的单一职责原则和接口隔离原则
存在的问题:
- 客户端需要对枝节点和子节点进行区分,才能处理不同层次的操作,无法依赖抽象,违背了设计模式的依赖倒置原则。
总结:在实现组合模式时,需要根据需要平衡透明性和安全性。
组合模式代码实例[3]:
何时使用组合模式
- 需求中体现部分与整体层次的结构时,希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑组合模式
- 当程序中有多个对象集合,且对象之间有“整体/部分”关系,并且想用一致方式处理这些对象时,需要组合模式
组合模式的优缺点
优点:
- 用户不需要关心对象是组合对象,组合模式可以让用户一致地使用组合结构和单个对象
- 组合模式中叶节点的添加十分方便,符合开放-关闭原则,便于维护
缺点:
- 叶节点和枝节点都是实现类,而不是接口,违反了依赖倒置原则
- 组合类的引入提升了设计的复杂度,客户端需要花时间理清类之间的层次关系
享元模式(Flyweight Pattern)
在程序设计中,有时需要使用大量对象,但使用很多对象会导致的内存开销过大的问题,若使用的对象大量都是重复,则会造成资源的浪费。享元模式是针对这一问题提出的,可以避免大量相似类的开销。
- 如果这些实例除了几个参数外基本上都是相同的时,就能够大幅度地减少需要实例化的类的数量。
- 如果能把那些参数移到类实例的外面,在方法调用时把它们传递进来,就可以通过共享大幅度地减少单个实例的数目。
什么是享元模式?
运用共享技术有效地支持大量细粒度的对象
- 本质:缓存共享对象,降低内存消耗。
- 享元模式属于工厂方法模式的改进
享元对象的状态分类
内部状态
在享元对象内部且不会随环境改变而改变的共享部分
外部状态
随着环境改变而改变,不可以共享的状态
享元模式的主要角色:
享元工厂(Flyweight Factory)
用来创建并管理Flyweight对象,主要用来确保合理地共享Flyweight,当用户请求一个Flyweight时,为对象提供一个已创建的实例,若不存在则为对象创建一个新的Flyweight实例
抽象享元类(Flyweight)
所有具体享元类的超类或接口,通过这个接口,Flyweight可以接受并作用于外部状态
具体享元类(Concrete Flyweight)
继承Flyweight超类或实现Flyweight接口,并为内部状态增加存储空间
非享元类(Unshared Concrete Flyweight )
指不需要共享的Flyweight子类
享元模式代码实例[3]:
什么时候使用享元模式
- 当一个应用程序使用了大量的对象,而这些大量对象产生了巨大的存储开销时应该考虑使用享元模式
- 对象的大多数状态可以使用外部状态,删除对象的外部状态可以用相对较少的共享对象取代很多组对象时,可以考虑使用享元模式
享元模式的优缺点
优点:
- 使用享元模式中共享对象的使用可以大大减少对象实例总数,节约存储开销,节约量随着共享状态的增多而增大
- 避免大量细粒度对象的使用,又不影响程序,运用共享技术有效地支持大量细粒度的对象
缺点:
- 需要维护一个记录了系统已有的所有享元的列表,列表也需要耗费资源
- 享元模式会增加系统的复杂度,对象共享需要使一些状态外部化,会增加程序的逻辑复杂度,在程序中有足够多的的对象实例可供共享时才值得使用享元模式
享元模式的应用
- NET中字符串string使用了享元模式,对于相同的字符串对象使用同一个实例,用引用指向的方式实现字符串的内存共享
- 棋类游戏中,棋子的颜色是内部状态,位置是外部状态,以围棋为例,使用享元模式,仅声明黑白两个棋子实例即可,大大减少了内存开销
中介者模式(Mediator Pattern)
当程序设计中使用大量对象时,对象之间的关联关系和系统代码逻辑复杂,可扩展性差,不利于应对变化。中介者模式通过对对象集体行为的封装来避免这个问题。
什么是中介者模式?
定义:用一个中介对象来封装一系列的对象交互。中介者使个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- 中介者模式容易在系统中应用,也容易误用。在考虑使用中介者模式前,一定得考虑好系统设计的合理性
中介者
中介者模式将集体行为封装成一个独立的中介者对象
- 中介者负责控制和协调一组对象间的交互,充当中介是的注重的对象不再相互显式引用,对象只知道中介者,减少了相互连接的数量。类之间的耦合度降低,有利于复用。
同事类
实现业务的具体类,各同事类之间的行为均通过中介者进行控制、协调。
中介者模式的关系图
![多对象关系图](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fb1980262-4aef-41c7-b29c-6fc777fdce6d%2FUntitled.png?table=block&id=50ab4f8d-d846-49c1-8e4a-86f8749c9a71&t=50ab4f8d-d846-49c1-8e4a-86f8749c9a71&width=336&cache=v2)
![中介者模式对象关系图](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fba2df71c-25f5-4e40-baa2-ded9397431e0%2FUntitled.png?table=block&id=2cba0e59-0f81-402e-8c1b-3e784486eeee&t=2cba0e59-0f81-402e-8c1b-3e784486eeee&width=384&cache=v2)
中介者模式使得对象均只需与中介者沟通,对象之间的关系转变为星型结构,能有效地减少系统的耦合
中介者模式与桥接模式:
- 桥接模式,把一个类里面多对多的关系,转化为类外部的多个类之间的多对一关系。
- 中介模式,把多个类的多对多的关系,转化为一对多的关系。从而让代码的职责更为单一,更利于复用、扩展、测试。
中介者模式代码实例[3]:
中介者模式的优缺点
优点:
- 减少各个对象实例之间的耦合,可以独立改变和复用各个与中介者关联的对象实例
- 将对象之间的协作进行抽象,将中介作为一个独立的概念封装在对象中,这样关注的对象就从对象各自本身的行为转移到它们之间的交互上来,也就是站在一个更宏观的角度去看待系统
缺点
- 中介者模式的优点来自集中控制,缺点也来自集中控制
- 中介者实现了控制集中化,将交互复杂性转变为中介者的复杂性,当同事类越多,中介者的业务就越复杂,代码会变得难以管理和改动。
中介者模式的应用:
中介者模式一般应用于一组对象以定义良好但复杂的方式进行通信的场合,以及想定制一个分布在多个类中的行为,而不想生成太多子类的场合
- .NET中Windows应用程序的Form,Web网站程序的aspx都使用了中介者模式
总结
桥接模式、组合模式和中介者模式都改变类或对象之间的关系,从而达到各自减少对象数量或降低代码复杂度的目的。
- 桥接模式使用合成/聚合复用原则,变继承为聚合,将抽象部分与它的实现部分分离,减少了子类数量,降低了代码的耦合度
- 组合模式用树形结构表示“部分-整体”的层次结构来管理对象,使得用户可以用统一的接口使用组合结构中的所有对象,简化了代码,提高了可扩展性
- 中介者模式使用中介者负责控制和协调一组对象间的交互,将多对多的关系转为一对多,降低了对象之间的耦合度,提升了代码的复用性、可扩展性。
享元模式则主要为了解决内存资源浪费问题,通过对对象之间相似部分的抽取,利用共享技术,减少对象实例。
区分这几种设计模式的关键,依旧在于把握目的(意图)的不同。
Reference
[1] 程杰. 大话设计模式[M/OL]. 清华大学出版社 2007-12-1, 2007.
[2] FREEMAN E, FREEMAN E, BATES B, 等. Head First 设计模式(中文版)[M/OL]. TAIWAN公司 O, 译. 中国电力出版社, 2007.
[3] JavaCodeAcc 模式相关代码 https://github.com/echoTheLiar/JavaCodeAcc/tree/master/src/designpattern/