设计模式学习笔记(详细) - 七大原则、UML类图、23种设计模式
设计模式七大原则
- 设计模式的目的:让程序有更好的复用性、可读性、可扩展性、可靠性,呈现出高内聚、低耦合的特性
- 七大原则:编程时应当遵守的原则,也是设计模式的基础,即设计模式设计的依据
设计原则的核心思想:找出应用中可能需要变化之处,把他们独立出来,不要和不需要变化的代码混合在一起;针对接口编程,而不是针对实现编程;为了实现交互对象之间的松耦合而努力
-- 单一职责原则
Single Responsibility,一个类只负责一项职责,方法较少时也可以一个方法只负责一项职责
注意事项:
- 降低类的复杂度,一个类只负责一项职责
- 提高可读性、可维护性
- 降低变更引起的风险
- 通常情况下,应该遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,才可以在方法级别保持单一职责原则。
-- 接口隔离原则
Interface Segregation Principle,客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上(接口里的抽象方法要高内聚),即将一个大的接口分解为多个小的接口,它的实现类只需要实现自己需要的接口。
例如Interface1有5个方法,A和C分别依赖它的实现类B和D(通过接口依赖),A依赖于B中的123方法,C依赖于D中的145方法,显然在传入实现类作为参数时,B中的45方法实现是多余的,D中的23方法实现是多余的,所以需要将接口拆分为3个接口I1,I23,I45
,B实现接口I1,I23
,D实现I1,I45
就可以了。
-- 依赖倒转原则
Dependence Inversion Principle:
- 高层模块不应该依赖于低层模块,二者都应该依赖于其抽象(比如Person依赖于Email,Email是消息的底层模块,可以使用抽象接口IReceiver)
- 抽象不应该依赖于细节,细节应该依赖于抽象
- 依赖倒转的思想是面向接口编程
- 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多,以抽象为基础搭建的架构比以细节为基础的架构要稳定的多,在java中,抽象指的是接口或抽象类,细节就是具体的实现类
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
依赖关系传递的三种方式:
- 接口传递(接口作为方法的参数)
- 构造方法传递(接口作为字段,通过构造方法传入)
- setter方法传递(接口作为字段,通过setter方法传入,构造器就不必要传入)
程序要依赖于抽象接口(字段、参数、返回值,用到都是依赖),不要依赖于具体实现
错误例子:A类方法参数上直接使用了具体的B类,那么A类直接依赖了细节,当与B类相似的功能扩展时A类不易扩展
-- 里氏替换原则
Liskov Substitution Principle:
- 在1988年,由麻省理工学院的一位姓里的女士提出。是指:对于每个类型T1的对象o1,都要类型为T2的对象o2,使得以T1定义的所有程序P在所有的o1都替换为o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型,即所有引用基类的地方都能透明的使用其子类的对象
- 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类方法(重写了就不透明了)
- 继承实际上让两个类的耦合增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题
-- 开闭原则(ocp原则)
Open Closed Principle:
- 是编程中最基础最重要的原则:对扩展开放(提供方),对修改关闭(使用方)
- 一个软件实体,如类,模块和方法应该对扩展开放(对提供方),对修改关闭(对使用方)
- 当需求变化时,尽量通过扩展实体的行为来实现变化,而不是通过修改已有代码来实现变化。
- 编程中使用其他原则以及使用设计模式的目的就是遵循开闭原则
-- 迪米特法原则
Demeter Principle:
- 一个对象应该保持对其他对象最少的了解
- 类与类的关系越密切,耦合度越大
- 迪米特法则又叫最少知道原则,即一个类对依赖的类知道的越少越好,也即是对于被依赖的类不管多么复杂,都尽量将逻辑封装到类的内部,对外除了提供public方法,不对外泄露任何信息
- 迪米特法则还有更简单的定义:只与直接的朋友通信,它的核心是降低类之间的耦合
- 直接朋友:对象与对象直接的耦合关系,即朋友关系,其中成员变量、方法参数、返回值为直接朋友,出现的局部变量不是直接朋友,陌生的类最好不要以局部变量的形式出现在类的内部。
-- 合成复用原则
Composite Reuse Principle,原则是尽量使用合成、聚合的方式,而不是使用继承
比如B类要使用A类的方法,如果使用继承,就会导致A和B的耦合性增强。可以让A类作为B类方法的参数使用(关联),或者让A类作为B类的属性并提供setter方法(聚合),或者让A类作为B类的属性并new出来(组合),这样B类和A类的耦合就会很小。
UML类图
Unified modeling language 统一建模语言,是一种用于软件系统分析和设计的语言工具,用于帮助软件开发人员进行思考和记录思路的结果。本身是一套符号的规定,有用例图、静态结构图(类图、包图、组件图、部署图)、动态行为图(交互图(时序图、协作图)、状态图、活动图)
类图是描述类与类之间关系的,是UML的核心,类与类之间的关系有六种:依赖、泛化(继承)、实现、关联、聚合、组合
- 依赖关系:只要类中使用到了对方,就存在依赖关系(属性、返回值、参数、方法中使用【违背迪米特法则】),虚线小箭头
- 泛化关系:依赖关系的特例,实际上就是继承关系,实线箭头
- 实现关系:依赖关系的特例,A类实现B接口,虚线箭头
- 关联关系:依赖关系的特例,是类与类之间的联系,有导航性(单向关系和双向关系)、多重性(一对一,多对一,一对多),实线
- 聚合关系:关联关系的特例(故有导航性和多重性,谁依赖谁),表示整体与部分的关系,整体与部分可以分开,实线空心箭头,(Computer类依赖于Mouse类、Monitor类,可以分开,是聚合关系)【通过setter方法依赖】
- 组合关系:关联关系的特例(故有导航性和多重性,谁依赖谁),表示整体与部分的关系,整体与部分不可以分开,实线实心箭头,(Person类依赖于IDCard类、Head类,要存在都存在,要不存在都不存在,不可以分开,是组合关系)【属性直接new,或构造器传入,或者业务上存在组合关系(共存)】
从泛化的角度讲:依赖关系(泛化、实现、关联、聚合、组合都是依赖关系,都是特例)是一个大的关系,其中关联关系下表示整体与部分关系的又有聚合和组合(故有关联关系的导航性和多重性,具体的谁聚合谁组合谁、组合几个属性)
设计模式分类
设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模板不是代码,而是某类问题的通用解决方案,设计模式代表了最佳的实践,这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。本质是提高软件的维护性、通用性和扩展性,并降低软件的复杂度。
《设计模式》经典设计模式的书,作者四人组GOF,设计模式并不局限于某种语言。
设计模式共分为三种类型,23种
- 创建型:单例模式,抽象工厂模式,原型模式,建造者模式,工厂模式
- 结构型:适配器模式,桥接模式,装饰者模式,组合模式,外观模式,享元模式,代理模式
- 行为型:模板方法模式,命令模式,访问者模式,迭代器模式,观察者模式,中介者模式,备忘录模式,解释器模式,状态模式,策略模式,责任链模式
单例模式
Singleton,八种方式(其实可以看成5种,饿汉式两种没太大区别,懒汉式只要一种线程安全):
- 饿汉式2种写法(静态变量、静态代码块)
- 懒汉式3种写法(同步方法保证线程安全,但是效率低)
- 双检锁(volitle + synchronized + 双重检查)
- 静态内部类(jvm保证线程安全,静态内部类保证懒加载)
- 枚举类(jvm保证线程安全,还能防止反序列化重新创建对象)
单例模式注意事项和细节说明:
- 单例模式保证了系统内存中只有一个对象,节省了系统资源,对于一些需要频繁创建和销毁的对象,使用单例模式可以提高系统的性能。
- 当想实例化一个对象时,使用方法而不是new
- 使用场景:频繁创建和销毁的对象、创建时耗时过多或过多资源(重量级对象)、经常使用的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂)
源码参考:jdk中的Runtime类是饿汉式
-- 饿汉式
点击查看代码
//饿汉式(静态常量)
//饿汉式(静态代码块)
// 线程安全 但有可能造成内存浪费
class Singleton {
//1 静态变量
private static final Singleton instance = new Singleton();
//静态代码块形式
// private static Singleton instance;
// static {
// instance = new Singleton();
// }
//2 私有化构造器 外部不能new
private Singleton() {}
//3 提供一个公共的静态方法 返回实例对象
public static Singleton getInstance() {
return instance;
}
}
-- 懒汉式
点击查看代码
//懒汉式
class Singleton {
private static Singleton instance;
private Singleton(){}
//懒汉式1 线程不安全
// public Singleton getInstance() {
// if (instance == null) {
// return new Singleton();
// }
// return instance;
// }
//懒汉式2 线程安全了 但是效率低
public synchronized Singleton getInstance() {
if (instance == null) {
return new Singleton();
}
return instance;
}
//懒汉式3 写法与2不一样 但是效果和1一样 没有起到同步的作用 不能使用
// public Singleton getInstance() {
// if (instance == null) {
// synchronized (Singleton.class) {
// return new Singleton();
// }
// }
// return instance;
// }
-- 双检锁(懒加载)
点击查看代码
//双检锁形式
// 懒加载 保证效率和线程安全
class Singleton {
private static volatile Singleton instance; //值改变时立刻更新到主内存 保证可见性 否则可能初始化多次
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
-- 静态内部类(懒加载)
点击查看代码
//静态内部类的形式 线程安全 效率高
// 1 在外部类加载时 内部类并不会加载 实现懒加载
// 2 利用jvm加载类的特性实现线程安全
class Singleton {
private Singleton() {}
private static class SingletonInstance {
//静态内部类加载时创建对象
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
-- 枚举类
点击查看代码
//使用枚举实现
// 线程安全 懒加载 也能避免反序列化创建新的对象
enum Singleton {
INSTANCE;
public void sayOK() {
System.out.println("ok");
}
}
工厂设计模式
Factory Pattern,工厂设计模式有三种:简单工厂模式、工厂方法模式、抽象工厂模式(工厂方法和抽象工厂可以看成一种)
- 简单工厂模式:也叫静态工厂模式,是将创建对象的逻辑放在一个factory类中
- 工厂方法模式:使用抽象类,将创建方法抽象,由子类实现,其余逻辑可以父类中
- 抽象方法模式:使用接口,将创建方法抽象,由实现类实现,形成工厂簇
参考源码:jdk中的Calendar类,使用了简单工厂模式,Calendar.getInstance()
简单工厂:新建一个类创建不同对象
工厂方法:抽象类,实现类创建不同的对象
抽象方法:接口,实现类创建不同的对象
总结:把如何创建不同对象的逻辑封装进工厂内,可以设计抽象类或接口为抽象工厂,Context聚合抽象工厂即可,需要什么样的工厂就传递给Context什么样的工厂实现类
简单工厂模式
定义一个创建的对象的类,由这个类封装创建对象的行为(代码)。
使用:A类要创建B类、C类、D类...的对象,将来A类要扩展,BCD也要扩展。简单工厂模式是将创建BCD类对象的逻辑放在Factory中,A及其扩展只需要使用Factory即可,将来扩展BCD类只需要修改Factory,扩展A也只需要拿一个Factory。
传统的
使用简单工厂
工厂方法模式(使用抽象类,多个is-a)
将简单工厂做成一个抽象类,将需要子类实现的方法做成抽象方法,具体的工厂去继承抽象类,这个抽象类的子类都是工程类,工厂方法模式将对象的实例化推迟到子类。
应用背景:Pizza又有北京的和上海的,所以Pizza的种类有BJAPizza、BJBPizza、SHAPizza、SHBPizza,可以看成是种类的组合,这时如果用简单工厂模式就会有BJPizzaFactory、SHPizzaFactory,每个factory中分别创建各自的对象,但是创建之外的方法和逻辑都是一样的,这时可以使用一个更抽象的factory,将其中创建对象的方法抽象化,其他相同的逻辑放在抽象factory中,达到让创建对象的逻辑下沉到实现类中。
总结:相比于简单工厂模式,工厂方法模式将某个或某些方法抽象化,将不变的部分放在抽象类里,变化的部分下沉到实现类中实现。简单工厂模式只是将创建对象的逻辑统一收集起来,适用于有一套固定的创建对象的逻辑。而工厂方法模式是将多种创建对象的逻辑中相同的实现逻辑统一收集到抽象类中,不同的逻辑由各个实现类实现,适用于有多种不同的创建对象的逻辑。
抽象工厂模式(使用接口,多个like-a)
将简单工厂做成interface,用于创建相关或者有依赖关系的对象簇,而无需指明具体的类,可以将简单工厂模式和工厂方法模式进行整合。
总结:抽象工厂是将工厂抽象为两层,接口层和实现类层,可以根据创建类型使用对应的工厂子类,这样将简单工厂变成了工厂簇,更利于代码的扩展和维护。
原型模式
Prototype Pattern,spring使用xml配置创建对象时可以选择type是prototype还是singleton,这里说的就是原型模式,原型模式指的是对象实例是一个原型,可以不断拷贝相同的对象出来。允许一个对象再创建另外一个可定制的对象(通过实现cloneable接口重写clone方法或自定义方法)
- 浅拷贝:只拷贝基本类型,引用类型拷贝引用。重写clone方法即可
(Sheep)super.clone()
- 深拷贝:拷贝所有可达对象,为引用类型申请新的对象空间
实现深拷贝的两种方法:
- 重写clone方法:在浅拷贝的基础上处理引用类型,要求该引用类型对象也能深拷贝
- 序列化/反序列化:重新定义一个新的deepclone方法,在该方法中序列化该对象,再进行反序列化返回,也可以使用其他的框架实现,比如fastjson
总结:要想使用Object的clone()方法实现克隆对象,必须实现cloneable接口,并重写Object的clone()方法为public,此时的clone()仅仅是浅拷贝。如果使用clone()方法实现深拷贝,必须要求该对象的属性是(基本类型、String、所有可达对象都实现了cloneable接口和重写了clone方法)。这个方法比较麻烦,也要求比较高,像Integer类型就无法克隆。推荐的方法时使用序列化和反序列化,可以使用字节流和对象流结合,也可以使用框架如fastjson。注意:需要序列化的类必须实现Serializable接口。
注意事项和使用细节:
- 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
- 不用初始化对象,而是动态的获得对象运行时状态
- 如果原始对象发生变化(增加或减少属性),其克隆对象也会发生相应的变化,无需修改代码
- 在实现深克隆时可能需要比较复杂的代码
- 缺点:需要为每个类配备一个clone方法,这对全新的类不是很难,但是对已有的类需要修改源码,违背了ocp原则
代码:
点击查看代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Sheep implements Cloneable{
private int id;
private Integer idx;
private String name;
private Sheep friend;
//浅拷贝
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeepSheep implements Cloneable, Serializable {
private int id;
private DeepSheep friend;
private DeepTarget deepTarget;
//深拷贝
//引用类型的对象必须也实现了Cloneable接口 重写了clone方法以处理非引用类型
@Override
public Object clone() throws CloneNotSupportedException {
DeepSheep deepSheep = null;
deepSheep = (DeepSheep) super.clone(); //处理非引用类型
if (friend == null) {
deepSheep.setFriend(null);
} else {
deepSheep.setFriend((DeepSheep) friend.clone()); //处理引用类型friend
}
if (deepTarget == null) {
deepSheep.setDeepTarget(null);
} else {
deepSheep.setDeepTarget((DeepTarget) deepTarget.clone());
}
return deepSheep;
}
//使用流进行序列化反序列化 实现深拷贝
public Object deepClone(){
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
Object o = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos); //没有空参的
oos.writeObject(this);
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
o = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
ois.close();
bis.close();
oos.close();
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return o;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeepTarget implements Cloneable, Serializable {
private int id;
private String name;
//浅拷贝
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
测试类:
点击查看代码
public class DebugTest {
public static void main(String[] args) throws CloneNotSupportedException {
//1 - 浅拷贝 clone(): 实现cloneable接口 重写clone方法为public
System.out.println("-----clone()------");
Sheep p1 = new Sheep(1, 11, "n1", null);
Sheep sheep = new Sheep(2, 22, "n2", p1);
Sheep sheep1 = (Sheep) sheep.clone();
//2 - 深拷贝 clone():实现cloneable接口 重写clone方法为public
//引用属性也必须实现cloneable接口 并重写了clone方法 (所有可达的对象必须都满足)
System.out.println("-----clone() deep------");
DeepSheep dp1 = new DeepSheep(1,null,null);
DeepSheep deepSheep = new DeepSheep(2, dp1, new DeepTarget(20, "dd20"));
DeepSheep deepSheep1 = (DeepSheep) deepSheep.clone();
//3 - 深拷贝 deepClone():使用序列化、反序列化
// - 可以在对象中使用deepClone方法 进行对象的序列化反序列化(实现Serializable接口)
System.out.println("-----clone() deep------");
dp1 = new DeepSheep(1,null,null);
deepSheep = new DeepSheep(2, dp1, new DeepTarget(20, "dd20"));
deepSheep1 = (DeepSheep) deepSheep.deepClone();
//4 - 深拷贝:使用框架
System.out.println("-----fastJSON deep------");
dp1 = new DeepSheep(1,null,null);
deepSheep = new DeepSheep(2, dp1, new DeepTarget(20, "dd20"));
deepSheep1 = JSON.parseObject(JSON.toJSONString(deepSheep), DeepSheep.class);
}
}
建造者模式
Builder Pattern,又叫生成器模式,是一种对象构建模式,将复杂对象的建造过程抽象出来,使这个抽象过程的不同实现方法可以构造出不同表现的对象。它允许用户只通过指定复杂对象的类型和内容就可以构建他们。
应用场景:建房子,过程为打桩、砌墙、封顶,不同的房子过程不一样
角色:产品(房子),抽象建造者(接口、抽象类,构造过程的抽象定义),具体建造者(实现接口,实现具体的构造过程),指挥者(组合抽象建造者,指挥其建造房子,它的作用就是隔离了用户与对象创建的过程,并且控制产品生产过程)。
传统解决:
使用建造者模式:
特点:建造者模式创建的产品一般具有较多的共同点,组成部分相似,如果产盘之间的差异性很大,不适合使用
总结:定义抽象类建造者,聚合建造的产品,定义建造过程。不同的产品继承抽象类建造者。定义指挥者,聚合抽象建造者,建造产品。
jdk源码参考:StringBuider
注意事项和使用细节:
- 客户端不需要知道产品的细节,将产品本身与创建过程解耦,使得相同的创建过程可以创建不同的对象
- 每一个具体的建造者都相对独立,与其他建造者无关,可以方便的替换或新增,用户使用不同的建造者可以得到不同的创建对象
- 可以更加精细化的控制产品的创建过程,将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序控制创建过程
- 增加建造者无需修改源码,符合ocp原则
- 建造者模式创建的产品一般具有较多的共同点,组成部分相似,如果产品差异较大不适合使用建造者模式
- 如果产品内部变化复杂,可能需要定义很多具体的建造者类实现这种变化,导致系统变得很庞大,这是需要考虑是否选择建造者模式
- 抽象工厂模式VS建造者模式:抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关系构建过程,只关心什么产品由什么工厂生产即可(产品已经有了)。而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
适配器模式
Adapter Pattern,将某个类的接口转换成客户端期望的另一个接口,主要目的是兼容性,让原本两个因接口不能一起工作的类可以协同工作,别名为包装器(Wrapper)。
用户调用适配器转换过来的接口,适配器调用被适配者的相关接口方法。
适配器模式属于结构性模式,主要有三类:
-
类适配器:继承被适配的类(要求是类,有局限性),实现目标接口(要求是接口),在目标接口的方法内使用继承过来的被适配类的接口。
-
对象适配器:聚合被适配的类(使用src的一个实例,不要求是类还是接口),实现目标接口(并不要求是接口),在目标接口的方法内调用使用被适配类的对象的方法。
-
接口适配器:当不需要全部实现接口提供的方法时,可以先设计一个抽象类实现src接口,并为该接口中每个方法提供默认实现(空方法),那么该抽象类的子类可以有选择的覆盖父类的某些方法来实现需求。
SpringMVC源码参考:SpringMVC中有很多controller(HTTPController,SimpleController...,请求的方法,也叫handler),每个controller(Handler)都有对应的实现了HandlerAdapter接口的Adapter,这个接口有两个方法,supports()
,handle()
,在请求(某种handler)进入DispatcherServlet时(聚合了所有HandlerAdapter,是一个List),遍历这个List,只要这个请求的handler的supports()
返回true,就使用这个HandlerAdapter调用handler()
(子类的方法)。外部的请求有很多种,对应的handler也有很多种,在有请求进来时,使用handler对应的Adapter去判断是否支持,支持的话就调用该handler的方法。
优点:扩展性强,扩展controller时,只要增加controller对应的Adapter就可以了,在使用上不用修改。
注意事项和使用细节:
- 类适配器
- 由于Java的单继承机制,类适配器需要继承src类,算是一个缺点,因为这要求dst必须是一个接口,有一定局限性
- src的方法会在adapter中暴露出来,增加了使用成本
- 由于继承了src类,可以重写src的方法,使得adapter的灵活性增强了
- 对象适配器
- 与类适配器的实现是同一种思想,只是实现方式不同,根据合成复用原则,使用组合替代继承,解决类适配器必须继承src的局限性问题,也不在要求dst必须是接口
- 成本更低,更灵活
桥接模式
Bridge Pattern,将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。基于类的最小设计原则,通过使用封装、聚合、继承等行为让不同的类承担不同的职责。主要特点是把抽象和行为实现分离,从而可以保持各部分的独立性以及应对他们的功能扩展。
应用场景:手机有不同类型、不同品牌,每种类型、每种品牌都有自己的操作实现方式。如何实现增加类型或品牌时可以容易扩展。
使用:定义一个抽象类(实现层次),定义一个接口(抽象层次,作为一个桥,可以让实现层次的子类使用自己的实现类),抽象类内聚合接口。这样在抽象类的子类里就可以使用接口实现类的方法。
传统方法:
桥接模式:
特点:
- 实现抽象与实现部分的分离,极大的提供了系统的灵活性,让抽象部分和实现部分独立,有助于系统进行分层设计,从而产生更好的结构化系统。
- 对于系统的高层部分,只需要知道抽象部分的实现接口就可以了,其他部分有具体业务完成。
- 桥接模式代替多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。
- 桥接模式的引入增加了系统的理解和设计难度,由于聚合关系建立在抽象层,要求开发者针对抽象进行设计和编程。
- 桥接模式要求正确失败出系统中两个独立变化的维度(抽象和实现),因此其实用范围有一定的局限性,
总结:
源码参考:jdbc,DriverManager是抽象类,聚合了java.sql.Connection,不同种类的数据库,如mysql,继承了java.sql.Connection接口,MySQLConnection又继承了java.sql.Connection,MySQLConnection接口的实现类是具体的实现,DriverManager直接就根据不同数据库的注册拿到了不同的Connection实现类。
装饰者模式
Decorator Pattern,动态的将新功能附加到对象上,在对象功能扩展方面,比继承更有弹性,也体现了开闭原则
使用:定义一个抽象类组件(被装饰者和装饰者都有的共同的抽象),被装饰者继承它,装饰者也继承,但是在装饰者中组合被装饰者(使用抽象类类型),并定义构造方法为抽象类类型,这样在装饰者子类的实现时,需要传入抽象类型的一个对象(被装饰对象或装饰后的对象,因为都是同一个抽象类型)
被装饰者可以设计缓冲层,提供公共部分
总结:接口或抽象类作为组件,实现类组合组件
源码参考:FilterInputStream就是一个装饰者,InputStream是顶级抽象类
组合模式
Composite Pattern,又叫部分整体模式,它创建了对象组的属性结构,架构对象组合成树形结构以表示“整体-部分”的层次关系。
使用:定义一个接口后抽象类为组合中的基本组件(component),每种类型的组件实现或继承,根据自身组件的类型选择聚合一个List<Component>
,这样的组件可以管理其他组件。如学校->学院->部门,学校的实现类里可以管理学院,学院的实现类里管理部门,部门则是叶子节点,不需要聚合List
特点:
- 需要遍历组织机构,或者处理的对象具有树形结构时,非常适合使用组合模式。
- 要求较高的抽象性,入股节点和叶子有很多差异性的话,不适合使用组合模式
总结:接口或抽象类作为组件,实现类聚合组件的List管理其他组件
源码参考:HashMap是Map的具体实现类,其中的putAll方法可以管理其他Map,HashMap里的Node就像是一个List(基本结构),管理其他Map(基本组件)
外观模式
Facade Pattern,也叫过程模式,为子系统中的一组接口提供一个一致的界面,定义一个高层接口,这个接口使得这一系统更加容易使用,帮助我们划分访问的层次。
使用:定义一个新的类,去调用各个子系统的接口,以实现某一类工作,避免客户分别调用各个子系统的各个接口
特点:
- 帮我们更好的划分访问层次
- 在维护一个遗留的大型系统时,可以能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个facade类,来提供遗留系统的比较清晰简单的接口,让新系统与facade类交互,提高复用性
- 不能过多的使用外观模式,要让系统有层次,利于维护为目的。
参考源码:mybatis的Configuration类,创建了MetaObject对象,对象使用到外观模式
享元模式
Flyweight Pattern,也叫蝇量模式,运用共享技术有效的支持大量细粒度的对象。
- 常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的对象,在在这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个。
- 享元模式能够解决重复对象的内存浪费问题,当系统中有大量相似对象,需要缓冲池时,不需要总是创建对象,可以从缓冲池里拿,这样可以降低内存使用,同时提高效率。
- 享元模式经典的场景就是池技术,string常量池,数据库连接池,缓冲池等都是享元模式,享元模式是池技术重要的实现方式。
享元模式提出了两个要求:细粒度和共享对象,这就涉及到内部状态和外部状态了,即将对象的信息分为内部状态和外部状态。
- 内部状态指对象共享出来的信息,存储在享元对象内部而不会随着环境的改变而改变
- 外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态
- 比如围棋游戏,棋子对象可以只有黑白两个
特点:解决重复对象的内存浪费问题
使用:产品的抽象类为A(享元角色),定义出对象的内部状态和外部状态的接口或实现。共享的部分继承为一个享元角色,其他不共享的继承为一个角色。定义一个factory,客户端在使用享元角色时,访问factory,如果有就直接拿,如果没有就创建对应的享元角色,并放进factory的HashMap/HashTable里面,供后面其他客户端使用时调用,共享对象。
注意事项和使用细节:
- 享元模式可以理解为共享对象
- 系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以内部化时,可以考虑使用享元模式
- 用唯一标识码判断,如果内存中有,则返回唯一标识码标识的对象,用hashmap/hashtable存储
- 享元模式大大减少了对象的创建,降低程序内存的占用,提高效率
- 享元模式提高了系统的复杂度,因为需要分离出内部状态和外部状态,内部状态具有固化特性,不随外部的改变而改变
- 使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制
总结:将总是使用又不变的对象放入池子中,减少对象的创建
参考源码:Integer使用Integer.value()调用的-128到127之间的数是缓存的
代理模式
Proxy Pattern:
-
静态代理:代理类与被代理类实现同样的接口,自己写代理对象,代理类聚合被代理类,在代理类的方法中调用被代理对象的方法
-
动态代理:与被代理类实现相同的接口,JDK代理,使用JDK的
Proxy
类动态生成代理对象
-
cglib代理:代理的目标没有实现的接口,cglib使用子类实现代理(不能是final类,final/static方法不会拦截)
spring AOP使用cglib实现方法拦截,cglib底层使用asm框架来转换字节码并生成新的类
如何选择代理:目标对象需要实现接口,使用JDK代理。目标对象不需要实现接口,使用cglib
代理模式的变体:
- 防火墙代理:内网通过代理穿透防火墙,实现对公网的访问
- 缓存代理:请求图片文件等资源时,先去缓存代理取,如果取不到再去公网或者数据库取,然后缓存
- 远程代理:远程对象的本地代理,通过它可以把远程对象当本地对象调用,远程代理通过网络和真正的远程对象沟通信息
- 同步代理:主要使用在多线程编程中,完成多线程间的同步工作
模板方法模式
Template Method Pattern,又叫模板模式,在一个抽象类公开定义了执行它的方法的模板,它的子类可以按需重写方法实现,但调用将以抽象类中定义的方式进行。简单来说,模板方法定义了操作中一个算法的骨架,而将一些步骤延迟到子类中,使得子类不改变算法的结构就可以重新定义该算法的某些特定步骤。
应用场景:做豆浆,需要很多步骤,选材料、添加配料、浸泡、放入豆浆机打碎,其中选材料不同,整个调用过程是一样的
使用:定义抽象类,抽象类中定义各个方法,其中添加配料做成抽象的让子类实现,做的过程为final方法,调用其他方法,实现类使用时使用父类的final方法,final方法中规定好了如何调用(模板方法)
钩子方法:在模板方法模式的抽象类中,可以定义一个空方法(或默认实现方法),它不做任何事情,子类可以视情况要不要覆盖它,该方法称为钩子方法。如果子类覆盖,可以写子类的逻辑,在抽象类的模板方法中影响原来的逻辑。钩子方法的价值是让子类实现
注意事项和使用细节:
- 基本思想是:算法只存在于一个地方(父类),容易修改。修改父类的模板方法子类也会继承。
- 实现了最大化代码的复用,父类已经实现的模板方法和已实现的某些步骤会被子类继承而直接使用。
- 既统一了算法,也提供了很大的灵活性,父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现。
- 缺点:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大。
- 一般模板方法都加final关键字,防止子类重写模板方法。
- 使用场景:当要完成某个过程,该过程执行一系列步骤,这一系列的步骤基本相同,个别步骤在实现时可能不同,通常考虑用模板方法处理。
总结:在抽象类中定义模板方法,定义好方法的执行顺序,抽象类可以有很多层,抽象类中也可以定义钩子方法,用于子类实现改变原来的逻辑。
命令模式
Command Pattern,在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个(遥控器控制多个家电的开关,每个开关也不知道底层执行了哪些操作),只需要在程序运行时指定具体的接收者即可,此时可以使用命令模式。
命令模式使得请求者与接收者消除彼此的耦合,让对象之间的调用关系更加灵活,实现解耦。(比如将军发布命令,士兵执行,其中的角色:将军是命令发布者,士兵是命令执行者,命令连接将军和士兵)
在命令模式中,将请求(每一个命令)封装为一个对象,以便使用不同的参数来表示不同的请求,命令模式也支持可撤销的操作。
使用:定义一个命令接口,具体的命令实现该接口,调用者聚合命令接口,具体的命令聚合具体的被调用者。
特点:
- 将发起请求的对象与执行请求的对象解耦(发起请求的对象聚合命令接口,执行请求的对象实现命令接口,再聚合实际执行的对象,在实现方法中调用实际执行对象的方法)。命令对象把命令请求者和命令接收者联系起来(请求者聚合命令对象或通过参数传给方法,命令对象聚合接收者)
- 容易设计一个命令队列,使用多线程去执行命令
- 容易实现请求的撤销和重做
- 缺点:可能导致系统有很多的具体命令类
- 空命令也是一种设计模式,省去判空操作
参考源码:jdbcTemplate(调用者)的query方法,通过参数StatementCallback调用了它的doInStatement方法(接口StatementCallback中的方法,这个接口相当于命令接口),实际执行的是query方法中的内部类(实现了StatementCallback接口)
访问者模式
Visitor Pattern,封装一些作用于某个中数据结构的各元素的操作(比如一个List<Person>
),可以在不改变数据结构的前提下定义作用于这些元素的新的操作(每个元素定义一个accept()方法,参数为Action,Action接口中可以定义方法参数为Person,这样Action可以操作Person,Person的accept就提供了一个对外接待访问者的接口)
主要是将数据结构与数据操作分离,解决数据结构与操作耦合的问题。使用的基本原理是在被访问的类里加对外提供访问者的接口(Person的accept方法,参数为访问者Action)
应用场景:需要对对象结构中的对象进行很多操作,操作之间没有关联,同时需要避免这些操作污染这些对象的类(只提供了accept方法,传入this给访问者的方法,这样访问者就可以操作自己这个对象了)
注意事项和使用细节:
优点:
- 将数据结构与数据操作分离,解决数据结构和操作耦合的问题
- 符合单一职责原则,程序扩展性、灵活性高
- 可以对功能进行统一,可以做报表、UI、拦截器和过滤器,适用于数据结构相对稳定的系统
缺点: - 具体元素对被访问者提供细节,也就是访问者关注了其他类的内部细节,违背了迪米特放在,造成具体元素变更比较困难
如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,比较适合访问者模式
迭代器模式
Iterator Pattern,提供一种遍历集合元素的统一接口,用一致的方法遍历集合元素,不需要知道集合对象的底层表示
使用:实现Iterator接口,实现其中的hasNext和next方法。某个类聚合了某种数据结构,在需要这个Iterator的时候new这个对象(某种类需要返回迭代器对象,在内部方法new)
总结:实现了Iterator接口,就是一个Iterator,在需要返回一个迭代器的地方返回一个该接口的实现类对象
源码参考:ArrayList的iterator,List接口有一个iterator()方法,ArrayList实现时返回了一个Itr(),这个Itr是一个内部类,实现了Iterator接口。
注意事项和使用细节:
- 提供统一的方法遍历对象,不用考虑聚合的类型,使用一种方法就可以遍历对象
- 隐藏对象的内部结构,遍历的时候只需要取到迭代器
- 提供了一种思想:一个类应该只有一个引起变化的原因(单一职责原则),在聚合类中,把管理对象的集合和遍历对象集合的职责分开,这样改变集合只影响聚合对象,改变遍历方式只影响迭代器。
- 当想要展示一组相似对象,或者遍历一组相同对象时,适合使用迭代器模式
- 缺点:每个聚合对象都需要一个迭代器
观察者模式
Observer Pattern,对象之间多对一依赖的一种设计方案,被观察的对象为Subject(一个接口,提供管理观察者的抽象方法),观察者为Observer(一个接口,方法被被观察者subject调用而被通知到变化),Subject通知所有的Observer变化。
使用:被观察者实现Observer接口(接口中有方法注册观察者、移除观察者、通知观察者等,或者自己类内部定义,比如JDK的Observable),在通知观察者方法中调用观察者的方法,因此,观察者要维护一个被观察者列表。
参考源码:JDK的Observable是类,被观察者,内部有管理观察者的方法,Observer接口,有update方法
中介者模式
Mediate Pattern,用一个中介对象来封装一系列的对象交互,使各个对象不需要显示的相互引用,从而使耦合松散,而且可以独立的改变他们之间的交互,使代码易于维护。
比如MVC模式,Mode,View,Controller,Controller就是一个中介者,在前后端交互时起到中间人的作用
多个子系统之间不进行交互,都与中介者交互,中介者保存所有的子系统对象(子系统在创建时传入中介者,调用中介者的方法,中介者聚合为Map),子系统在需要其他子系统合作时,发消息给中介者(利用自己聚合的中介者),中介者调用自己维护的子系统的方法
使用:中介者维护子系统列表,子系统聚合中介,使用中介调用它的方法,把消息发给它,中介在根据需要调用其他对象,进行交互
注意事项和使用细节:
- 多个类相互耦合会形成网状结构,使用中介者模式可以将网状结构分离为星型结构,进行解耦
- 减少类间依赖,降低耦合,符合迪米特法则
- 中介者承担了较多的责任,一旦中介者出现问题,整个系统都会收到影响
- 如果设计不当,中介者对象会变得过于复杂
备忘录模式
Memomento Pattern,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态(将对象的状态保存为一个memo对象)
使用:被保存的对象使用两个方法即可,一个创建memo并返回,一个从memo恢复,memo类的内容就是对象的状态,可以创建另外一个CareTaker管理memo对象。
注意事项和使用细节:
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便的回到某个历史状态
- 实现了信息的封装,用户不需要关心状态的保存细节
- 如果类的成员变量过多,会占用比较大的资源,并且每一次保存都会消耗一定的内存,需要注意
- 为了节约内存,可以和原型模式配合使用(不变的东西浅拷贝,变的东西深拷贝)
解释器模式
Interpreter Pattern,在编译原理中,一个算术表达式通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器构建语法分析树,最终形成一颗抽象的语法分析数。这里的词法分析器和语法分析器都可以看做是解释器。
解释器模式:是指给定一个语言(表达式),定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子(表达式)
应用场景:
- 可以将一个需要执行的句子表示为一个抽象语法树
- 一些重复出现的问题可以用一种简单的语言来表达
- 一个简单语法需要解释的场景
使用:定义表达式接口(Expression),将表达式中的词法进行分类(比如加减法中的,符号表达式(又分为加法,减法),变量表达式),分别实现该接口,在上下文(Context,包含一些解释器之外的全局信息)中创建表达式,并解释。
特点:
- 当一个语言需要解释执行,可以将该语言中的句子表示为一个抽象语法树(加减法的例子中最终通过栈形成了一个Expression),就可以考虑使用解释器模式
- 应用场景:编译器、运算表达式、正则表达式
- 会引起类膨胀,解释器模式采用递归调用方法,会导致调试非常复杂,效率可能会降低
状态模式
State Pattern,主要用来解决对象在多种状态转换时,需要对外输出不同的行为的问题。状态和行为是一一对应的,状态之间可以相互转换。当一个对象的状态改变时,允许改变其行为,这个对象看起来像是改变了其类。(不同状态不同行为,那么每个状态都设计为一个类,共同实现一个接口或抽象类,顶层接口将所有的行为都设计为抽象的或抽象类默认实现)
应用场景:当一个时间或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候,可以考虑使用状态模式。
使用:需要一个上下文(Context)维护所有的状态(比如抽奖活动类),定义一个接口或抽象类(State,可以提供默认方法,或抽象类实现接口写默认方法),具体的状态实现或继承State,在Context里维护所有的状态对象。
注意事项和使用细节:
- 代码有很强的可读性,每个状态的行为封装为一个类
- 方便维护,将容易产生问题的if else删除了,如果把每个状态的行为都放到一个类中,每次调用方法都要判断当前是什么状态,不但会产生很多if-else,还容易出错
- 符合开闭原则,容易增删状态
- 会产生很多类,每个状态对应一个类,状态过多时会产生很多类,加大维护难度
策略模式
Strategy Pattern,定义算法族,分别封装起来,让他们直接可用互相替换(通过聚合到使用者的上层接口,使用者可以随时使用修改算法),让算法的变化独立于使用算法的用户(用户只是聚合了策略的顶层接口)
体现了几个设计原则:
- 把变化的部分(策略)从不变代码中分离出来
- 面对接口编程而不是具体的类
- 多用组合、聚合,少用继承
注意事项和使用细节:
- 策略模式的关键:分析项目中的变化部分与不变部分
- 核心思想是:多用聚合、组合,少用继承,用行为类组合,而不是行为的继承,更有弹性
- 体现了对修改关闭,对扩展开放的原则,客户端增加行为策略不需要修改原有代码,只需要添加一种策略即可,避免了多重if-else
- 提供了可以替换继承的办法:策略模式将算法封装在独立的Strategy中,使得可以独立于Context改变它,使它易于切换、易于理解、易于扩展
- 需要注意的是:每添加一个策略都要增加一个类,策略过多时会导致类数目庞大
参考源码:JDK的Comparator就是一个策略接口,Arrays中使用了,在sort()方法中,会根据不同的comparator进行不同的排序
职责链模式
Chain of Responsibility Pattern,又叫责任链模式,为请求创建一个处理该请求的对象的链,将请求的发送者和接收者进行解耦。通常每个接收者都会包含另一个接收者的引用,如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者。
使用:定义一个抽象的处理者(Handler),并聚合一个handler,定义一个抽象的处理方法由子类实现,并在创建处理者时定义好下一级处理者,可以是一个环状的。
特点:
- 将请求和处理分开,实现解耦,提高系统的灵活性
- 简化了对象,使对象不需要知道链的结构(否则必须知道整个链怎么处理 使用if-else)
- 性能会受到影响,特别是在链比较长的时候,一般通过在handler中设置一个最大节点数量,在setNext方法中判断是否已经超过阈值,超过则不允许该链建立(可以使用List管理链,事先将链上的对象创建好,用个for循环遍历链调用处理方法,用了多态),避免出现链过长无意识的破坏系统性能
- 调试不方便,采用了类似递归的形式,调试时逻辑可能比较复杂
- 最佳应用场景:有多个对象可以处理同一个请求时,比如多级请求,审批流程,JavaWeb中Tomcat对Encoding的处理,拦截器
源码参考:SpringMVC中的HandlerExecutionChain类,其中的拦截器使用的责任链模式
idea画UML类图
- 安装plantUML插件
- 下载安装graphviz:https://graphviz.org/download/ ,idea插件已经自带了
- 使用:https://plantuml.com/zh/class-diagram
总结
- 创建型
- 工厂模式:将如何创建不同对象的过程进行抽象,不注重对象构建本身,注重如何将现有很多对象根据情况返回不同对象,即只关心什么产品由什么工厂产生
- 原型模式:以一个对象为原型,进行对象复制,解决重复对象的创建效率问题
- 建造者模式:将某个产品的构建过程进行抽象,注重对象构建过程,即关心新产品如何构建
- 结构型
- 适配器模式:A调用适配器,适配器调用B,适配器可以继承A实现B(类适配器)。A聚合适配器,实现B接口或聚合或继承(对象适配器)。也可以定义抽象类实现src接口给出默认实现,适配器在继承抽象类,重写想要使用的接口(接口适配器)。解决接口之间调用不兼容的情况
- 桥接模式:将实现与抽象分离,每个层次可以单独变化,解决类爆炸的问题
- 装饰者模式:装饰者和被装饰者都是component,装饰者聚合component,所以可以不断的装饰component
- 组合模式:抽象类Component,实现类聚合
List<Component>
以达到管理其他Component的目的 - 外观模式:定义高层接口,在高层接口中分别调用各个子系统的接口
- 享元模式:共享相同的对象
- 代理模式:使用代理去调用想要调用的对象的方法,静态代理(与被代理类实现或继承同样的接口,代理类聚合被代理类),jdk代理(被代理类需要实现接口,代理该类),cglib代理(继承被代理类实现,不需要实现接口,代理该对象)
- 行为型
- 模板方法模式:在抽象类中公开定义了执行它的方法的模板方法,它在子类可以根据需要重写方法。狗子方法:在抽象类中定义一些钩子方法,目的是让子类实现,以改变原来的执行逻辑。主要是定义统一的执行逻辑模板
- 命令模式:适合发送不同命令但并不知道执行者是谁,使用命令接口将发送者与接收者解耦(发送者聚合命令接口,具体的命令聚合接收者)。主要是将命令发送者与接收者解耦
- 访问者模式:A子类的方法的参数时B类,B子类方法的参数时A类,A作为访问者传给B子类的方法时,B可以使用双分派(操作取决于参数A和子类B)调用A类或B类的方法(或具体的实现类)。主要是将数据结构与数据操作分离
- 迭代器模式:提供一种遍历集合元素的统一接口,用一致的方法遍历集合元素,不需要知道对象的底层表示。主要是不需要暴露数据的底层存储结构就可以遍历数据
- 观察者模式:两个接口,一个观察者,一个被观察者,被观察者的实现类维护观察者的List,需要通知是,调用所有观察者的接口中的统一方法。主要用于一对多,一方通知多方
- 中介者模式:两个抽象类,一个是中介(可以注册同事),一个是同事(很多同事,聚合中介),中介维护一个所有同事的Map。由于同事聚合了中介,所以可以调用中介的方法,中介可以根据参数知道是谁发来的进行协调。主要用于一对多,一方协调多方,相互调用
- 备忘录模式:用一个memo对象保存另外一个originator对象的一些状态,并可以通过一个memo对象恢复当前originator的状态,用另外一个类管理memo,比如List、Map等。主要用于存储一份对象当前的状态
- 解释器模式:定义抽象表达式,由此进行对语法分类,最终将语句通过栈表达为一个抽象语法树进行递归解释最终结果.主要只用于语法解释
- 状态模式:定义所有状态的上下文并维护所有可能的状态(将自身传递给所有的状态,时所有的状态具有当前相同的上下文),聚合State接口为上下文的当前状态。State接口定义所有可能的操作(上下文的,比如抽奖),并聚合上下文(在上下文中管理时创建各个状态并传入this进行聚合),以便在后续利用该上下文修改上下文当前的状态。主要用于有多种状态,且行为不同,不同状态之间相互转换
- 策略模式:将算法与使用者分开(一个使用,多个算法,算法是接口),Context可以聚合不同的算法,并随时可以改变。提供了替换继承的办法
- 责任链模式:将请求与处理分开(一个请求,多个处理,处理是接口),每个处理实现类聚合处理接口,也即是下一个处理者。主要用于一个请求,多个对象处理
本文来自博客园,作者:Bingmous,转载请注明原文链接:https://www.cnblogs.com/bingmous/p/15647256.html