本笔记是基于b站黑马程序员的设计模式教程,仅供参考!!如果想获取代码,欢迎联系charoneo或在下方的评论区留言,注意只有软件设计原则部分的代码是黑马程序员视频中的,其他代码是作业要求的代码,具体每个模式的要求可以见每个包下面的requirement.txt
设计模式笔记
设计模式概述
什么是设计模式
软件设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。具有一定的普遍性,可以反复使用。可以提高程序员的思维能力、使程序设计更加标准化、设计的代码可重用性高
设计模式分类
创建型模式:用于描述怎么创建对象,将对象的创建和使用分离(解耦),有单例模式、原型模式、工厂方法、抽象工厂、建造者5种
结构型模式:描述如何将类或对象按某种布局组成更大的结构,有代理、适配器、桥接、装饰、外观、享元、组合等7种模式
行为型模式:描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责,有模版方法、策略、命令、职责链、状态、观察者、中介者、迭代器、备忘录、解释器等11种
UML
统一建模语言是用来设计软件的可视化建模语言。他的特点是简单、统一、图形化,能够表达软件设计中的动态与静态信息。
类图
类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类,类的内部结构以及它们与其他类的关系。类图不显示暂时性信息,类图是面向对象建模的主要组成部分。
类图表示法
包含类名、属性和方法
斜体表示抽象、下划线表示静态
属性/方法名称前加的加号和减号表示了这个属性/方法的可见性
- +:表示public
- -:表示private
- #:表示protected
属性的完整表示方法是:可见性 名称 : 类型 [ = 缺省值]
方法的完整表示方式是:可见性 名称(参数列表) [ : 返回类型]
类与类之间的关系
关联关系是对象之间的一种引用关系,用于一类对象与另一类对象之间的关系,分为一般关联关系,聚合关系和组合关系
1.一般关联
一般关联可分为单向关联、双向关联、自关联
单向关联
在uml图中用一个带箭头的实线表示
双向关联
就是对方各自持有对方类型的成员变量
在UML类图中用一个不带箭头的直线表示
自关联
在UML类图中用一个带有箭头且指向自身的线表示,如LinkedList
2.聚合关系
聚合关系是关联关系的一种,是强关联关系,是整体与部分的关系
成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在
在UML图中中带空心的菱形来表示
3.组合关系
组合表示类之间整体与部分的关系,但它是一种更强烈的聚合关系
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也不存在
在UML类图中用实心菱形来表示
4.依赖关系
依赖关系是一种使用关系,是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责
在UML图中用带箭头的虚线来表示
5.继承关系
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊,父类与子类之间的关系
在UML图中用空心三角箭头的实线来表示
6.实现关系
实现关系是接口与实现类之间的关系,类中的操作实现了接口中所声明的所有的抽象操作
在UML图中用空心的三角箭头来表示
软件设计原则
单一职责原则SRP
单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分
降低类的复杂度、提供类的可读性、提高系统的可维护性
开闭原则OCP
对扩展开放,对修改关闭。实现一个热插拔的效果,使用接口和抽象类
注意图中右上方是依赖关系
代码见package principles.OCP;
里氏代换原则LSP
子类必须能够替换其基类,而不影响程序的正确性。
如果一个方法能够接受一个基类作为参数,那么它应该也能接受基类的子类对象,而不会导致行为异常
子类可以扩展父类的功能,但不能改变父类原有的功能(比如重写)
重写会使整个继承体系的可复用性比较差,特别是运用多态比较频繁时
反例如下:
代码见package principles.LSP.before;
修正
注意右侧两个应该是依赖关系(虚线)
代码见package principles.LSP.after;
依赖倒转原则DIP
高层模块不应该依赖底层模块,两者都应该依赖其抽象。要求对抽象进行编程,不要对实现进行编程
A类是高层模块,而B类是底层模块
反例
代码见package principles.DIP.before;
这样的问题是组装的电脑cpu只能是intel的,内存条只能是金士顿的
修正
代码见package principles.DIP.after;
接口隔离原则ISP
客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上
代码见package principles.ISP;
迪米特法则(Law of Demeter)
最少知识原则,如果两个软件实体无需直接连接,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。每个模块都要和它关系最亲密的模块打交道
迪米特法则中的“朋友”是指:当前对象本身,当前对象的成员对象,当前对象所创建的对象,当前对象的方法参数
代码见package principles.demeter;
合成复用原则CRP
尽量使用组合或者聚合等关联关系来实现,其次才考虑使用继承来实现
直接继承的缺点:
- 父类对子类是透明的,“白箱”复用
- 子类和父类的耦合性高
- 限制了复用的灵活性
继承复用如下图
聚合复用如下图
如果还需要增加光能汽车类,就不再需要定义红色或者白色的子类
创建者模式
将对象的创建和使用分离,降低系统的耦合度,使用者不需要关注对象的创建细节
单例设计模式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象
饿汉式:类加载就会导致该单实例对象被创建
对象的创建随着类的加载而创建,所以存在内存浪费问题
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
双重检查锁模式、静态内部类方式(JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载)、枚举类(利用JAVA的底层机制)
单例模式可能存在的问题:
- 序列化和反序列化可能破环单例模式(先读取一个对象写入到文件,然后通过读文件的方式来获取到新的对象,因为每次获取到的都是该文件中的对象的拷贝,所以每次获取到(读文件)的对象内存地址都是不一样的)
- 反射破环单例模式(获取Singleton的字节码对象、获取无参构造方法对象、取消访问检查、创建Singleton对象)
对应的解决方法:
在Singleton类中添加
readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值底层实现如下图
在类中添加一个boolean属性来判断是否是第一次创建对象,如果不是,则抛出异常
代码见package patterns.singleton; 其中IoDHSingleton.java中写了关于解决两个可能存在的问题的代码,但是没有main测试
在JDK中,
Runtime.java
类就是饿汉式单例设计模式
工厂模式
反例:
背景:如果创建对象的时候直接new该对象,就会对该对象耦合严重,加入我们需要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则
工厂模式:如果需要更换对象,直接在工厂里更换该对象即可,达到了与该对象解耦的目的,工厂模式的最大优点是解耦
简单工厂模式
分为抽象产品,具体产品,具体工厂
解除咖啡店和具体产品对象的依赖,但是又产生的新的耦合
所以优点是把对象的创建和业务逻辑(点咖啡)分开,这样以后就避免了修改客户端代码,如果要实现新的产品直接修改工厂类;缺点是还是要修改工厂类,违背了开闭原则
代码见package patterns.factory.simpleFactory;(例子不一样)
工厂方法模式
完全遵循开闭原则,具体工厂创建具体产品
分为抽象产品,具体产品,抽象工厂(提供了创建产品的接口),具体工厂(实现抽象工厂中的抽象方法)
上图左上部分应该是虚线三角(implements)
如果需要新增咖啡品种(产品类),那么需要新增对应品种的工厂(工厂类),符合开闭原则的对扩展开放,对修改关闭。但这也增加了系统的复杂度
代码见package patterns.factory.factoryMethod;(例子不一样)
抽象工厂模式
区别产品族和产品等级
一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无需指定所要产品的具体类就能得到同族的不同等级的产品的模式结构
缺点:当产品组需要增加一个产品时,所有的工厂类都需要进行修改,不满足开闭原则
使用场景:每次只使用某一族的产品
代码见package patterns.factory.abstractFactory;(例子不一样)
扩展:简单工厂+配置文件解除耦合
在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可(构建一个map来存储name和Object,通过名称来获取对象)
如需要新增咖啡品种,只需要修改配置文件就行
在JDK中,Collection.iterator方法运用到了工厂模式
原型模式
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个原型对象相同的新对象
抽象原型类、具体原型类、访问类
浅克隆(原型模式):创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性指向的对象的内存地址。只复制对象的基本数据类型和引用类型的地址。
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象的地址
适用于对象的创建非常复杂,使用原型快速创建对象,性能和安全要求高
使用输出流对象来实现深克隆,先写对象到文件中,再把文件中的对象读取到新在对象中(克隆)
代码文件见package patterns.prototype; (和黑马例子不同)
建造者模式
将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示(电脑装配)
分离了部件的构造(Builder负责)和装配(Director负责),从而可以构造出复杂的对象。实现了构建和装配的解耦。不同的构建器,相同的装配,可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象
分为抽象建造者类、具体建造者类、产品类、指挥者类
具体自行车例子:
建造者模式的封装性很好;客服端不必知道内部细节,将产品本身与产品的创建过程解耦;将复杂的产品的创建步骤分解在不同的方法中;容易进行扩展,如果有新的需求,通过实现一个新的建造者类就可以完成
但是只能生产类似的产品
模式拓展:
在产品内部创建一个Builder静态内部类,在内部类中,每次build具体组件的时候返回的都是自身,从而实现链式编程,最后通过一个build方法返回产品

