目录
设计模式的意义
编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,复用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好的
- 代码复用性
- 可读性
- 可扩展性
- 可靠性 (当我们增加新的功能后,对原来的功能没有影响)
- 使程序呈现高内聚,低耦合的特性
1. 七大原则
设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模式的基础
设计模式常用的七大原则有:
- 单一职责原则
- 接口隔离原则
- 依赖倒转(倒置)原则
- 里氏替换原则
- 开闭原则
- 迪米特法则
- 合成复用原则
1.1 单一职责原则
大白话解释:一个类就是一个最小的功能单位
描述
一个类应该只负责一项职责。
如类A负责两个不同职责:职责1,职责2。 当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为 A1,A2。
实例
假设有一个“交通工具”类,他的作用只有一个,就是“运行交通工具”,假设它只有一个run方法,打印“交通工具 xx 在地上跑”这句话。
如果我们的交通工具只是车,这个类没有问题,如果交通工具加上“飞机”、“船”,那么“交通工具 飞机 在地上跑”、“交通工具 船 在地上跑”就不符合实际。
由于交通工具有多个,因此这个类不符合“单一职责原则”。
我们可以将其改为3个类,“水上交通工具”、“空中交通工具”、“陆地交通工具”,分别对海陆空负责(单一职责)。
此外,由于这个类的功能比较单一,只有run方法,我们也可以在方法级别上实现单一职责原则,即为该类创建“水上运行”、“空中运行”、“陆地运行”方法。
注意事项与细节
- 降低类的复杂度,一个类只负责一项职责。
- 提高类的可读性,可维护性
- 降低变更引起的风险
- 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违 反单一职责原则;
如果类中方法数量足够少,可以在方法级别保持单一职责原则
1.2 接口隔离原则
大白话解释:实现接口的所有类都应当觉得接口中没有多余的方法
Interface Segregation Principle
描述
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
实例
A通过调用B,需要操作1、2、3,
C通过调用D,需要操作1、4、5,
但是B、D都实现了1、2、3、4、5,显然B、D都实现了多余的方法。
若要符合“接口隔离原则”,只需要让B、D实现必需的接口即可。
然而,实际中我们不一定能确定B是否真的不需要4、5方法,也不能确定D是否真的不需要2、3方法。
因此,不是说学好设计模式就万事大吉的。
实际还得多方面考虑。
1.3 依赖倒转(倒置)原则
大白话解释:我们都是用接口声明一个变量,而不是直接使用具体类(如声明一个ArrayList,最左边用的是List接口;声明一个HashMap,最左边用的是Map)
Dependence Inversion Principle
描述
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转(倒置)的中心思想是
面向接口编程
- 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。
以抽象为基础搭建的架构比以细节为基础的架构要稳定的多
。(在java中,抽象指的是接口或抽象类,细节就是具体的实现类) - 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
就是一句话,前期设计应当从最基本、最核心的抽象入手,构建接口。
实例
用户(User类)通过receive方法接收信息(Message类)。
如果用户接受的消息包括Email、QQ、WeChat…等多种方式,那么我们就需要在User类中写多个receive重载函数分别接收不同的消息类。
然而,谁也不知道以后还会有什么消息类,这就导致每次增加一个消息类,我们都得对User类进行修改。
如果Message不是类,而是一个接口,receive接收的是Message接口,就能解决问题。
只需要让各种不同的消息类实现Message接口,receive就能够接收他们;如果出现新的消息类,也只需要增加该消息类并实现Message接口即可,不需要对原有代码进行更改。
依赖关系传递的方式
- 声明接口传递:在普通方法参数中声明接口
- 构造方法传递:在构造方法参数中声明接口
- setter方法传递:类中有个接口成员,该成员通过setter声明
注意事项和细节
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好
- 变量的
声明类型尽量是抽象类或接口
, 这样我们的变量引用和实际对象间,就存在 一个缓冲层,利于程序扩展和优化 - 继承时遵循里氏替换原则
1.4 里氏替换原则
大白话解释:少继承、别重写父类方法
OO中的继承,产生的问题
- 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。
- 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承, 则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障
解决以上问题,考虑里氏替换原则。
描述
Liskov Substitution Principle
- 如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1 的子类型。换句话说,
所有引用基类的地方必须能透明地使用其子类的对象
。 - 在使用继承时,遵循里氏替换原则,在子类中
尽量不要重写父类的方法
- 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过
聚合
,组合
,依赖
来解决问题。.
实例
类A的fun1是减法器,类B继承了类A;但是类B不小心将fun1重写成了加法器。
假设极端情况,类A就只有fun1方法,那么类B继承类A就没有必要了,把A唯一的方法都重写了。
实际编程中常常重写父类的方法,但是整个继承体系的复用性、稳定性较差。
一般可以这么做:让原来的父类A和子类B都继承一个更通俗的基类,取消AB继承关系,AB直接采用聚合、组合、依赖的关系实现方法调用。
比如上述实例,A和B都继承一个基础类Base(为了保证一些基本方法),然后B中声明一个成员类A,此时B可以写一个方法调用A的专有方法即可。
1.5 开闭原则
大白话解释:写的系统易于扩展,不允许修改。
Open Closed Principle
描述
- 开闭原则是编程中最基础、最重要的设计原则
- 一个软件实体如类,模块和函数应该
对扩展开放,对修改关闭
。用抽象构建框架,用实现扩展细节 - 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
- 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则
1.6 迪米特法则
大白话解释:减少类之间的依赖
描述
- 一个对象应该对其他对象保持最少的了解
- 类与类关系越密切,耦合度越大
迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部
。对外除了提供的public 方法,不泄露任何信息
迪米特法则还有个更简单的定义:只与直接的朋友通信
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。
耦合的方式很多,依赖,关联,组合,聚合 等。其中,我们称出现成员变量
,方法参数
,方法返回值
中的类
为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部
。
此外,有时候我们也会引入了“陌生的朋友”而不自知。
比如较长的调用链,调用链中可能生成了多个陌生的类,这也是不被允许的。
迪米特法则的目的在于降低类之间的耦合度。
1.7 合成复用原则
大白话解释:多用组合、聚合,少用继承
描述
尽量使用合成/聚合的方式,而不是使用继承。
如果仅仅是为了让B类使用A类的方法,就让B继承A,只是徒增耦合。
我们只需要在B中聚合一个A的对象,或者将A作为B的某个方法参数。
小结
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
- 针对接口编程,而不是针对实现编程。
- 为了交互对象之间的松耦合设计而努力
2. UML类图
3. 设计模式
设计模式分为三种类型,共23种
- 创建型模式:单例模式、工厂模式、抽象工厂模式、原型模式、建造者模式。
- 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
- 行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者 模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式(责任链模式)。
大白话解释:
创建型模式:
- 单例模式:创建一个对象,使得整个系统中,这个对象有且仅有一个。
- 工厂模式:多个类的创建交给一个工厂,客户端给出所需的类的类型,工厂返回该对象
- 抽象工厂模式:给每一类产品都创建一个工厂
- 原型模式:利用一个对象本体(称为原型),克隆出另一个
- 建造者模式:一个对象的创建过程比较复杂,通过一系列建造过程完成创建
结构型模式:
- 适配器模式:将类通过一个“转换器”转换某些属性让它变得适合使用
- 桥接模式:将类的某一可变属性抽出来作为类组合进去(几何图形有三种颜色,将三种颜色抽取出来作为三个类)
- 装饰模式:把类放进去加工一下,得到具有额外功能的类
- 组合模式:具有树形结构的类群,使用组合模式让其变成一个树
- 外观模式:一大堆类的运行放在一个类中(外观),一键调用
- 享元模式:实现元素共享
- 代理模式:为了控制对象的访问,加一个代理人
行为型模式:
- 模板方法模式:把执行流程(算法)都放在一个方法中,形成一个模板
- 命令模式:将所有命令单独抽出来作为类来调用,而不是作为方法
- 访问者模式:A进入B中(B某个方法使用了A),调用A的方法访问B(A的方法中又需要传入B)
- 迭代器模式:遍历类
- 观察者模式:一对多关系时,这个一想要通知多个对象
- 中介者模式:让客户与多个类的沟通,都通过一个中介类
- 备忘录模式:记录一个类的状态
- 解释器模式:解释一串表达式
- 状态模式:一个流程存在特别多的状态,使用状态模式
- 策略模式:把类的某一类方法(策略)抽出来作为类,实现动态改变
- 职责链模式(责任链模式):拦截链,轮着一个拦一个
3.1单例模式
单例模式有八种实现方法(有一种是错误示范):
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)
- 懒汉式(线程安全,同步代码块)
- 双重检查
- 静态内部类
- 枚举
3.2抽象工厂模式
简单工厂模式
AbstractProduct:抽象类产品,比如说披萨
ConcreteProduct:具体的产品,比如说中国披萨,美国披萨,巴西披萨等
SimpleFactory:用于创建披萨类,依赖于抽象披萨
FacrotyClient:工厂的使用者,通过FactoryClient,调用SimpleFactory生成不同的披萨。
3.3原型模式
原型模式(Prototype Pattern)是用于创建重复的对象
,同时又能保证性能。
Spring的bean.xml中配置的bean,scope可以选择单例,也可以选择prototype,即原型模式创建。
如果你的bean中有初始化信息,那么通过prototype模式创建的bean,都会带上这些初始化信息
原理:实现Cloneable接口,利用Object的clone,实现克隆
浅拷贝:使用clone克隆的对象,基本类型是值传递(新的对象),引用类型只是地址传递(依旧是指向旧的对象,如果此时对该对象修改,也会影响原有对象)。
深拷贝:
对于拷贝对象中的引用类型,也实现Cloneable接口,然后对其单独处理。但是这种方式局限性太大,不推荐,直接学习另一种方式,利用反序列化
原理:序列化之后,能够保存所有“值信息”
反序列化得到的是一个新的对象。
public class PrototypePattern {
public static void main(String[] args) {
Sheep s1 = new Sheep("duoli","black");
Sheep friend = new Sheep("friend","red");
s1.setFriend(friend);
Sheep s2 = (Sheep) s1.deepClone();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1.getFriend()==s2.getFriend());
}
}
class Sheep implements Serializable {
private String name;
private String color;
// 深拷贝测试对象
private Sheep friend;
Sheep(String name,String color){
this.name=name;
this.color=color;
}
@Override
public String toString() {
return "Sheep{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", friend=" + friend +
'}';
}
Object deepClone() {
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
ByteArrayInputStream bis = null;
try {
// 序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
return (Sheep) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}finally {
try {
if (bos != null) {
bos.close();
}
if (oos != null) {
oos.close();
}
if (bis != null) {
bis.close();
}
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void setFriend(Sheep friend) {
this.friend = friend;
}
public Sheep getFriend() {
return friend;
}
}
- 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
- 不用重新初始化对象,而是
动态地获得对象运行时的状态
- 如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码
- 在实现深克隆的时候可能需要比较复杂的代码
- 缺点:需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了ocp原则
3.4建造者模式
BuilderPattern
盖房子,需要逐步完成一系列工序才能得到最终的房子。
建造者模式,就是将产品(房子)与产品构造过程(一系列工序)进行解耦。
涉及角色:
产品:Product,比如一栋房子
抽象建造者:Builder,大家一直认为施工队该有的基本操作
具体建造者:ConcreteBuilder,实际开工的工人,只提供动作,指挥者让干嘛就干嘛
指挥者:Director,工头(隔离了老板与工人直接接触,负责控制房子的生产过程)
调用过程:
(客户端)老板告诉(指挥者)工头,我要“这种”房子(指定建造者,
工人),工头对这类房子的建造方式已经记住了,然后告诉工人开始搭建,最后工头将房子交给老板。
此处老板指定建造者,而不是老板定制(指定)房子,是因为:
如果定制房子,老板就得对房子的参数非常熟悉才行
如果有新的建造者加入,就得更改客户端代码
package creationMode._4_builderPattern;
public class BuilderPattern {
public static void main(String[] args) {
// 老板跟包工头说要一个小平房
Director director = new Director(new ConcreteHouseBuilderOne());
House result1 = director.getResult();
System.out.println(result1);
// 老板跟包工头说要一栋高楼
director = new Director(new ConcreteHouseBuilderTwo());
House result2 = director.getResult();
System.out.println(result2);
}
}
// 产品:房子
class House{
private String name;
private Integer height;
House(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getHeight() {
return height;
}
public void setHeight(Integer height) {
this.height = height;
}
@Override
public String toString() {
return "House{" +
"name='" + name + '\'' +
", height=" + height +
'}';
}
}
// 抽象建造者
interface AbstractHouseBuilder{
void buildStepOne();
void buildStepTwo();
House getResule();
}
// 具体建造者:工人1号
class ConcreteHouseBuilderOne implements AbstractHouseBuilder{
private House house = new House();
@Override
public void buildStepOne() {
this.house.setName("小平房");
}
@Override
public void buildStepTwo() {
this.house.setHeight(10);
}
@Override
public House getResule() {
return house;
}
}
// 具体建造者:工人2号
class ConcreteHouseBuilderTwo implements AbstractHouseBuilder{
private House house = new House();
@Override
public void buildStepOne() {
this.house.setName("大高楼");
}
@Override
public void buildStepTwo() {
this.house.setHeight(100);
}
@Override
public House getResule() {
return house;
}
}
// 指挥者:包工头
class Director{
private AbstractHouseBuilder builderOne;
Director(AbstractHouseBuilder builderOne){
this.builderOne = builderOne;
}
House getResult(){
builderOne.buildStepOne();
builderOne.buildStepTwo();
return builderOne.getResule();
}
}
StringBuilder使用的建造者模式
- Appendable 接口定义了多个append方法(抽象方法), 即Appendable 为抽象建造者, 定义了抽象方法
- AbstractStringBuilder 实现了 Appendable 接口方法,这里的AbstractStringBuilder 已经是建造者,只是不能实例化
- StringBuilder 即充当了指挥者角色,同时充当了具体的建造者,建造方法的 实现是由 AbstractStringBuilder 完成, 而StringBuilder 继承了 AbstractStringBuilder
特点
- 客户端(使用程序)不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同 的产品对象
- 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程
- 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程, 系统扩展方便,符合 “开闭原则”
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,
如果产品之间的差异性很大,则不适合使用建造者模式
,因此其使用范围受到一定的限制。 - 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化, 导致系统变得很庞大,因此在这种情况下,要考虑是否选择建造者模式.
抽象工厂模式VS建造者模式
:抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心构建过程
,只关心什么产品由什么工厂生产即可。而建造者模式则是要求按照指定的蓝图
建造产品,它的主要目的是通过完成一系列工序而产生一个新产品
3.5适配器模式
描述
- 适配器模式(
Adapter
Pattern)将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性
,让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为包装器(Wrapper
) - 适配器模式属于结构型模式
- 主要分为三类:
类适配器模式
、对象适配器模式
、接口适配器模式
工作原理
- 适配器模式:将一个类的接口转换成另一种接口.让原本接口不兼容的类可以兼 容
- 从用户的角度看不到被适配者,是解耦的
- 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法
- 用户收到反馈结果,感觉只是和目标接口交互,如图
类适配器
Adapter类,通过继承
src类,实现 dst 类接口,完成src->dst的适配。
实例
国家只提供了220V电压,我们的手机需要5V电压,因此需要一个适配器。
适配器继承220V电压(为了拿到电压),然后实现5V的接口(相当于适配器变压标准)。
接着手机充电的时候,只需要遵循5V接口就行了。
然后用户Client左拿适配器(充电器),右拿手机,就可以充电了。
注意事项
- Java是单继承机制,所以类适配器需要继承src类这一点算是一个缺点, 因为这要求dst必须是接口,有一定局限性;
- src类的方法在Adapter中都会暴露出来,也增加了使用的成本。
- 由于其继承了src类,所以它可以根据需求重写src类的方法,使得Adapter的灵活性增强了。
对象适配器
Adapter类,通过持有
src类,实现 dst 类接口, 完成src->dst的适配。
细节
- 对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。 根据合成复用原则,使用组合替代继承, 所以它解决了类适配器必须继承src的 局限性问题,也不再要求dst必须是接口。
- 使用成本更低,更灵活。
接口适配器
- 一些书籍称为:适配器模式(Default Adapter Pattern)或缺省适配器模式。
- 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求
- 适用于一个接口不想使用其所有的方法的情况。
实例
如上图,接口提供了4个方法,如果我们直接实现接口,将不得不实现全部方法。
但是我们加一个适配器,实现接口中的全部方法(但是都是空方法,没有写具体实现),
然后A继承适配器,就可以有选择的重写自己想要的方法。
Spring源码适配器模式分析
实现逻辑:前端发起请求,DispatchServlet调用doDispatch()方法,调用controller方法。
为什么应用适配器模式:前端发起的请求有多种,可能需要HttpController进行处理,也可能是SimpleController或者AnnotationController进行处理,但是我们并不知道要用哪个;最简单的方式就是在处理的时候用if-else对请求类别进行甄别,然后调用对应controller;但是,显然这种模式违背了OCP原则。
适配器模式:
处理请求的控制器接口;
分别处理请求的三个具体控制器;
每个具体的控制器都有一个对应的适配器,这些适配器都遵循同一个适配器接口。
dispatchServlet拿到请求之后,通过适配器判断控制器类型,并调用控制器方法。
模拟运行轨迹:
前端发起Http请求==》DispatcherServlet拿到请求,调用doDispatch()
》doDispatch()拿到该请求,交给HandlerAdapter适配器进行类别判断》判断结果为Http请求,此时用HttpHandlerAdapter调用HTTPController中的方法,处理请求。
适配器模式注意事项
- 三种命名方式,是根据 src是以怎样的形式给到Adapter(在Adapter里的形式)来命名的。
- 类适配器:以类给到,在Adapter里,就是将src当做类,
继承
对象适配器:以对象给到,在Adapter里,将src作为一个对象,持有
接口适配器:以接口给到,在Adapter里,将src作为一个接口,实现
- Adapter模式最大的作用还是将原本不兼容的接口融合在一起工作。
- 实际开发中,实现起来不拘泥于上述三种经典形式
3.7桥接模式
考虑这么一个问题:有“圆形”“正方形”“三角形”三个形状,然后有“红色”“绿色”“蓝色”三种颜色,现在我们需要“红色正方形”、“绿色正方形”、“蓝色正方形”、“红色三角形”、“绿色三角形”、“蓝色三角形”、“红色圆形”、“绿色圆形”、“蓝色圆形”,需要几个类?
如果是按x色x形分别建类,那么显然需要3*3个类。
这种实现快速简单,但是却难以扩展。比如我们现在多了一个绿色,那么就得再建三个“绿色xx形”,形成类爆炸。
这就是理解桥接模式的方式——属性维度。
将每一维,都单独抽出来,然后通过组合聚合的形式放在一个桥接类中。
这个“抽象”与“实现”的区别,网上并没有找到结论。
个人觉得,没啥区别,抽象接口直接跟Client交互,并且组合实现类,因此,可以将重要的“属性”作为抽象。
或者说,抽象与实现是主从关系,实现属于抽象,比如说“颜色属于形状,因此形状是抽象,颜色是实现”。
此外,个人觉得这个二维维度可以扩展成多维。
3.8装饰模式
定义
在不改变原有对象的基础之上,将功能附加到对象上。提供了比继承更有弹性的替代方案(扩展原有对象功能)
优点
- 扩展一个类的功能或者给一个类添加附加职责
- 给一个对象动态的添加功能,或动态撤销功能。
类图特点:
- 被装饰者和装饰器都实现同一个接口(抽象类),该接口中有被装饰者的
可装饰属性
相关方法比如说用调味品装饰咖啡,那么咖啡的描述和价格就是可装饰属性,此时该顶级接口应当带上描述和价格,只有这样,才能在装饰后对这些属性进行动态修改。
3.9组合模式
常见这种形式:
- 一个用户有多个订单,一个订单有多个订单详情,一个订单详情有多个商品
- 一个学校有多个学院,一个学院有多个系,一个系有多个班,一个班有多个同学
这种模式,可以抽象为一棵树
。
学校是一个根节点,同学是叶子节点。
这就是组合模式。
- ClientApp是调用方,只跟Component发生关联
- Component是抽象节点,包含根节点和叶子节点的所有默认行为
- Composite是非叶子节点
- Leaf是叶子节点。
注意:
- Component作为抽象类时,将
叶子节点也包含的行为
作为抽象方法,让叶子也能实现;将叶子节点不包含的行为
(如移除子节点和增加子节点)写一个默认实现,抛出不支持调用的异常,让其他子节点自己实现。 - 一般情况下,尽管都是非叶子节点,但是也可能实现不一致(比如增加一个学院和增加一个系,实际逻辑不一样),因此不能只写一个非叶子节点,可能存在多个很相似的非叶子节点
其他细节:
- 简化客户端操作。客户端只需要面对一致的对象而
不用考虑整体部分或者节点叶子
的问题。 - 具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系, 客户端不用做出任何改动.
- 方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容易添加节点 或者叶子从而创建出复杂的树形结构
- 需要遍历组织机构,或者处理的对象具有树形结构时, 非常适合使用组合模式.
- 要求较高的抽象性,
如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
划重点:树形结构适合、能帮助客户端忽略节点和叶子的差异性、如果节点和叶子本身存在较大差异则不适合使用组合模式(比如学校和学生的操作肯定存在较大差异,建议将学生、班级从这个树状结构排除,让系作为叶子节点)
3.10外观模式
考虑:
家庭影院系统
- 幕布的开关
- 投影仪的开关,亮度调节,节目选择
- 躺椅的准备与收起
看电影的步骤:
准备躺椅
打开幕布
打开投影仪,调节亮度,选择节目
结束看电影的步骤:
关闭投影仪
关闭幕布
收起躺椅
如果是用户直接面对躺椅、幕布、投影仪,那他就得一步一步操作(尽管这里看起来不复杂),假设是一个子系统非常多的大系统,那么这些一步一步操作就会非常复杂。
可以这么做:
弄一个Facade类,组合
幕布、投影仪、躺椅,用户只需要输入一个电影名,这个影院系统就自动完成所有准备工作。
外观模式的注意事项和细节
- 外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
- 外观模式对客户端与子系统的耦合关系,让子系统内部的模块更易维护和扩展
- 通过合理的使用外观模式,可以帮我们更好的划分访问的层次
- 当系统需要进行
分层设计
时,可以考虑使用Facade模式 - 在
维护一个遗留
的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade类,来提供遗留系统的比较清晰简单的接口, 让新系统与Facade类交互,提高复用性 - 不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好。 要以
让系统有层次,利于维护为目的
。