在你接触过的安卓项目当中,如监听器、适配器、迭代器等并不陌生,然而它们无不体现着设计模式的精髓。设计与模式的结合,往往与设计能力与代码质量息息相关。同理,逆过来思考此类问题,对于一些优秀项目、源码的理解障碍往往是对其设计 (逻辑、性能、解耦等) 的理解,而非源码本身。而作为开发者,知其然知其所以然,这也正是我们深入学习设计模式的理由之一。
当然,我们还要正视学习设计模式的心态。掌握了各种设计模式,并不代表你的设计能力与代码质量就会突飞猛进,同样在项目中运用设计模式也不是生搬硬套就解决问题了,在《 Head First 设计模式 》一书中,则把设计模式的使用心智分为:初学者、中级人员和悟道者,虽有“玄学”的味道,但也恰当。即没有最好的模式,而是你综合众多因素,根据经验、方法来筛选合适的设计模式与你的项目结合,运用。
最后,对于设计模式的学习,不要局限于《 Android 源码设计模式 》本身,你可以搭配一些经典论文、综述,或者书籍,以至于怀疑一个问题的正误,多比对、多思考,以得到最精确的理解。而本笔记的作用也在于此,即一个设计模式的架构,或是借鉴学习,亦或是复习之需便于查询。
- 推荐书目:
- 《 设计模式(可复用面向对象软件的基础)》:待补充
- 推荐项目:
更新进程
- 2018.01.30:完成序言;
- 2018.01.31:更新第壹章;
- 2018.02.05:更新第贰章 ( 共 23 种设计模式 );
- 2018.03.19:更新完毕.
内容总览
零 本书架构
面向对象六大原则
单一职责原则
优化代码第一步。即就一个类而言,应该有且仅有一个引起它变化的原因。
开闭原则
让程序更稳定,更灵活。即软件中的对象(类、模块、函数等)应该对于扩展是开放的,但对于修改是封闭的。
里氏替换原则
构建扩展性更好的系统。
依赖倒置原则
让项目拥有变化能力,即依赖抽象,不依赖具体实现。
接口隔离原则
系统拥有更高灵活性。
迪米特原则
也称为「最少知识原则」。即一个对象应对其他对象有最少的了解。
二十三种设计模式
模式名称 | 一句话描述 |
---|---|
单例模式 | 一个类只有一个实例 |
Build 模式 | 自由拓展你的项目 |
原型模式 | 使程序运行更高效 |
工厂方法模式 | 生成复杂对象 |
抽象工厂模式 | - |
策略模式 | 时势造英雄 |
状态模式 | 随遇则安 |
责任链模式 | 使编程更有灵活性 |
解释器模式 | 化繁为简的翻译机 |
命令模式 | 让程序畅通执行 |
观察者模式 | 解决、解耦的钥匙 |
备忘录模式 | 编程中的后悔药 |
迭代器模式 | 解决问题的第三者 |
模块方法模式 | 抓住问题的核心 |
访问者模式 | 数据结构与操作分离 |
中介者模式 | 调解者模式或调停者模式 |
代理模式 | 委托模式 |
组合模式 | 物以类聚 |
适配器模式 | 得心应手粘合剂 |
装饰模式 | 动态给对象添加额外职责 |
享元模式 | 对象共享,避免创建多对象 |
外观模式 | 统一编程接口 |
桥接模式 | 连接两地的交通枢纽 |
MVC 与 MVP 模式
壹 面向对象编程六大原则
单一职责原则
- Single Responsibility Principle,SRP.
- 即就一个类而言,应该仅有一个引起它变化的原因。
如何划分一个类,一个函数的职责?每个人的经验不同,观点看法也不同,故视具体任务而定。但它也有一些基本的知道原则:
- 两个完全不一样的功能就不应该放到同一个类中。
- 一个类中应该是一组相关性很高的函数、数据的封装。
开闭原则
- Open Close Principle,OCP.
- 即软件中的对象(类、模块、函数等)应该对于扩展是开放的,但对于修改是封闭的。
- 勃兰特·梅耶. 《面向对象软件构造》中提倡:
- 新的或改变的特性应通过新建不同的类实现,新建的类可通过
继承
的方式来重用原类的代码。 - 已存在的实现类对于修改是封闭的,但新的实现类可通过
覆写父类的接口
应对变化。
- 新的或改变的特性应通过新建不同的类实现,新建的类可通过
开闭原则知道我们,当软件需变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。
里氏替换原则
往往开闭原则与里氏替换原则是生死相依、不离不弃的。
- Liskov Substitution Principle,LSP。
- 所有引用基类的地方必须能透明地使用其子类的对象。
1 | public abstract class View { |
上述例子中,任何继承自 View 类的子类都可以设置给 show 方法,即里氏替换。这样千变万化的 View 传递给 Window,Window 只管组织 View,并显示在屏幕上。
依赖倒置原则
- Dependence Inversion Principle,DIP.
- 一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节。
依赖倒置原则的几个关键点:
高层模块不应该依赖低层模块,两者都应以来其抽象(接口或抽象类)
高层模块指调用端,低层模块指实现类。
抽象不应该依赖细节
- 细节应依赖抽象
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系。一句话概括:
面向接口编程,面向抽编程
1 | /* |
接口隔离原则
- Interface Segregation Principles,ISP.
- 类间的依赖关系应建立在最小的接口上。ISP 将非常庞大、臃肿的接口拆分成更小的和更具体的接口。IPS的目的是系统解开耦合。
如上例中,ImageLoader 中的 ImageCache,ImageLoader 只需要知道该缓存对象有存、取缓存图片的接口即可,其他一概不管。
迪米特原则
一个对象应对其他对象有最少的了解、即类的内部如何实现与调用者、依赖者没关系,调用者或依赖者之需知道它需要的方法即可,其他一概不管。
图 1-1 展示了租客、中介与房间相互之间的需求关系。
图 1-1 租客、中介与房间关系 因为租客只需要房子,即把需求转达中介,对房子具体的租金、维修、签约等交由中介处理,租客不需要再了解细节。改进效果见图 1-2。
图 1-2 改进:租客、中介与房间的关系
贰 二十三种设计模式解析
单例模式
单例模式的定义
确保某
一个类只有一个实例
,而且自行实例化并向整个系统提供这个实例。一个类只有一个实例
:避免产生多个对象消耗过多资源,如访问 I/O 和数据库等资源。
单例模式 UML 类图
实现单例模式主要有如下几个关键点:
构造函数
不对外开放,一般设为私有
;- 通过一个
静态方法
或者枚举返回
单例类对象; - 确保单例类的对象有且只有一个,尤其多线程环境下;
确保单例类对象在
反序列化
时不会重新构建对象;序列化:将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以是字节或是XML等格式。
反序列化:而字节的或XML编码格式可以还原完全相等的对象。这个相反的过程又称为反序列化。
单例模式的简单示例
例如一个公司只有一个 CEO,一个应用只有一个 Application 对象等。下面以公司里的 CEO 为例来简单演示,即一个公司可有几个 VP,无数个员工,但 CEO 只有一个。
1 | // 员工的基类 |
单例模式的其他实现方式
懒汉模式
- 声明一静态对象;
- 调用 getInstance() 方法初始化 ( 用时才初始化,即惰性处理机制 )
1 | // 懒汉单例模式:用时才初始化,即惰性处理机制 |
- 懒汉单例模式的优缺点:
- 优点 - 使用时才实例化,一定程度上节约资源。
- 缺点 - 每次调用 getInstance() 都进行同步,造成不必要同步开销。
Double CheckLock (DCL)
1 | public class Singleton { |
DCL 又称「丑陋的优化」?
DCL 虽一定程度解决了资源消耗,多余同步、线程安全等问题,但某种情况下还是会出现失效问题 ( 双重检查锁定(DCL) ),即称「丑陋的优化」。
线程A,执行
sInstance = new Singleton();
,编译器会编译成多条汇编指令,具体汇编指令的分工为:- Step.01 给 Singleton 实例分配内存;
- Step.02 调用 Singleton() 的构造函数;
- Step.03 将 sInstance 对象指向分配的内存空间;
然而 Java 编译器允许处理器乱序执行,即有「1-2-3」或「1-3-2」的执行顺序。
- 若执行「1-3-2」的顺序,这样会使 DCL 的优化失效,即第三步执行完毕,sInstance 非空,线程B取走 sInstance。再使用时就会报错。
静态内部类单例模式
1 | public class Singleton { |
第一次加载 Singleton 的 getInstance() 方法才会使 sInstance 被初始化。因此,第一次调用 getInstance() 方法会导致虚拟机加载 SingletonHolder 类.
内部类是延时加载的,只会在第一次使用时加载,不使用不加载。这样,即保证了线程安全,又保证单例对象唯一性,同时也延迟单例的实例化。
枚举单例
1 | // 默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。 |
总结
不管以哪种形式实现单例模式,它们的核心原理都是将
构造函数私有化
,并通过静态方法获取一个唯一的实例
。获取实例的过程须保证线程安全,防止反序列化导致重新生成实例对象等。
选择哪种实现形式取决项目本身,如是否是复杂的并发环境、JDK 版本是否过低、单例对象的资源消耗等。
- 单例模式的优缺点
- 优点
1) 只生成一个实例,减少系统的性能开销;
2) 当一对象的产生需要较多资源时,如读取配置、产生其他依赖对象时,可通过应用启动时直接产生一个单例对象,永久驻留内存。 - 缺点
1) 单例模式一般没有接口,扩展性难;
2) 单例对象若持有 Context,那么很容易引发内存泄漏,此时需注意传递给单例对象的 Context 应该是Application.Context
。
- 优点
Bulider 模式
Bulider 模式的定义
- 创建型设计模式。
- 将一个复杂对象的
构建
与它的表示
分离,使得同样的构建过程可以创建不同的表示。 - Builder 模式是一步步创建一个复杂对象的,它允许用户在不知内部构建细节的情况下,可以更精细地控制对象的构造流程。
Builder 模式的使用场景
- 产品类非常复杂,或产品类中调用顺序不同产生不同的作用,这时需要使用 Builder 模式。
- 初始化一个对象特别复杂,如参数多,且很多参数都具有默认值。
Builder 模式的UML类图
Builder 模式的简单实现
便于理解,本示例的 UML 类图见图 2-3。
1 | /* |
Builder 模式实战
1 | /* |
总结
- Builder 模式,通过作为配置类的构建器将配置的构建和表示分离开来,同时也将配置从目标类中隔离出来,避免过多的 Setter 方法暴露在目标类当中。
- Builder 模式的优缺点
- 优点
1) 良好的封装性,不必知道产品内部组成的细节;
2) 建造者独立,易于扩展。 - 缺点
1) 产生多余 Builder 对象及 Director 对象,消耗内存。
- 优点
原型模式
原型模式的定义
- 创建型的模式。
- 定义:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
- 原型拥有样板实例,可克隆内部属性一致的对象。
- 原型模式多用于创建复杂的或构建耗时的实例,即复制一个已经存在的实例可使程序运行更高效。
原型模式的使用场景
- 类初始化需消耗非常多的资源 ( 数据、硬件资源等 )。
- 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限。
- 一个对象需提供其他对象访问,且各调用者可能都需修改其值时,可考虑用原型模式或拷贝多个对象以供调用者使用,即
保护性拷贝
。
原型模式的 UML 类图
原型模式的简单实现
便于理解,本示例的 UML 类图见图 2-5。
1 | /* |
注:通过 clone() 拷贝对象时并不会执行构造函数。如果在构造函数中需要一些特殊的初始化操作类型,在使用 Cloneable 实现拷贝时,注意构造函数不会执行的问题。
浅拷贝和深拷贝
浅拷贝:上述例子实际上只是一个浅拷贝,也称
影子拷贝
,即只是副本文档引用原始文档的字段。图 2-6 浅拷贝示意图 1
2
3
4
5
6secDoc.setText("This is a Paper.");
secDoc.addImage("Image B");
secDoc.showDocument(); // Case 1
originDoc.showDocument(); // Case 2
// Case 1,2 都增加了图片 “Image B”,原因是 secDoc 只是单纯指向了 this.mImages深拷贝:为了解决浅拷贝所带来的“问题” ( 视具体问题而定 ),引入深入拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
protected WordDocument clone() {
try {
WordDocument doc = (WordDocument) super.clone();
doc.mText = this.mText;
// doc.mImages 指向 mImages 的一份拷贝,而不是 this.mImages 本身
doc.mImages = (ArrayList<String>) this.mImages.clone();
return doc;
} catch(Exception e) {
}
return null;
}
原型模式实战
1 | /* |
用户信息的更新,限定于与 LoginSession 类在同一包下才能执行,即 Partion A 的操作,使这样的限定失效。我们可作以下改进:
- 在 User 类中实现 Cloneable 接口。
- 在 LoginSession 中将 getLoginedUser() 改为
return loginedUser.clone()
,即在任何地方调用,获得的都是用户拷贝的对象,修改只是作用于拷贝的对象。
1 | // 实现 Cloneable 接口 |
总结
- 原型模式的优缺点
- 优点 - 原型模式是在内存中二进制的拷贝,比 new 一个对象性能更优。
- 缺点 - 内存中拷贝,构造函数是不会执行的。
工厂方法模式
工厂方法模式的定义
- 创建型设计模式。
- 定义一个用于创建对象的接口,让子类决定实例化哪个类。
工厂方法模式的使用场景
- 复杂对象的创建,而用 New 就可以完成创建的对象则不必使用工厂方法了。
工厂方法模式的 UML 类图
工厂方法模式的 UML 如图 2-7 所示。
1 | public abstract class Product { |
另外,我们可以利用反射的方式实现多工厂方法模式,具体见下述代码。
1 | public abstract class Factory { |
工厂方法模式的简单实现
某汽车厂主要就是组装某款 SUV 车型,比如 Q3、Q5、Q7,对于这类车型来说,内部结构差异并不是很大,因此一条生产线足以应付 3 种车型,对于该类生产线可提供一抽象类定义。
便于理解,本示例的 UML 类图见图 2-8。
1 | public abstract class AudiFactory { |
工厂方法模式的实战
Android 数据持久化有很多方式,如 SharedPreferences (XML)、SQLite (关系数据库)。对数据操作的方法无非就是增、删、改、查,若我们将每种数据储存的方式作为一个产品类,在抽象产品类中定义对数据操作的方法,即我们宏观层面把握操作的逻辑,具体的实现逻辑由储存数据的方式决定。
1 | public abstract class IOHandler { |
抽象工厂模式
抽象工厂模式的定义
- 创建型设计模式。
- 为创建一组相关或者是相互依赖的对象 提供一个
接口
,而不需要指定它们的具体类。
抽象工厂模式的使用场景
一个对象族有相同约束时可以使用抽象工厂模式。如:
Android、iOS、Window Phone 下都有短信软件和拨号软件,两者属于软件范畴,但由于操作系统平台不一样,其代码实现细节也是有差异的,则我们可考虑使用抽象工厂方法模式去产生不同平台下的同款软件。
抽象工厂模式的 UML 类图
抽象工厂方法模式的 UML 如图 2-9 所示。
1 | public abstract class AbstractProductA { // 抽象产品类 A |
抽象工厂模式的简单实现
在简单工厂模式的简单实现中,我门以车厂生产汽车为例。虽 Q3、Q5、Q7 同为一车系,但三者之间的零部件产别却很大,如 Q3、Q7 当中,Q3 装配的是国产发动机,普通轮胎和普通制动系统;Q7 则装配的是进口发动机,全尺寸越野轮胎和制动性能极好的制动系统。
即同为一系列车,大家共有部件有发动机、轮胎和制动系统等,由于具体的部件品质不同,装配的细节又不同。故我们可将抽象工厂模式应用当中,化繁为简。具体的架构如图 2-10 的 UML 类图所示。
1 | public abstract class CarFactory { |
总结
- 抽象工厂方法模式的优缺点
- 优点 - 分离接口与实现,即客户端使用抽象工厂的创建对象,客户端不知具体实现是谁,客户端只是面向产品的接口编程而已,使其从具体的产品实现中解耦。
- 缺点
1) 类文件的爆炸性增加。
2) 不太容易扩展新的产品类,因为每当增加一个产品类,就需修改抽象工厂,故所有具体工厂类均会被修改。
策略模式
策略模式的介绍
实现某功能,可以有多种算法或策略选择,例如排序算法,有插入排序、归并排序、冒泡排序等。
思考:多种排序算法,可以写在一个类中,一个方法对应一种具体排序。但是缺点也是很明显,即臃肿;维护成本高,且容易引发错误;每增加一种排序需修改封装类的源码。
改进:提供一个统一接口,不同的算法或策略有不同的实现类。
策略模式的使用场景
- 针对同类问题的多种处理方式,仅仅是
具体行为
有差别。 - 需要安全地封装多种
同类型
的操作。 出现同一抽象类,有多个子类,而又需使用
if-else
或switch-case
来选择具体子类。但缺点也明显,耦合性高;代码臃肿难维护。
策略模式的 UML 类图
策略模式的 UML 如图 2-11 所示。
策略模式的简单实现
下面以在北京乘坐公共交通工具的费用计算来演示一简单示例。在 2014 年 12 月 20 号之后,北京提高公交价格,不在是单一票价制,而是分段计费。显然,公交车和地铁的价格计算方式是不一样的。但是,我们的示例中是需要计算乘不同出行工具的成本,故我们采用策略模式进行设计、编码。
便于理解,本示例的 UML 类图如图 2-12 所示。
1 | public interface CalculateStragety { |
策略模式的实战应用
对于默认情况下,ImageLoader 会按照先后顺序加载图片,但在实际算法当中,相反顺序加载图片也是有可能的,即反序列加载图片。当然加载方式可看作多种策略,共同的目标是实现加载图片。图 2-13 是 ImageLoader 的 UML 类图。
1 | public interface LoadPolicy { // 加载策略接口 |
总结
- 策略模式的优缺点
- 优点
1) 很好地演示了开闭原则,也就定义了抽象。
2) 耦合度相对较低,扩展方法。 - 缺点 - 随着策略的增加,子类会变得繁多。
- 优点
状态模式
- 状态模式和策略模式和结构几乎一样,但它们的目的本质完全相异。
- 状态模式:行为是平行的,不可替换的。
- 策略模式:行为彼此独立,可相互替换。
状态模式的定义
当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
状态模式的使用场景
代码中包含大量与对象状态有关的条件语句。如操作中含有庞大的多分支语句 ( if-else
或 switch-case
),且这些分支依赖与该对象的状态。
若使用状态模式来优化架构,即每一条件分支放于独立的类。
状态模式的 UML 类图
状态模式的简单示例
下面以电视遥控器为例演示状态模式的实现。便于理解,本示例的 UML 类图如图 2-15 所示。
状态模式实战
在新浪微博中,用户在未登录的情况下点击转发按钮,此时会先让用户登录,然后再执行转发操作;如果已登录的情况下,那么用户输入转发的内容后就可以直接进行操作。
总结
- 状态模式的优缺点
- 优点 - 将所有与一个特定的状态相关的行为都放入一个状态对象中,它提供了一个更好的方法来组织与特定状态相关的代码,将繁琐的状态判断转为结构清晰的状态类族。
- 缺点 - 必然增加系统类和对象的个数。
责任链模式
责任链模式的定义
- 行为型设计模式。
- 通俗定义:每个节点看作一对象,每一对象拥有不同的处理逻辑,将一请求从链式的首端发出,沿着链的路径一次传递每个节点对象,直至有对象处理这个请求为止。
- 标准定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成链,并沿着这条链传递该请求,直至有对象处理它为止。
责任链模式的使用场景
- 多个对象可以处理同一请求,但具体由哪个对象处理则在运行时动态决定。
- 在请求处理者不明确的情况下向多个对象中的其一提交一个请求。
- 需要动态指定一组对象处理请求。
责任链模式的 UML 类图
责任链模式的 UML 类图如图 2-17 所示。
1 | // 抽象处理者 |
责任链模式的简单实现
在公司中报销费用中,审批的流程其实就是一个类似责任链的实例。例如,小明是请求的发起者,而处理者有组长、部门主管、经理和老板,对于不同额度的报销费用需要不同级的处理者审批,准确地说,每一类人代表这条链上的一个节点。
例如小民是请求的发起者,而老板则是处于链条顶端的类,小民从链的底端开始发出一个申请报账的请求,首先由组长处理该请求,组长比对后发现自己权限不够于是将该请求转发给位于链中下一个节点的主管,主管比对后发现自己权限不足又将该请求转发给经理,经理也基于同样的原因将请求转发给老板,这样层层转达直至请求被处理。即至始至终小民关心的是报账结果,而不用在乎处理者是谁。责任链模式在这里很好地将请求的发起者与处理者解耦。
便于理解,本示例的 UML 类图如图 2-18 所示。
1 | // 抽象领导者 |
责任链模式实战
Android 中我们可以借鉴责任链模式的思想来优化 BroadcastReceiver 使之成为一个全局的责任链处理者。
我们知道 Broadcast 可以被分为两种:
- Normal Broadcast:普通广播,异步广播,发出时可被所有的接收者收到。
- Ordered Broadcast:有序广播,依优先级依次传播的,直到有接收者将其终止或所有接收者都不终止它。
有序广播这一特性与我们的责任链模式很相近,通过它可实现一种全局的责任链事件处理。
1 | // 具体的实现思路是,通过 Intent 的限制值来限定最终的广播权归谁所有 |
总结
- 责任链模式的优缺点
- 优点 - 对请求者和处理者关系解耦,提高代码灵活性。
- 缺点 - 递归调用。特别是处理者太多,那么遍历定会影响性能。
解释器模式
解释器模式的定义
- 行为型设计模式。
- 概念:给定一个语言,定义它的
文法
的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。 - 文法:例如我们熟悉的「主谓宾结构」,通过下述短语举例,我们可把短语抽象看作:I am a/an [noun.]
主语 | 谓语 | 宾语 |
---|---|---|
I | am | a designer |
I | am | a teacher |
再举例:假设有如以 ab 开头 ef 结尾,中间排列 N(N>=0) 个 cd 的字符串。
abcdcd…cdef
在计算机科学中,我们将上述字符串中的 “a”、“b”、“c”、“d”、“e” 和 “f” 这 6 个字符称为一种形式语言的
字符表
。而这些字符组成的集合,如 “abcdcd…cdef” 这样由字符表构成的字符串则称为
形式语言
。注意这里的语言不是文法。假设定义一个符号 S,从符号 S 出发推导上述字符串,即可得到如下推导式:
1
2S ::= abA*ef
A ::= cd
::==
称为推导;*
表示闭包,上述推导式中意思是,符号 A 可以有 0 或 N 个重复;- 非终结符号:
S
和A
则称非终结符号,即它们能推导出式子右边的表达式; - 终结符号:”pqmn”,“ab”,“ef”,即无法再推导;
解释器模式的使用场景
某个
简单语言
需要解释执行且可将该语言中的语句表示为抽象语法树
时可考虑使用解释器模式。如:有非终结符号 p+q+m-n,即该数学表示式可表示为一棵抽象语法树。如图 2-19 所示。
某些特定的领域出现不断重复的问题时,可将该领域的问题转化为一种语法规则下的语句,然后构建解释器来解释该语句。
英文字母的大小写转换;阿拉伯数字转为中文的数字…
即它们都是一个个终结符,不同的只是具体内容。
解释器模式的 UML 类图
解释器模式的 UML 类图如图 2-20 所示。
1 | public abstract class AbstractExpression { |
总结
- 解释模式的优缺点
- 优点 - 灵活的扩展性,即我们想对文法规则进行扩展延伸时,只需增加相应的非终结符解释器,并在构建抽象语法树时,使用到新增的解释器对象进行具体的解释即可。
- 缺点
1) 对于每一条文法对应至少一个解释器,其会生成大量的类,导致后期维护困难;
2) 构建其抽象语法树会显得异常繁琐,甚至可能出现需要构建多棵抽象语法树的情况。
命令模式
命令模式的定义
- 行为型设计模式。
- 介绍:将一系列的方法调用封装,用户只需调用一个方法执行,那么所有这些被封装的方法就会被挨个执行调用。
- 定义:
- 将一个请求封装成一个对象,从而让用户使用不同的请求把客户端参数化。
- 对请求排队或者记录请求日志,以及支持可撤销的操作。
命令模式的使用场景
- 需要抽象出待执行的动作,然后以参数的形式提供处理 (类似过程设计中的回调机制)。
- 在不同的时刻指定、排列和执行请求。一个命令对象可以有与初始请求无关的生存期。
- 需要支持取消操作。
- 需要支持事务操作。
- 支持修改日志功能,若系统崩溃,这些修改可重做一遍。
命令模式的 UML 类图
命令模式的 UML 类图如图 2-21 所示。
Receiver:接收者角色
该类负责具体实施或执行一个请求,通俗地说,执行具体逻辑的角色。
Command:命令角色
定义所有具体命令类的抽象接口。
ConcreteCommand:具体命令角色
该类实现了 Command 接口,在 execute() 方法中调用接收者角色的相关方法,在接收者和命令执行的具体行为之间加以弱耦和。
Invoker:请求者角色
该类的职责就是调用命令对象执行具体的请求,相关的方法我们称为行动方法。
这里其实大家可以看到,命令模式的应用其实可用一句话概述,就是将行为调用者与实现者解耦。
1 | // 接收者类 |
命令模式的简单实现
这里以古老的俄罗斯方块游戏为例,在命令模式下如何操控俄罗斯方块变换。游戏中含有 4 个按钮,即上下左右。设定玩游戏的人相当于我们的客户端,游戏上的 4 个按钮相当于请求者,而执行具体按钮命令的逻辑方法可看作命令角色。
便于理解,本示例的 UML 类图如图 2-22 所示。
1 | // 接收者角色 |
对于大部分开发者来说,更愿意接受的形式:
1 | TetrisMachine machine = new TetrisMachine(); |
调用逻辑做得如此复杂,其实是为了开发起来方便,即每次我们增加或修改游戏功能只需修改 TetrisMachine 类即可。
当然,其实这样做是有原因的,即设计模式种有一条重要的原则:对修改关闭对扩展开放。具体好处是:
- 如修改功能、代码的具体逻辑,以上例为例,即修改 TetrisMachine 类即可。
- 此外,命令模式还可以实现命令记录的功能,如在 Buttons 里使用数据结构存储执行过的命令对象,需要时可恢复。
总结
- 命令模式的优缺点
- 优点 - 更灵活的控制性以及更好的扩展性;更弱的耦合性。
- 缺点 - 类的膨胀,大量衍生类的创建。
观察者模式
观察者模式的定义
定义对象间一种 一对多
的 依赖关系
,使得每当一个对象改变状态,则所有依赖与它的对象都会得到通知并被自动更新。
观察者模式的使用场景
- 关联行为场景,即关联行为是可拆分的,而不是“组合”关系。
- 事件多级触发场景。
- 跨系统的消息交换场景,如消息队列、事件总线的处理机制。
观察者模式的 UML 类图
观察者模式的 UML 类图如图 2-23 所示。
观察者模式实战
总结
- 观察者模式主要的作用就是对象解耦,将观察者与被观察者完全隔离,只依赖于 Observer 和 Obserable 抽象。
如:ListView 就是运用了 Adapter 和观察者模式,使之它的扩展性、灵活性增强,且耦合度却很低。
- 观察者模式的优缺点
- 优点
1) 增强系统灵活性、可扩展性;
2) 将观察者与被观察者之间是抽象耦合,应对业务变换。 - 缺点 - 应用观察者模式,需考虑开放效率和运行效率问题 (一般考虑采用异步的方式)
- 优点
备忘录模式
备忘录模式的介绍
- 行为型设计模式。
- 用于保存对象当前状态,并在之后可再次恢复到此状态。
- 保证被保存的对象状态不能被对象从外部访问,目的是为了保护好被保存对象状态的完整性及内部实现不向外暴露。
备忘录模式的定义
在不破坏封闭的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原生保存的状态。
备忘录模式的 UML 类图
备忘录模式的 UML 类图如图 2-24 所示。
Originator
负责创建一个备忘录,可以记录、恢复自身的内部状态。同时 Originator 还可以根据需要决定 Memoto 存储自身的哪些内部状态。
Memoto
备忘录角色,用于储存 Originator 的内部状态,并且可以防止 Originator 以外的对象访问 Memoto。
Caretaker
负责储存备忘录,不能对备忘录的内容进行操作和访问,只能够将备忘录传递给其他对象。
备忘录模式的简单实例
对于备忘录模式来说,比较贴切的场景应该是游戏中的存档功能,该功能就是将游戏进度存储到本地文件系统或者数据库中,下次再次进入时从本地加载进度,使得玩家能够继续上一次的游戏之旅。下面我们以“使命召唤”这款游戏为例简单演示备忘录模式的实现。
首先我们建立游戏类 CallOfDuty,备忘录类 Memoto 和负责管理 Memoto 的 CareTaker 类。玩游戏到某个节点对游戏进行存档,然后退出游戏,再重新进入时从存档中读取进度,并且进入存档时的进度。
便于理解,本示例的 UML 类图如图 2-25 所示。
1 | // ”使命召唤“ 游戏 ( 简化的数据模型,仅供简单演示 ) |
总结
- 备忘录模式是在不破坏封装的条件下,通过备忘录对象 (Memoto) 存储另外一个对象内部状态的快照,在需求的时候把对象还原到存储的状态。
- 备忘录的优缺点
- 优点 - 恢复状态机制;信息封装
- 缺点 - 消耗内存
迭代器模式
迭代器模式的介绍
- 又称游标 (Cursor) 模式,行为型设计模式。
迭代器模式源于对容器的访问,若我们将遍历的方法封装在容器中,则存在问题:
- 不仅维护自身内部数据且要对外提供遍历的接口方法。
- 不能对同一个容器同时进行多个遍历操作。
不提供遍历方法,而让使用者自行实现,必暴露内部细节。
解决方案:在客户访问类与容器直接插入一个第三者
迭代器
。
迭代器模式的定义
提供一种方法顺序访问一个容器对象中的各个元素,而不需暴露该对象内部细节。
迭代器模式的 UML 类图
迭代器模式的 UML 类图如图 2-26 所示。
Iterator:迭代器接口
迭代器接口,负责定义、访问和遍历元素的接口。
ConcreteIterator:具体迭代器类
具体迭代器类,实现迭代器接口,并记录遍历的当前位置。
Aggregate:容器接口
容器接口,负责提供创建具体迭代器角色的接口。
ConcreteIterator:具体容器类
具体容器类,具体迭代器角色与该容器相关联。
1 | // 迭代器接口 |
总结
- 迭代器模式的优缺点
- 优点 - 支持以不同的方式遍容器对象,也可以有多个遍历,弱化了容器类与遍历算法之间的关系。
- 缺点 - 类文件的增加。
- 当然几乎每一种高级语言都有相应的内置迭代器实现,故本章的内容在于了解而非应用。
模板方法模式
模板方法模式的介绍
若我们知道一个算法所需的关键步骤,并确定了这些步骤的执行顺序,但某些步骤的具体实现是未知的,或实现是随着环境变化的。
例如,执行程度的流程大致为:
Step.01:检查代码正确性
Step.02:链接相关类库
Step.03:编译
Step.04:执行程序
即上述步骤不一样 ( 实现细节 ),但执行流程是固定的。
模板方法模式的定义
- 定义一个操作的
算法框架
。 - 将步骤延迟到子类,使子类不改变算法结构即可重定义该算法的某些特定步骤。
模板方法模式的使用场景
- 多个子类有公有的方法,且逻辑基本相同。
- 重要复杂的算法,可把核心算法设计为模板方法,周边细节功能则由各个子类实现。
重构时,使用模板方法,即相同代码抽取到父类中,然后通过
钩子函数
约束其行为。钩子函数:普通的抽象类多态,即它在模板方法模式中提供了改变原始逻辑的空间。
模板方法模式的 UML 类图
模板方法模式的 UML 类图如图 2-27 所示。
总结
模板方法模式:流程封装,即把某个固定的流程封装到一个 final 函数中,并让子类能够定制这个流程中的某些或者所有步骤。
要求父类提供共同代码,即提高代码复用性、可扩展性。
模板方法的优缺点
- 优点
1) 封装不变部分,扩展可变部分。
2) 提取公共部分代码。 - 缺点:代码阅读有难度?
- 优点
访问者模式
访问者模式的介绍
- 将
数据操作
与数据结构
分离的设计模式。 - 软件系统拥有由许多对象构成的对象结构,这些对象类拥有一 accept() 方法接受访问者对象访问。
访问者是一接口,拥有一 visit() 方法对访问到的对象结构中不同类型的元素作出不同的处理。
- 在对象结构的一次访问中,遍历整个对象结构,对每个元素实施 accept() 方法。
- 每一元素的 accept() 方法会调用访问者的 visit() 方法,即访问者可针对对象结构设计不同的访问类来完成不同操作。
访问者模式的定义
封装用于某种数据结构中各元素操作,且在不改数据结构的前提下定义这些元素的新操作。
访问者模式的使用场景
- 对一对象结构中的对象进行不同且不相关的操作。
- 需避免操作“污染”对象的类。
- 增加新操作是不修改这些类。
- 对象结构稳定,但经常需在对象结构上定义新操作。
访问者模式的 UML 类图
访问者模式的 UML 类图如图 2-28 所示。
访问者模式的简单示例
公司给员工进行业绩考核,评定由公司高层负责。但不同领域的管理人员对与员工的评定标准不一样。即我们把员工分为工程师和经理,评定员工分为 CTO 和 CEO。
假定 CTO 关注工程师的代码量,经理的新产品数量;CEO 关注工程师的 KPI,经理的 KPI 及新产品数量。
便于理解,本示例的 UML 类图如图 2-29 所示。
1 | // 员工基类 |
总结
- 对象结构足够稳定,需在对象结构上经常定义新操作,且需对对象结构中的对象进行很多不同且不相关的操作,考虑访问者模式。
- 访问者模式的优缺点
- 优点
1) 单一职责原则,即各角色职责分离。
2) 数据结构和作用于该结构上的操作解耦。 - 缺点
1) 具体元素对访问者公布细节。
2) 具体元素变更导致修改成本大。
3) 违反依赖倒置原则,即为了“区别对待”而依赖了具体类,没有依赖抽象,如上例中的 Engineer 与 Manager。
- 优点
中介者模式
中介者模式的介绍
- 又称为调节者模式或调停者模式,行为型设计模式。
中介者模式的定义
- 包装一系列对象相互作用的方式,使这些对象不必相互明显作用。
- 将多对多的相互作用转化为一对多的相互作用。
- 将对象的行为和协作抽象化。
中介者模式的使用场景
- 对象间交互操作较多且每个对象的行为操作都依赖彼此时,为防止修改其中一对象的行为同时涉及修改很多其他对象的行为。
- 该模式将对象之间的多对多关系变成一对多关系。
- 中介者对象将系统从网状结构变成以调停者为中心的星形结构,以降低系统复杂性,提高可扩展性作用。
中介者模式的 UML 类图
中介者模式的 UML 类图如图 2-30 所示。
1 | // 抽象中介者 |
中介者模式的简单实现
便于理解,本示例的 UML 类图如图 2-31 所示。
中介者模式就是用来协调多个对象之间的交互,就像上例中的主板,没有主板这个中介者,那么电脑里的每一个零部件都要与其他零部件建立关联。
比如 CPU 要与内存交互,与显卡交互以及与 IO 设备交互,那么这样一来就会构成一个错综复杂的网状图,而中介者模式即将网状图变成一个结构清晰的星形图。
中介者模式实战
协调多个交互的对象,Android 中这么多形形色色控件也算是交互对象。其中社交、网商等应用的用户登录模块,账号框、密码框、登录按钮之间的相互制约、联系,正是中介者模式的表现,具体的实例样式可自行尝试。
总结
- 中介者模式的优缺点
- 优点 - 将网状般的依赖关系转化为以中介者为中心的星形结构,即使用中介者模式可对这种依赖关系进行解耦使逻辑结构清晰。
- 缺点 - 若几个类间的依赖关系并不复杂,使用中介者模式反而会使原本不复杂的逻辑结构变得复杂。
代理模式
代理模式的定义
- 结构型设计模式。
- 为其他对象提供一种代理以控制对这个对象的访问。
代理模式的使用场景
- 无法或不想直接访问某个对象或访问某对象存在困难。
- 为保证客户端使用的透明性,委托对象与代理对象需实现相同的接口。
代理模式的 UML 类图
代理模式的 UML 类图如图 2-32 所示。
Subject:抽象主题类
该类主要职责是声明真实主题与代理的共同接口方法,其可是抽象类或接口。
RealSubject:真实主题类
该类也被称为被委托类或者被代理类,该类定义了代理所表示的真实对象,由其执行具体的业务逻辑方法,而客户类则通过代理类间接地调用真实主题类中定义的方法。
ProxySubject:代理类
该类也称为委托类或者代理类,该类持有一个对真实主题类的引用,在其所实现的接口方法中调用真实主题类中相应的接口方法执行,以起到代理的作用。
1 | // 抽象主题类 |
代理模式的简单实现
以生活中常有的例子,老板拖欠工资甚至克扣工资的情况,而最恰当的途径就是通过法律诉讼解决问题。一旦选择走法律途径解决该纠纷,那么不可避免地需请一个律师来作为自己的诉讼代理人。
便于理解,本示例的 UML 类图如图 2-33 所示。
1 | // 诉讼接口类 |
静态代理
代码由程序员自己或者通过一些自动化工具生成固定的代码再对其进行编译,即说在我们的代码运行前代理类的 Class 编译文件就已经存在。上述例子即为静态代理的实现模式。动态代理
通过反射机制动态地生成代理者的对象,即我们在编译阶段不需要知道代理者是谁,代理谁我们将在执行阶段决定。Java 提供了便捷的动态代理接口 InvocationHandler。
同样,以动态代理方式实现上述例子,本示例的 UML 类图如图 2-34 所示。
1 | public class DynamicProxy implements IncocationHandler { |
总结
- 代理模式的优缺点
- 优点 - 代理模式可看作一种针对性优化。
- 缺点 - 暂没有明显的缺点。
组合模式
组合模式的介绍
- 又称部分整体模式,结构型设计模式。
它将一组相似的对象看作一个对象处理,并根据一个树状结构来组合对象,然后提供一个统一的方法去访问相应对象。
例如公司组织结构的树状图,如图 2-35 所示。
图 2-35 公司组织结构的树状图 在组合模式中,我们将这样的一个拥有分支的节点称之为枝干构件,位于树状结构顶部的枝干结构比较特殊,我们称为根结构件,因其为整个树状图的始端。同样对于像行政部和研发部这样没有分支的结构,我们称之为叶子结构,这样的一个结构就是组合模式的雏形。如图 2-36 所示。
图 2-36 组合模式的雏形
组合模式的定义
将对象组合成 树状结构
以表示 “部分-整体” 的 层次结构
,使得用户对单个对象和组合对象的使用具有一致性。
组合模式的使用场景
- 表示对象的
部分-整体
层次结构时。 - 从一个整体中能够独立出部分模块或功能的场景。
组合模式的 UML 类图
组合模式的 UML 类图如图 2-37 所示。
上述所讲与依赖倒置原则相违背,既然是面向接口编程,则我们就该把焦点放在接口设计上,即在 Composite 的一些实现方法定义到 Component 中。
这样,我们会得到一个不一样的组合模式,也称为安全的组合模式,该安全组合模式的 UML 类图见图 2-38 所示。
透明组合模式不管是叶子还是枝干节点都有相同的结构,那么意味着不能单一的 getChildren() 方法得到子字节的类型 (已是叶子节点),则必须在方法实现的内部进行判断。
1 | // 透明的组合模式抽象根节点 |
组合模式的简单实现
在操作系统中,文件系统其实就是一种典型的组合模式例子。
具体地,文件系统中文件就是可被具体程序执行的对象,文件夹就是可存放文件和文件夹的对象。文件系统的组合模式表示如图 2-39 所示。
便于理解,本实例的 UML 类图如图 2-40 所示。
1 | // 表示文件或文件夹的抽象类 |
总结
- 组合模式与解释器模式有一定的类同,两者在迭代对象时都涉及递归的调用,但组合模式所提供的属性层次结构使我们能
一视同仁
对待单个对象的对象集合。 - 组合模式的优缺点
- 优点
1) 清楚定义分层次的复杂对象,表示对象的全部或部分层次,让高层模块忽略了层次差异,方便对整个层次结构进行控制。
2) 高层模块可一致地使用一个组合结构或其中单个对象。
3) 在组合模式中增加新的枝干结构和叶子构件很方便,无须对类库进行修改。
4) 通过叶子对象和枝干对象的递归组合,形成复杂的树形结构,但对其控制却非常简单。 - 缺点
1) 新增构件时,不好对枝干中的构件类型进行限制,不能依赖类型系统来施加这些约束,因为大多数情况下他们来自相同的抽象层。
2) 因此必须进行类型检查来实现。
- 优点
适配器模式
适配器模式的介绍
- ListView、GirdView、RecyclerView 都需要使用 Adapter。
- 两个没有关系的类型之间交互,一种解决方法是修改各自类接口;另一种情况是使用一个 Adapter,在两种接口间创建一个 “混血儿” 接口,将两接口兼容。
适配器模式的定义
把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法一起工作的两个类能一起工作。
适配器模式的使用场景
- 系统需要使用现存类,而此类接口不符系统需求,即接口不兼容。
- 需一个统一的输出接口,而输入端的类型不可预知。
适配器模式的 UML 类图
适配器模式也分两种,即类适配器模式和对象适配器模式。
- 类适配器模式,如图 2-41 所示。
目标接口需要的是 operation2(),而 Adaptee 对象中有一个 operation3(),因此不兼容。故通过 Adapter 实现一个 operation2() 将 Adapter 的 operation3() 转换为 Target() 需要的 operation2()。
- 对象适配器模式,如图 2-42 所示。
这种实现方式直接将要被适配的对象传递到 Adapter 中,使用组合的形式实现接口兼容的效果。即带来的好处有,在被适配的对象中不暴露方法细节;且相对类适配器,由于继承了被适配对象,在 Adapter 类中出现一些奇怪接口。因此对象适配器模式的实现更加灵活。
适配器模式的简单示例
以电源适配器为例,分别以类适配器和对象适配器模式阐述具体情况。
- 5V 电压是 Target 接口。
- 220V 电压是 Adaptee 类。
- 将电压 220V 转换到 5V 是 Adapter 类。
1 | /** |
总结
- 适配器模式的优缺点
- 优点
1) 更好的复用性:系统需使用现有的类,而此类的接口不符系统需求,则通过适配器模式可让这些功能得到更好的复用。
2) 更好的扩展性。 - 缺点 - 若可对系统重构,尽可能不使用适配器,过多使用适配器,容易让系统凌乱,不易整体把握。
- 优点
装饰模式
装饰模式的介绍
- 又称为包装模式,结构性设计模式。
- 使用一种对客户端透明的方式来动态地扩展对象的功能,同时它也是继承关系的一种替代方案。
装饰模式的定义
动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式相比生成子类更为灵活。
装饰模式的使用场景
需要透明地、动态地扩展类的功能时,装饰模式不失一种理想方案。
装饰模式的 UML 类图
装饰模式的 UML 类图如图 2-43 所示。
Component:抽象组件
可以是一个接口或者抽象类,充当被装饰的原始对象。
ConcreteComponent:组件具体实现类
该类是 Component 类的基本实现,也是我们装饰的具体对象。
Decorator:抽象装饰者
其承担的职责是为了装饰我们的组件对象,其内部一定要有一个指向组件对象的引用。
ConcreteDecorator:抽象装饰者
对抽象装饰者做出具体实现。
1 | // 抽象组件类 |
装饰模式实战
其实装饰模式并不复杂,也不陌生,它就是一种 类间的封装
,例如我们常在 Activity 的 onCreate() 方法中做一些相关的初始化操作。
1 | public class DecoratorActivity extends Activity { |
总结
装饰模式与代理模式的区别 (容易混淆)
- 装饰模式:以对客户端透明的方式
扩展对象的功能
,即继承关系的一个替代方案。 代理模式:给一个对象提供一个代理对象,并由
代理对象
来控制对原有对象引用。代理模式,即对代理的对象施加控制。
- 装饰模式:以对客户端透明的方式
享元模式
享元模式的介绍
- 又称 FlyWeight,代表轻量级的意思,结构型设计模式。
对象池
的一种实现。享元模式用来是尽可能减少内存使用量,它适用于可能存在大量重复对象的场景。
目的:缓存可共享的对象,达到对象共享,避免过多创建对象,即提升性能、避免内存移除。
- 享元对象
- 内存状态:可共享,不随环境变化
- 外部状态:不可共享,随环境变化
- 对象容器:在经典的享元模式中,对象容器为一 Map,它的
键
是享元对象的内部状态
,它的值
是享元对象本身
。
- 客户端通过这个内部状态从享元工厂中获取享元对象,若有缓存则使用缓存对象,否则创建一个享元对象并存入容器中。
享元模式的定义
使用共享对象可有效地支持大量的细粒度的对象。
享元模式的使用场景
- 系统中存在大量的
相似对象
。 - 细粒度的对象都具备较接近的外部状态,且内部状态与环境无关,即对象没有特定身份。
- 需要
缓冲池
的场景。
享元模式的 UML 类图
享元模式的 UML 类图如图 2-44 所示。
享元模式的简单示例
例1. 过年回家买火车票,无数人在客户端上订票 (有多次购票、刷票的情况),即不断向服务端发送请求。
而每次查询,服务器必须做出回应,具体地,用户查询输入出发地和目的地,查询结构返回值只有一趟列车的车票。而数以万计的人有同样需求,即不间断请求数据,每次重新创建一个查询的车票结果,即造成大量重复对象创建、销毁,使得服务器压力加重。
享元模式正好适合解决该情形的问题,例如 A 到 B 地的车辆是有限的,车上铺位分硬卧、软卧和坐票三种,将这些可公用的对象缓存起来。用户查询时优先使用缓存,反之则重新创建。
便于理解,本示例的 UML 类图如图 2-45 所示。
1 | public interface Ticket { |
例2. 我们知道 Java 中 String 是存在于常量池中,即一个 String 被定义之后它就被缓存到了常量池中,当其他地方使用同样的字符串,则直接使用缓存,而非创建。
1 | public void testString() { |
总结
- 享元模式的优缺点
- 优点 - 大幅度地降低内存中对象的数量。
- 缺点
1) 为了使对象可共享,需将一些状态外部化,使程序的逻辑复杂化。
2) 将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。
外观模式
外观模式的介绍
- 又称门面模式 (Facade模式),结构型设计模式。
- 通过一个外观类使得整个系统中接口只有一个
统一的高层接口
,即这样降低用户使用成本,也对用户屏蔽了很多实现细节。 - 外观模式是
封装API
的常用手段。
外观模式的定义
- 要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。
- 外观模式提供一个高层次接口,使得子系统更易于使用。
外观模式的使用场景
为一个复杂的子系统提供一个简单接口。
对于系统进行定制、修改,这种易变性,使得隐藏子系统的具体实现变得尤为重要,对外隐藏子系统的具体实现,隔离变化。
- 构建一层次结构的子系统,子系统间相互依赖,则通过 Facade 接口进行通信,从而简化他们的依赖关系。
外观模式的 UML 类图
外观模式的 UML 类图如图 2-46 所示。
总结
- 外观模式的精髓在于
封装
。通过一高层次结构为用户提供统一的 API 入口,使得用户通过一个类就基本能够操作整个系统。 - 外观模式的优缺点
- 优点
1) 对客户端隐藏子系统细节,因而减少客户对于子系统的耦合。
2) 外观类对子系统的接口封装,使得系统更易于使用。 - 缺点
1) 外观类没有遵循开闭原则,当业务出现变更时,可能需要直接修改外观类。
- 优点
桥接模式
桥接模式的介绍
- 又称桥梁模式,结构型设计模式。
- 承接者连接
两边
的作用,两边指抽象部分和实现部分。
桥接模式的定义
将 抽象部分
和 实现部分
分离,使它们都可以独立地进行变化。
桥接模式的使用场景
- 对于不希望使用继承或因多层次继承导致系统类的个数急剧增加的系统,考虑使用桥接模式。
- 需要在构件的抽象化角色和具体角色之间增加更多灵活性,避免两层次间建立静态的继承关系,可通过桥接模式使它们在抽象层建立一个关联关系。
- 一个类存在两个独立变化的维度,且这两个维度都需进行扩展。
- 任何多维度变化类或多个树状类之间的耦合可通过桥接模式解耦。
桥接模式的 UML 类图
桥接模式的 UML 类图如图 2-47 所示。
Abstraction:抽象部分
该类保持一个对实现部分对象的引用,抽象部分中的方法需要调用实现部分的对象来实现。该类一般为抽象类。
RefinedAbstraction:优化的抽象部分
抽象部分的具体实现,该类一般是对抽象部分的方法进行完善和扩展。
Implementor:实现部分
可以为接口或抽象类,其方法不一定要与抽象部分中的一致,一般情况下是由实现部分提供基本的操作,而抽象部分定义的则是基于实现部分这些基本操作的业务方法。
ConcreteImplementorA/B:实现部分的具体实现
完成实现部分中定义的具体逻辑。
1 | // 实现部分的抽象接口 |
桥接模式实战
View 的视图层级与执行真正的硬件绘制相关类之间的关系可看作是一种桥接模式。即模仿这种行为,我们可自定义控件以桥接的方式提供多种不同的实现机制。
以进度条为例,我们可继承 View 类来实现进度条控件,自定义水平、垂直和圆形等不同形式的进度条。
便于理解,本示例的 UML 类图如图 2-48 所示。
总结
桥接模式,分离抽象与实现,其优点毋庸置疑,即灵活的扩展以及对客户来说透明的是实现。但不足之处在于运用桥接模式进行设计,是有一定难度的,需多加推敲与研究。
叁 MVC 与 MVP 模式
MVC
模型 - 视图 - 控制器 (Model - View - Controller,MVC),是一种
框架模式
,而非设计模式。GOF 把 MVC 看作3种设计模式,即观察者模式、策略模式与组合模式的合体,也就是一个基于
发布/订阅者模型
的框架。软件开发领域的3种级别重用:
- 内部重用:同一应用中能公共使用的抽象块。
- 代码重用:将通用模块组合成库或工具集,以便在多个应用和领域都能使用。
应用框架重用:为专用领域提供通用的或者现成的基础结构,以获得最高级别的重用性。
平时开发过程中,常见的框架模式除了 MVC 外,还有 MVVC、MTV、CBD、ORM 和 MVP。
- MVC 模式的优缺点
- 优点 - 表现层与业务层分离实现,各司其职。若在实际项目中,即前端工程师专注界面的研发,后端工程师致力于业务逻辑。
- 缺点:
1) Model 和 View 严格分离,在调试应用程序时较困难。
2) 小规模项目,采用 MVC 模式反而显得工序更加繁琐。
MVP
数据的存取 - 用户界面 - 交互中间人 (Model - View - Presenter,MVP),分离显示层和逻辑层,它们之间通过接口进行通信,降低耦合。
MVP 模式的三个角色
Presenter:交互中间人
View 和 Model 沟通的桥梁。它从 Model 层检索数据后,返回给 View 层,使得 View 和 Model 之间没有耦合,也将业务逻辑从 View 角色上抽离。View:用户界面
在 Android 中,通常指 Activity、Fragment 或某个 View 控件,它含有一个 Presenter 成员变量,且 View 需实现以逻辑接口。将 View 上操作通过转交给 Presenter 实现,最后 Presenter 调用 View 逻辑接口将结果返回给 View。Model:数据的存取
主要提供数据的存取功能。Presenter 需通过 Model 层存储、获取数据。
与 MVC 模式的区别
MVC 特点
1) 用户可向 View 发送指令,再由 View 直接要求 Model 改变状态。
2) 用户可向 Controller 发送指令,再由 Controller 发送给 View。
3) Controller 起到时间路由的作用,同时业务逻辑都部署在 Controller 中。View 可直接访问 Model,即 MVC 模式的耦合性还是相对较高的。