具体代码见package patterns.builder; (和黑马不同)
区别:工厂方法模式注重的是整体对象的创建方式,建造者模式注重的是部件构建的过程
结构型模式
描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象
代理模式
给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象的中介
按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在java运行时动态生成
分为抽象主题类(真实主题和代理对象实现的业务方法)、真实主题类、代理类
具体代码见package patterns.proxy; (和黑马不同)
JDK动态代理
Java提供了一个动态代理类Proxy,它提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象
执行流程如下:
- 在测试类中通过代理对象调用sell()方法
- 根据多态的特性,执行的是代理类(系统内存中的$Proxy0)中的sell()方法
- 代理类(系统内存中的$Proxy0)中的sell()方法又调用了InvocationHandler接口的子实现类对象的invoke方法
- invoke方法通过反射执行了真实对象所属类(TrainStation)中的Sell()方法
CGLIB动态代理(略)
代理模式的优点
- 中介作用和保护目标对象作用
- 扩展目标对象的功能
- 客户端与目标对象分离,降低了系统的耦合度
适配器模式
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作,有类适配器模式和对象适配器模式
分为目标接口(中式插头)、适配者类(欧标插头)、适配器类(转换插头)
类适配器模式
定义一个适配器类来实现当前系统的业务接口,同时有继承现有组件库中已经存在的组件
注意适配器类实现目标接口,继承适配者类
类适配器模式违背了合成复用模式
对象适配器模式
将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口
也就是把TFCard作为适配器类中的一个成员,适配器类中的构造函数的参数是TFCard
在JDK中,StreamDecoder作为适配器完成了InputStreamReader把InputStream字节流转换为Reader字符流
代码见package patterns.adapter(与黑马不同)
装饰者模式
在不改变现有对象的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式
分为抽象构件(小吃)、具体构件(炒饭、炒面)、抽象装饰、具体装饰
注意聚合画反了,新增一条Garnish继承Fastfood的线
可以带来比继承更加灵活的扩展功能,使用更加方便,完美遵循开闭原则。
装饰者类和被装饰者类可以独立发展,不会相互耦合,防止类爆炸
在JDK中,BufferedWriter使用装饰者模式对Writer子实现类进行了增强(增加了装饰),添加了缓冲区
代理和装饰者的区别:
装饰者是由外界传递进来(在类中只是声明),可以通过构造方法传递
静态代理是在代理类的内部创建(声明并创建),以此来隐藏目标对象
代码见package patterns.decorator;(与黑马不同)
桥接模式
将抽象与实现分离,使它们可以独立的变化。它是组合关系代理继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度
分为抽象化角色、扩展抽象化角色、实现化角色、具体实现化角色
抽象化角色聚合了实现化角色,体现出了两个维度之间的关系,关键是聚合部分注入接口实现类对象
提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,不需要修改原系统,满足开闭原则
代码见package patterns.bridge;(与黑马不同)
外观模式
为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。外部应用程序不用关心内部子系统的具体的细节
是“迪米特法则”的典型应用
分为外观角色、子系统角色
降低了子系统和客户端之间的耦合度,使得子系统的变化不会影响客户端
对客户屏蔽了子系统组件,使子系统用起来更加简单
不符合开闭原则
tomcat中使用RequestFacade类定义私有成员变量Request,并且方法的实现调用Request的实现。然后,将RequestFacade上转为servletRequest
代码见package patterns.facade;(与黑马不同)
组合模式
又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构
满足开闭原则
分为抽象根节点(最上方的文件夹,公共抽象类)、树枝节点(二级或三级文件夹)、叶子节点(文件)
透明组合模式
抽象根节点角色中声明了所有用于管理成员对象的方法,这样确保所有的构件类都有相同的接口,是组合模式的标准形式
不够安全,叶子节点调用树枝节点的相关方法在运行时期会抛异常
安全组合模式
在抽象根节点角色中没有声明任何用于管理成员对象的方法,而是在树枝节点中声明并实现了这些方法。
不够透明,客户端不能完全针对抽象编程,必须有区别的对待叶子和容器构件
代码见package patterns.combination;(与黑马不同)
享元模式flyweight
运用共享技术有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率
存在内部状态(不随环境改变)和外部状态(随环境而改变)
分为抽象享元角色、具体享元角色、非享元角色(非共享的具体享元类对象,通过实例化创建)、享元工厂角色(当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,存在则提供给客户;不存在则创建新的享元对象)
极大减少内存中相似或者相同的对象数量,节约系统资源。享元模式外部状态相对独立,且不影响内部状态
在JDK类中,Integer类使用了享元模式。Integer默认线创建并缓存 -128~127之间数的Integer对象。如果调用valueOf时超出了这个范围,就会创建一个新的Integer对象
代码见package patterns.flyweight;(与黑马不同)
行为型模式
描述程序在运行时复杂的流程控制,以描述多个类或对象之间怎样相互协作共同完成单个对象无法完成的任务
类行为型模式:采用继承机制
对象行为型模式:采用组合或者聚合在对象间分配行为
模板方法模式
定义一个操作中的算法骨架(顺序),而将算法的一些步骤延迟到子类中,使得子类可以不改变算法结构的情况下重定义该算法的某些特定步骤
分为抽象类(模板方法和基本方法,基本方法又有抽象方法(取号、排队、打分)和具体方法(办理具体业务))和具体子类
提高了代码的复用性:把相同的部分代码放在抽象的父类中,把不同的代码放在父类中并声明成抽象的,要求子类去重写
符合开闭原则
适用于算法的整体步骤很固定,但其中个别部分易变
在JDK中,InputStream类使用了模版方法模式
反向控制:父类其实调用子类的方法
代码见package patterns.template;(与黑马不同)
策略模式
定义了一系列算法(如IDEA和eclipse),并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户(程序员)。属于对象行为模式,通过对算法进行封装,把使用算法的责任和算法的实现分隔开来,并委派给不同的对象对这些算法进行管理
分为抽象策略类、具体策略类和环境类(管理策略类)
策略类可以自由切换、增加一个新的策略只需要添加一个策略类即可,符合“开闭原则”
但是客户端必须知道所有的策略类,并自行决定
在JDK中,Comparator(抽象策略角色)中的Arrays类(环境类)中有个sort()方法用到了Comparator子实现类中的compare()方法
代码见package patterns.strategy;(与黑马不同)
命令模式
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分隔开。这样两者之间通过命令对象进行沟通,方便将命令对象进行存储、传递、调用、增加与管理
分为抽象命令类、具体命令类、实现者/接收者、调用者/请求者(持有一系列命令)
降低系统的耦合度,使得调用者和接收者不直接交互;增加或删除命令非常方便,满足开闭原则;方便undo和redo操作
在JDK中,Runnable是一个典型命令模式,Runnable担任命令的角色,Thread充当的是调用者
代码见package patterns.command;(与黑马不同)
责任链模式
为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止
分为抽象处理者角色、具体处理者角色、客户类角色
降低了请求发送者和请求接收者的耦合度;增加了系统的可扩展性,增加了给对象指派职责的灵活性;明确各类的职责范围,符合类的单一职责原则
在javaWeb开发中,FilterChain是职责链(过滤器模式)的典型应用
代码见package patterns.chainOfResponsibility;(与黑马不同)
状态模式
反例
问题:使用了大量的switch case这样的判断;扩展性差
对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为
分为环境角色、抽象状态角色、具体状态角色
将所有与某个状态有关的行为放到一个类中,并且可以方便的增加新的状态
但是会增加系统类和对象的个数、不满足开闭原则
代码见package patterns.state;(与黑马不同)
观察者模式
发布-订阅模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有观察者对象,使它们能够自动更新自己
分为抽象主题、具体主题(维护一个观察者列表)、抽象观察者、具体观察者
降低了目标与观察者之间的耦合关系;可以实现广播机制
在JDK中,通过java.util.Observable类(抽象主题类)和java.util.Observer接口(抽象观察者)实现了观察者模式
代码见package patterns.observer;(与黑马不同)
中介者模式
调停模式,定义了一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们的交互
分为抽象中介者角色、具体中介者角色、抽象同事类角色、具体同事类角色
星型结构
代码见package patterns.mediator;(与黑马不同)
迭代器模式
提供一个对象来顺序访问聚合对象(比如集合)中的一系列数据,而不暴露聚合对象的内部表示
分为抽象聚合角色、具体聚合角色、抽象迭代器角色、具体迭代器角色
支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式
增加新的聚合类和迭代器类都很方便,无需修改原有代码,满足“开闭原则”的要求
在JDK中, List是抽象聚合类、ArrayList是具体聚合类、Iterator时抽象迭代器、list.iterator()是具体迭代器(返回的是Iterator的子实现类)
代码见package patterns.iterator;(与黑马不同)
访问者模式
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作
分为抽象访问者角色、具体访问者角色、抽象元素角色、具体元素角色、对象结构角色(包含元素角色)
扩展性好,复用性好、分离无关行为
但是违背了开闭原则和依赖倒转原则
代码见package patterns.visitor;(与黑马不同)
扩展(双分派技术)(略)
备忘录模式
快照模式,在不破环封装性的前提下,捕获一个对象的内部状态的,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态
分为发起人角色(创建和恢复备忘录)、备忘录角色、管理者角色(保存和获取备忘录)
备忘录有两个等效的接口:
窄接口:只允许管理者把备忘录对象传给其他的对象
宽接口:允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态
- “白箱“备忘录模式
备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色对内部所存储的状态对所有对象公开
- “黑箱”备忘录模式
备忘录角色对发起人提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类
提供了一种可以恢复状态的机制;实现了内部状态的封装;发起人不需要管理和保存其内部状态的各个备份,符合单一职责原则
代码见package patterns.memento;(白箱)(与黑马不同)
解释器模式
给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子
分为抽象表达式角色(规范)、终结符表达式角色、非终结符表达式角色、环境角色、客户端
易于改变和扩展文法,增加新的解释表达式比较方便,符合开闭原则
代码见package patterns.interpreter;(与黑马不同)
黑马后面Spring框架部分此处省略