本系列文章共分为六篇:
设计模式(一)设计模式的分类与区别
设计模式(二)创建型模式介绍及实例
设计模式(三)结构型模式介绍及实例
设计模式(四)行为型模式介绍及实例(上)
设计模式(五)行为型模式介绍及实例(下)
设计模式(六)设计模式的常见应用
设计模式,实质上是一套被反复使用的代码设计经验,它提供了在软件设计过程中重复性问题的解决方案。其目的是为了提高代码的可重用性、代码的可读性和代码的可靠性。
设计模式的本质是建立在对类的封装性、继承性和多态性以及类的关联关系和组合关系等充分理解的基础上,对面向对象设计原则的实际运用。
【对扩展开放,对修改关闭】
在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码
。
可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。
抽象的设计方式灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。同时软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需根据需求重新派生一个实现类来扩展功能即可。
【任何父类可以出现的地方,子类一定可以出现】。
该原则是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用。
实现开闭原则的关键步骤就是抽象化,而父类与子类的继承关系就是抽象化的具体实现。因此,里氏代换原则是对开闭原则的补充。
该原则简单来说,就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法
。其具体体现为:
如果程序违背了里氏替换原则,就需要取消原来的继承关系,重新设计它们之间的关系。
【程序要依赖于抽象接口,不要依赖于具体实现】。
该原则其定义中包含了三层含义:
1>
高层模块不应该依赖低层模块,两者都应该依赖其抽象
。
2>抽象不应该依赖细节。
3>细节应该依赖抽象。
依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
该原则的核心思想是:要面向接口(接口或者抽象类)编程,不要面向实现(具体的实现类)编程
。在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多(这里的抽象指的是接口或者抽象类,而细节是指具体的实现类)。
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,在实际开发中,该原则的具体体现为:
【一个类应该有且仅有一个引起它变化的原因,否则类就应该被拆分】。
如果一个类承担了太多的职责,至少存在以下两个缺点:
1) 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力。
2)当客户端需要使用该对象的某一个职责时,不得不同时实现其他职责,从而造成冗余代码或代码的浪费。
单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性
。如果遵循单一职责原则将有以下优点:
如果一个类承担的责任过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。
软件设计要做的很多内容,就是发现职责并把那些职责相互分离。至于判断的标准,就是:如果能想到多余一个的动机去改变一个类,那么这个类就具有多于一个的职责
。
单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。
【一个类对另一个类的依赖应该建立在最小的接口上】
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的,区别如下:
在具体应用接口隔离原则时,需要从以下几个方面考虑:
一个接口只服务于一个子模块或业务逻辑
。隔离:建立单一接口,不要建立臃肿庞大的接口;即接口要尽量细化,同时接口中的方法要尽量少。
接口隔离原则与单一职责原则的不同:接口隔离原则与单一职责的审视角度是不相同的,单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少。
【只与你的直接朋友(前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等)交谈,不跟“陌生人”说话】。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。该原则的目的是降低类之间的耦合度,提高模块的相对独立性
。
从迪米特法则的定义可知,它强调以下两点:
所以,在运用迪米特法则时要注意以下 6 点:
【尽量先使用组合、聚合等关联关系来实现,其次才考虑使用继承关系来实现】。
如果要使用继承关系,则必须严格遵循里氏替换原则。
采用组合或聚合的方式复用类时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
常规的分类方式是根据其作用来划分,总共有三类:创建型模式、结构型模式和行为型模式。
在理解设计模式时,可以先重点关注其特点,再探究其适用场景
。
该模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用new运算符直接实例化对象,它的主要特点是将对象的创建与使用分离
。
该类别包括5种具体的模式:
模式 | 功能 |
---|---|
单例模式 | 某个类只能生成一个实例 ,该类提供了一个全局访问点供外部获取该实例 |
工厂方法模式 | 定义一个用于创建(一类)产品 的接口,由子类决定生产什么产品 |
抽象工厂模式 | 提供一个创建产品族 的接口,其每个子类可以生产一系列相关的产品 |
建造者模式 | 将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。 可简单理解为 对复杂对象进行分模块创建 |
原型模式 | 将一个对象作为原型,通过对其进行复制而克隆 出多个和原型类似的新实例 |
一句话简单理解5种创建型模式:
- 单例模式:某个类能自行生成全局唯一实例。
- 工厂方法模式:由实现工厂接口的具体子类工厂决定生产什么产品,只能生产一类产品。
- 抽象工厂模式:工厂也抽象,产品也抽象,可以生产多类产品。
- 建造者模式:将复杂的对象分解为多个简单的对象,然后分步骤构建。
- 原型模式:利用现有对象作为“原型”,通过克隆,创建相同或相似对象。
该模式关注类和对象的组合,即如何将类或对象按某种布局组成更大的结构
。
该类别包括7种具体的模式:
模式 | 功能 |
---|---|
适配器模式 | 将一个类的接口转换成另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作,说白了就是接口转换 |
桥接模式 | 将抽象部分与实现部分分离(这个说法不太直观,可以简单理解为从抽象类和接口两个维度来组合想要的对象) ,使它们都可以独立的变化 |
组合模式 | 将对象组合成树形结构以表示"部分-整体 "的层次结构,使得用户对单个对象和组合对象的使用具有一致性 |
装饰模式 | 向现有的对象添加新的功能,同时又不改变其结构 ,即动态地给一个对象添加一些额外的职责 |
外观模式 | 隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口 ,使得这一子系统更加容易使用 |
享元模式 | 运用共享技术来有效地支持大量细粒度对象的复用 |
代理模式 | 为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象 ,从而限制、增强或修改该对象的一些特性 |
一句话简单理解7种结构型模式:
- 适配器模式:在开发者调用的接口和现有接口不一致时,增加一个"转换接口"。
- 桥接模式:用抽象类和接口定义两个可变维度,分别对其进行不同的实现,从而组成多种不同对象。
- 组合模式:定义一个接口,让树枝对象和树叶对象都实现该接口,并具有不同的实现,进而表现整体与部分的关系。
- 装饰器模式:不改变现有对象的结构,动态地增加一些功能。
- 外观模式:在复杂的系统上,提供一个对外的一致性接口(外观)。
- 享元模式:复用需要大量使用的、功能较为简单的对象。
- 代理模式:用代理对象来控制对原有对象的访问权限。
该模式用于描述类或对象之间怎样通信、协作共同完成任务,以及怎样分配职责
。
该类别包括11种具体的模式:
模式 | 功能 |
---|---|
访问者模式 | 在不改变数据结构元素的前提下,为一个数据结构中的每个元素提供多种访问方式 |
模板模式 | 定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法 的某些特定步骤 |
策略模式 | 定义一系列算法,将每个算法封装起来,使它们可以相互替换(同一个功能的不同实现) |
状态模式 | 允许一个对象在其内部状态发生改变时改变其行为能力,常见场景为:一个对象的行为取决于它的状态 |
观察者模式 | 多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象 ,从而影响其他对象的行为,常见的是:发布-订阅 |
备忘录模式 | 在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它,撤销-恢复 |
中介者模式 | 定义一个中介对象来简化原有对象之间的交互关系 ,降低系统中对象间的耦合度,使原有对象之间不必相互了解 |
迭代器模式 | 提供一种方法来顺序访问(遍历) 聚合对象中的一系列数据,而不暴露聚合对象的内部表示 |
解释器模式 | 提供如何定义语言的文法 ,以及对语言句子的解释方法,即解释器 |
命令模式 | 将一个请求封装为一个对象 ,使发出请求的责任和执行请求的责任分割开 |
责任链模式 | 将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链 ,直到请求被处理为止 |
一句话简单理解11种结构型模式:
- 模板方法模式:父类定义固定的框架和公共部分,子类实现可变部分/步骤。
- 策略模式:有多种独立的算法,供客户端替换使用。
- 状态模式:对象的行为依赖于其状态,将状态提取为状态对象,将对象之间的直接交互改成对象和状态对象之间的交互,降低对象之间的耦合度。
- 观察者模式:一个对象变化了,要通知到别的对象,以便处理这种变化。
- 备忘录模式:可以保存和撤销对象的行为。
- 中介者模式:禁止多个对象之间相互"通信",交给中介者去进行,所有对象和交互者"通信"即可。
- 迭代器模式:如何遍历对象。
- 解释器模式:解释带有特定语法的句子。
- 命令模式:将命令/请求封装为对象,在对象与对象之间传递信息,降低对象之间的耦合度。
- 责任链模式:多个请求处理者记录对下一个处理者的引用,降低请求发起者和多个处理者之间的耦合度。
- 访问者模式:定义一个对象,可以在不改变复杂对象的数据结构的前提下,访问数据结构中的各元素。
模式 | 适用场景 |
---|---|
单例模式 | 1)只要求生成一个全局对象 2)需要频繁实例化,而创建的对象又频繁被销毁 |
工厂方法模式 | 父工厂类中只有创建产品的抽象接口,将产品对象的实际创建工作推迟到具体子工厂类当中 |
抽象工厂模式 | 系统中有多个产品类,但每次只使用其中的某一类产品 |
建造者模式 | 对象可以分模块初始化 |
原型模式 | 1)不同对象之间相似度高 2)创建对象比较麻烦,但复制比较简单 |
适配器模式 | 不同模块之间接口不一致 |
桥接模式 | 不同类都有多个变化的维度,这些维度可以组合成不同的结果 |
组合模式 | 示一个对象整体与部分的层次结构 |
装饰器模式 | 不影响原有模块功能,需要动态地添加、撤销一些附属功能 |
外观模式 | 隐藏子系统的复杂操作,对外提供简单的接口 |
享元模式 | 系统中存在大量相同或相似的对象,只保存一个就行 |
代理模式 | 控制访问权限 |
模板模式 | 子类执行固定步骤下的不同具体步骤 |
策略模式 | 不同算法互相替代使用 |
状态模式 | 态在很大程度上决定了系统的运行 |
观察者模式 | 实现群发、群消息提醒等一对多功能 |
备忘录模式 | 需要保存与恢复数据的场景 |
中介者模式 | 不同对象之间关系复杂,需要调整 |
迭代器模式 | 为遍历某种结构提供统一接口 |
解释器模式 | 语言的文法较为简单,格式固定 |
命令模式 | 请求调用者与请求接收者解耦 |
责任链模式 | 有多个对象可以处理一个请求 |
访问者模式 | 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构 |
依赖于状态
的行为;封装的是算法
。代理模式控制对象访问权限;装饰器模式用于向对象添加职责
。访问者模式一般遍历复杂结构
,如树结构或组合结构等,结构中每个结点可以同构也可以异构,前提是只要提供一个预定的统一访问接口即可;迭代器模式用于遍历元素类型一致的集合
(多是线性结构,当然也可以是非线性结构),不用为每个元素定义统一的访问接口。外观模式 一般是将一个子系统进行包装,目的是简化接口
。装饰器模式是为了给现有对象增加功能
,一般接口不变或接口增加;适配器模式是为了改变其接口,功能保持不变
。对象状态
,后者针对对象行为
。工厂方法模式 | 抽象工厂模式 |
---|---|
针对的是一个产品等级结构 | 针对的是面向多个产品等级结构 |
一个抽象产品类 | 多个抽象产品类 |
可以派生出多个具体产品类 | 每个抽象产品类可以派生出多个具体产品类 |
每个具体工厂类只能创建一个具体产品类的实例 | 每个具体工厂类可以创建多个具体产品类的实例 |
原型模式和享元模式,前者是在创建多个实例时,对创建过程的性能进行调优;后者是用减少创建实例的方式,来调优系统性能。
它们的使用是分场景的。在有些场景下,我们需要重复创建多个实例,例如在循环体中赋值一个对象,此时我们就可以采用原型模式来优化对象的创建过程;而在有些场景下,我们则可以避免重复创建多个实例,在内存中共享对象就好了
要实现一个原型类,需要具备三个条件:
实现Cloneable接口
:Cloneable接口与序列化接口的作用类似,它只是告诉虚拟机可以安全地在实现了这个接口的类上使用clone方法。在JVM中,只有实现了Cloneable接口的类才可以被拷贝,否则会抛出CloneNotSupportedException异常。重写Object类中的clone方法
:在Java中,所有类的父类都是Object类,而Object类中有一个clone方法,作用是返回对象的一个拷贝。在重写的clone方法中调用super.clone()
:默认情况下,类不具备复制对象的能力,需要调用super.clone()来实现。 其实深拷贝就是基于浅拷贝来递归实现具体的每个对象。
在一些重复创建对象的场景下,我们就可以使用原型模式来提高对象的创建性能。如循环体内创建对象时,我们就可以考虑用clone的方式来实现,示例:
for(int i=0; i<list.size(); i++){
Student stu = new Student();
...
}
//可以优化为:
Student stu = new Student();
for(int i=0; i<list.size(); i++){
Student stu1 = (Student)stu.clone();
...
}
享元模式是运用共享技术有效地最大限度地复用细粒度对象的一种模式。该模式中,以对象的信息状态划分,可以分为内部数据和外部数据。内部数据是对象可以共享出来的信息,这些信息不会随着系统的运行而改变;外部数据则是在不同运行时被标记了不同的值。
享元模式在实际开发中的应用也非常广泛。例如Java的String字符串,在一些字符串常量中,会共享常量池中字符串对象,从而减少重复创建相同值对象,占用内存空间。
此时需要注意前面的符号,“+”表示public,“-”代表private,“#”代表protected。
示例:
public class Animal {
public boolean isAlive = true;
public void metabolism(String oxygen,String water) {
}
public void breed() {
}
}
public interface Fly {
void flit();
}
泛化(generalization)关系时指一个类(子类、子接口)继承另外一个类(称为父类、父接口)的功能,并可以增加它自己新功能的能力,继承是类与类或者接口与接口最常见的关系,在Java中通过关键字extends来表示。
继承关系用空心三角形+实线来表示
。
示例:
public class Person {
private String name;
private int age;
public void speak() {
}
}
public class Student extends Person{
private long studentNo;
public void study() {
}
}
public class Teacher {
private long teacherNo;
public void teaching() {
}
}
实现(realization)是指一个class实现interface接口(一个或者多个),表示类具备了某种能力,实现是类与接口中最常见的关系,在Java中通过implements关键字来表示。
实现关系用空心三角形+虚线表示
。
示例:
public interface Read {
public void readStory();
}
public class Human implements Read{
@Override
public void readStory() {
System.out.print("读英雄志");
}
}
关联关系体现的是两个类,或者类与接口之间的强依赖关系。
在Java中,关联关系是使用实例变量
来实现的。
关联关系用实线箭头表示
。
示例:
public class Monkey {
}
public class Zoo {
private Monkey monkey;
}
聚合(aggregation)是关联关系的特例,是强的关联关系,聚合是整个与个体的关系,即has-a关系,此时整体和部分是可以分离的,他们具有各自的生命周期
。
聚合关系也是使用实例变量来实现的,在java语法上区分不出关联和聚合,关联关系中类出于一个层次,而聚合则明显的在两个不同的层次。
聚合关系用空心的菱形+实线箭头来表示
。
示例:
public class Son {
}
public class Daughter {
}
public class Family {
private Son son;
private Daughter daughter;
}
组合(compostion)也是关联关系的一种特例,体现的是一种contain-a关系,比聚合更强,是一种强聚合关系。它同样体现整体与部分的关系,但此时整体与部分是不可分的,整体生命周期的结束也意味着部分生命周期的结束
。
组合关系用实心的菱形+实线箭头来表示
。
同时,组合关系的连线两端还可以有一个数字“1”和数字“2”,这被称为基数
,表明这一端的类可以有几个实例,一个鸟很显然应该有两只翅膀。如果一个类有无数个实例,则就用“n”来表示,关联关系、聚合关系也可以有基数的。
示例:
public class Brain {
}
public class Person {
private Brain brain;
}
依赖(dependency)关系也是表示类与类之间的连接,表示一个类依赖于另外一个类的定义,依赖关系时是单向的。简单理解就是类A使用到了类B,这种依赖具有偶然性、临时性,是非常弱的关系。但是类B的变化会影响到类A。
在java中,依赖表现为:局部变量,方法中的参数和对静态方法的调用
。
依赖关系用虚线箭头来表示
。
public class MobilePhone {
public void transfer() {
}
}
public class Person {
private String name;
public void call(MobilePhone mobilePhone) {
mobilePhone.transfer();
}
}