个人博客导航页(点击右侧链接即可打开个人博客):大牛带你入门技术栈
设计模式
设计模式(design pattern)是对软件设计中普遍存在的(反复出现)的各种问题,所提出的解决方案。
设计模式七大原则
-
单一职责
- 降低类的复杂度,一个类只负责一项职责
- 提高类的可读性,可维护性
- 降低变更引起的风险
- 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则
-
接口隔离
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
-
依赖倒转(倒置)Dependence Inversion Principle
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转(倒置)的中心思想是面向接口编程
- 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
注意:
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好
- 变量的生命类型尽量是抽象类或接口,这样我们的变量引用和实际对象之间就存在一个缓冲层,利于程序扩展和优化
- 继承时遵循里氏替换原则
-
里氏替换
- 里氏替换原则( Liskov Substitution Principle)在1988年,由麻省理工学院的以为姓里的女士提出的
- 如果对每个类型为T的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象
- 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法
- 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题
-
开闭原则 ocp
- 开闭原则( Open Closed Principle)是编程中最基础、最重要的设计原则
- 一个软件实体,如类、模块和函数,应该对扩展开放(提供方),对修改关闭(使用方)。用抽象构建框架,用实现扩展细节
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
- 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则
-
迪米特法则
- 一个对象应该对其他对象保持最少的了解
- 类与类关系越密切,耦合度越大
- 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息
- 迪米特法则还有个更简单的定义:只与直接的朋友通信
- 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部
注意:
- 迪米特法则的核心是降低类之间的耦合
- 由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)的耦合关系,并不是要求完全没有依赖关系
-
合成复用
尽量使用合成/聚合的方式,而不是使用继承
设计模式的目的
- 代码重用性:相同代码不用多次编写
- 可读性:编码规范性
- 可扩展性
- 可靠性:新增加的功能对原来的功能没有影响
- 使程序高内聚、低耦合
设计原则的核心思想
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
- 针对接口编程,而不是针对实现编程
- 为了交互对象之间的松耦合设计而努力
UML 类图
-
UML 内容见网络
-
类之间的关系:依赖、泛化(继承)、实现、关联、聚合、组合
-
依赖
- 类中用到了对方
- 如果是类的成员属性
- 如果是方法的返回类型
- 如果是方法接收的参数类型
- 方法中使用到
-
泛化(继承)
- 实际上就是 A 类继承 B 类,是依赖关系的特例
-
实现
- 实际上就是 A 类实现 B 类,是依赖关系的特例
-
关联
- 实际上就是类与类之间的关系,是依赖关系的特例
- 关联具有导航性,即双向关系或单向关系
- 关联具有多重性
- “1” 有且仅有一个
- “0...” 0个或多个
- “0,1” 0个或1个
- “n...m” n到m个
- “m...*” 至少m个
-
聚合(Aggregation)
- 表示整体和部分的关系,整体与部分可以分开。聚合关系是关联关系的特例,所以它具有关联的导航性与多重性
-
组合(Composition)
- 整体与部分的关系,但是整体与部分不可以分开
- 如果程序中 A 类与 B 类符合聚合条件,但 A 类定义了对 B 类的级联操作,如级联删除,那么 A 和 B 就是组合
-
设计模式概述
掌握层次
- 第1层:刚开始学编程不久,听说过什么是设计模式
- 第2层:有很长时间的编程经验,自己写了很多代码,其中用到了设计模式,但是自己却不知道
- 第3层:学习过了设计模式,发现自己已经在使用了,并且发现了一些新的模式挺好用的
- 第4层:阅读了很多别人写的源码和框架,在其中看到别人设计模式,并且能够领会设计模式的精妙和带来的好处
- 第5层:代码写着写着,自己都没有意识到使用了设计模式,并且熟练的写了出来
设计模式介绍
- 设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模式不是代码,而是某类问题的通用解决方案,设计模式(Design pattern)代表了最佳的实践。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的
- 设计模式的本质提高软件的维护性、通用性和扩展性,并降低软件的复杂
- 《设计模式》是经典的书,作者是 Erich Gamma、 Richard helm、 Ralph Johnson 和 John Vlissides Design(俗称“四人组GOF”)
- 设计模式并不局限于某种语言,java、php、c++ 等都有设计模式
设计模式分类
分为三种类型,共 23 种,不同书籍对分类和名称略有差别
- 创建型:单例模式、工厂模式、抽象工厂模式、原型模式、建造者模式
- 结构型:适配器模式、桥接模式、装饰模式、组台模式、外观模式、享元模式、代理模式
- 行为型:模版方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)
单例模式 Singleton
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)
比如 Hibernate 的 SessionFactory,它充当数据存储源的代理,并负责创建 Session对象。 SessionFactory 并不是轻量级的,一般情况下,一个项目通常只需要一个 SessionFactory 就够,这时就会使用到单例模式
单例模式分类
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)
- 懒汉式(线程安全,同步代码块)
- 双重检查
- 静态内部类
- 枚举
饿汉式(静态常量)
操作步骤
-
构造器私有化(防止 new)
-
类的内部创建对象
-
向外暴露一个静态的公共方法(如 getInstance)
-
实现代码
class Singleton { private Singleton(){} private final static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } }
优缺点
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题
- 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 这种方式基于 classloader 机制避了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 Instance 就没有达到 lazy loading 的效果
- 结论:这种单例模式可用,可能造成内存浪费
饿汉式(静态代码块)
操作步骤
-
构造器私有化(防止 new)
-
类的内部使用静态代码块创建对象
-
向外暴露一个静态的公共方法(如 getInstance)
-
实现代码
class Singleton { private Singleton(){} private final static Singleton instance; static { instance = new Singleton(); } public static Singleton getInstance() { return instance; } }
优缺点
同上
懒汉式(线程不安全)
操作步骤
-
构造器私有化(防止 new)
-
向外暴露一个静态的公共方法(如 getInstance),内部判断是否已经实例化对象,如果已经实例化则直接返回,否则,先实例化再返回
-
实现代码
class Singleton { private Singleton(){} private static Singleton instance; public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
优缺点
- 线程不安全
- 结论:在实际开发中,不要使用这种方式
懒汉式(线程安全,同步方法)
操作步骤
-
构造器私有化(防止 new)
-
向外暴露一个同步静态的公共方法(如 getInstance),内部判断是否已经实例化对象,如果已经实例化则直接返回,否则,先实例化再返回
-
实现代码
class Singleton { private Singleton(){} private static Singleton instance; public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
优缺点
- 解决了线程不安全问题
- 效率太低了,每个线程在想获得类的实例的时候,执行 getInstance() 方法都要进行同步。 而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例直接 return 就行了。该方法进行同步效率太低
- 结论:在实际开发中,不推荐使用这种方式
懒汉式(线程安全,同步代码块)
操作步骤
-
构造器私有化(防止 new)
-
向外暴露一个静态的公共方法(如 getInstance),内部判断是否已经实例化对象,如果已经实例化则直接返回,否则,先同步实例化再返回
-
实现代码
class Singleton { private Singleton(){} private static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; } }
优缺点
- 线程不安全
- 这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致, 假如一个线程进入了 if (instance == null) 判断语句块,还未来得及往下执行, 另一个线程也通过了这个判断语句,这时便会产生多个实例
- 结论:在实际开发中,不能使用这种方式
双重检查
操作步骤
-
构造器私有化(防止 new)
-
创建静态 volatile 实例(此时实例化)
-
向外暴露一个静态的公共方法(如 getInstance),内部判断是否已经实例化对象, 如果已经实例化则直接返回,否则,在同步代码块内判断是否实例化并选择是否实例化, 最后返回
-
实现代码
class Singleton { private Singleton(){} private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
优缺点
- Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (instance == null) 检查,这样就可以保证线程安全了。
- 这样,实例化代码只用执行一次,后面再次访问时,判断 if (instance == null) ,直接 return 实例化对象,也避免的反复进行方法同步
- 线程安全、延迟加载、效率较高
- 结论:在实际开发中,推荐使用这种单例设计模式
静态内部类
操作步骤
-
构造器私有化(防止 new)
-
创建静态内部类,该类中有一个静态属性 Singleton
-
向外暴露一个同步静态的公共方法(如 getInstance),返回内部类实例化对象
-
实现代码
class Singleton { private Singleton(){} private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static synchronized Singleton getInstance() { return SingletonInstance.INSTANCE; } }
优缺点
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程
- 静内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时, 调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton的实例化
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性, 在类进行初始化时,别的线程是无法进入的
- 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 结论:推荐使用
枚举
-
实现代码
enum Singleton { INSTANCE; public void ok() {} } public class Test { public static void main(String[] args) { Singleton instance = Singleton.INSTANCE; instance.ok(); } }
优缺点
- 这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题, 而且还能防止反序列化重新创建新的对象
- 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式
- 结论:推荐使用
工厂模式
简单工厂模式(静态工厂模式 SimpleFactory)
介绍
- 简单工厂模式是属于创建型模式,是工厂模式的一种。 简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。 简单工厂模式是工厂模式家族中最简单实用的模式
- 简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为(代码)
- 在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式
工厂方法模式
介绍
- 工厂方法模式是定义一个创建对象的抽象方法,由子类决定要实例化的类。 工厂方法模式将对象的实例化推迟到子类。
抽象工厂模式
介绍
- 抽象工厂模式:定义了一个 interface 用于创建相关或有依赖关系的对象簇,而无需指明具体的类
- 抽象工厂模式可以将简单工厂模式和工厂方法模式进行整合
- 从设计层面看,抽象工厂模式就是对简单工厂模式的改进,或者称为进一步的抽象
- 将工厂抽象成两层, AbsFactory(抽象工厂)和具体实现的工厂子类。 程序员可以根据创建对象类型使用对应的工厂子类。 这样将单个的简单工厂类变成了工厂簇,更利于代码的维护和扩展
工厂模式小结
- 工厂模式的意义: 将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。 从而提高项目的扩展和维护性
- 三种工厂模式
- 设计模式的依赖抽象原则
- 创建对象实例时,不要直接 new 类,而是把这个 new 类的动作放在一个工厂的方法中,并返回。 有的书上说,变量不要直接持有具体类的引用
- 不要让类继承具体类,而是继承抽象类或者是实现 interface
- 不要覆盖基类中已经实现的方法
原型模式 Prototype
基本介绍
- 原型模式是指:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象
- 原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节
- 工作原理:通过将一个原型对象传给那个要发动创建的对象,原型对象实现 Cloneable 接口, 这个要发动创建的对象通过请求原型对象,拷贝它们自己来实施创建,即 obj.clone()
- 形象的理解:孙大圣拔出猴毛,变出其它孙大圣
浅拷贝
- 对于数据类型是基本数据类型变量,浅拷贝会直接进行值传递, 也就是将该属性值复制一份给新的对象
- 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等, 那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。 因为实际上两个对象的该成员变量都指向同一个实例。 在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
- 浅拷贝是使用默认的 clone() 方法来实现, 例:
Sheep sheep = (Sheep) super.clone()
深拷贝
- 复制对象的所有基本数据类型的成员变量值
- 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。 也就是说,对象进行深拷贝要对整个对象进行拷贝
- 深拷贝实现方式1:重写 clone() 方法来实现深拷贝
- 深拷贝实现方式2:通过对象序列化实现深拷贝
扩展
- java clone 浅拷贝和深拷贝
- 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容(推荐)
建造者模式 Builder
基本介绍
- 建造者模式又叫生成器模式,是一种对象构建模式。 它可以将复杂对象的建造过程抽象出来(抽象类别), 使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象
- 建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象 的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节
- 建造者模式的四个角色
- Product(产品角色):一个具体的产品对象
- Builder(抽象建造者):创建一个 Product 对象的各个部件指定的接口或抽象类
- Concrete Builder(具体建造者):实现接口,构建和装配各个部件。
- Director(指挥者):构建一个使用 Builder 接口或抽象类的对象。它主要是用于创建一个复杂的对象。 它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程
建造者模式的注意事项和细节
- 客户端(使用程序)不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦, 使得相同的创建过程可以创建不同的产品对象
- 每一个具体建造者都相对独立,而与其他的具体建造者无关, 因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象
- 可以更加精细地控制产品的创建过程。 将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程
- 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似, 如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化, 导致系统变得很庞大,因此在这种情况下,要考虑是否选择建造者模式
- 抽象工厂模式 vs 建造者模式
- 抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品: 具有不同分类维度的产品组合,采用抽象工厂模式不需要关心构建过程,只关心什么产品由什么工厂生产即可
- 建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品
适配器模式 Adapter
基本介绍
- 适配器模式,也叫包装器(Wrapper),将某个类的接囗转换成客户端期望的另一个接口表示, 主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作
- 适配器模式属于结构型模式
- 主要分为三类:类适配器模式、对象适配器模式、接口适配器模式
工作原理
- 适配器模式:将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容
- 从用户的角度看不到被适配者,是解耦的
- 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法
- 用户收到反馈结果,感觉只是和目标接口交互
类适配器模式
介绍
Adapter类,通过继承 src 类,实现 dst 类接口,完成 src->dst 的适配
对象适配器模式
介绍
- 基本思路和类的适配器模式相同,只是将 Adapter 类作修改,不是继承 src 类, 而是持有 src 类的实例,以解决兼容性的问题。 即:持有src类,实现dst类接口,完成 src->dst 的适配
- 根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系
- 对象适配器模式是适配器模式常用的一种
注意事项
- 对象适配器和类适配器其实算是同一种思想,只不过实现方式不同
- 根据合成复用原则,使用组合替代继承,所以它解决了类适配器必须继承 src 的局限性问题,也不再要求 dst 必须是接口
- 使用成本更低、更灵活
接口适配器模式
介绍
- 一些书籍称为:适配器模式(Default Adapter Pattern)或缺省适配器模式
- 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法), 那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求
- 适用于一个接口不想使用其所有的方法的情况
注意事项和细节
- 三种命名方式,是根据 src 是以怎样的形式给到 Adapter(在 Adapter 里的形式)来命名的
- 类适配器:以类给到,在 Adapter 里,就是将 src 当做类,继承
- 对象适配器:以对象给到,在 Adapter 里,将 src 作为一个对象,持有
- 接口适配器:以接口给到,在 Adapter 里,将 src 作为一个接口,实现
- Adapter 模式最大的作用是将原本不兼容的接口融合在一起工作
- 实际开发中,实现起来不拘泥于我们讲解的三种经典形式
桥接模式 Bridge
介绍
- 桥接模式是指:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变
- 是一种结构型设计模式
- 桥接模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责。 它的主要特点是把抽象(Abstraction)与行为实现(Implementation)分离开来, 从而可以保持各部分的独立性以及应对他们的功能扩展
注意事项
- 实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来, 这有助于系统进行分层设计,从而产生更好的结构化系统
- 对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了,其它的部分由具体业务来完成
- 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本
- 桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围有一定的局限性。 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为使用
装饰者模式 Decorator
介绍
- 装饰者模式:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,也体现了开闭原则(OCP)
组合模式 Composite
介绍
- 组合模式,又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示“整体部分”的层次关系
- 组合模式依据树形结构来组合对象,用来表示部分以及整体层次
- 这种类型的设计模式属于结构型模式
- 组合模式使得用户对单个对象和组合对象的访问具有一致性, 即:组合能让客户以一致的方式处理个别对象以及组合对象
注意事项和细节
- 简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题
- 具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系,客户端不用做出任何改动
- 方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容易添加节点或者叶子,从而创建出复杂的树形结构
- 需要遍历组织机构,或者处理的对象具有树形结构时,非常适合使用组合模式
- 要求较高的抽象性,如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
外观模式 Facade
介绍
- 外观模式,也叫“过程模式:外观模式为子系统中的一組接口提供一个一致的界面, 此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
- 外观模式通过定义一个一致的接口,用以屏蔽内部子系统的细节, 使得调用端只需跟这个接口发生调用,而无需关心这个子系统的内部细节
注意事项和细节
- 外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
- 外观模式对客户端与子系统的耦合关系,让子系统内部的模块更易维护和扩展
- 通过合理的使用外观模式,可以帮我们更好的划分访问的层次
- 当系统需要进行分层设计时,可以考虑使用 Facade 模式
- 在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个 Facade 类, 来提供遗留系统的比较清晰简单的接囗,让新系统与 Facade 类交互,提高复用性
- 不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好, 要以让系统有层次、利于维护为目的
享元模式 Flyweight
介绍
- 享元模式,也叫蝇量模式:运用共享技术有效地支持大量细粒度的对象
- 常用于系统底层开发,解决系统的性能问题。 像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿来用,避免重新创建。 如果没有我们需要的,则创建一个
- 享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时, 不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率
- 享元模式经典的应用场景就是池技术, String 常量池、数据库连接池、缓冲池等等都是享元模式 的应用,享元模式是池技术的重要实现方式
- 享元模式提出了两个要求:细粒度和共享对象。这里就涉及到内部状态和外部状态, 即将对象的信息分为两个部分:内部状态和外部状态
- 内部状态:对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变
- 外部状态:对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态
代理模式 Proxy
介绍
- 代理模式,为一个对象提供一个替身,以控制对这个对象的访问,即通过代理对象访问目标对象。 这样做的好处是可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能
- 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象
- 代理模式有不同的形式,主要有三种
- 静态代理
- 动态代理(JDK代理、接口代理)
- Cglib 代理(可以在内存动态的创建对象,而不需要实现接口,属于动态代理)
静态代理
- 静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同的父类
动态代理
- 代理对象不需要实现接口,但是目标对象要实现接口,否则不能用动态代理
- 代理对象的生成,是利用 JDK 的 API,动态的在内存中构建代理对象
- 动态代理也叫做:JDK代理、接口代理
Cglib 代理
- 静态代理和 JDK 代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象, 并没有实现任何的接口,这个时候可使用目标对象子类来实现代理,这就是 Cglib 代理
- Cglib 代理也叫作子类代理,它是在内存中构建一个子类对象,从而实现对目标对象功能扩展,有些书也将 Cglib 代理归属到动态代理
- Cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口。 它广泛的被许多 AOP 的框架使用,例如 Spring AOP 实现方法拦截
- 在 AOP 编程中如何选择代理模式
- 目标对象需要实现接口,用 JDK 代理
- 目标对象不需要实现接口,用 Cglib 代理
- Cglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类
- 需要引入 Cglib jar
- 在内存汇总动态构建子类,注意代理的类不能为 final,否则报错 java.lang.IllegalArgumentException
- 若目标对象的方法为 final/static,那么久不会被拦截,即不会执行目标对象额外的业务方法
代理模式的变体
- 防火墙代理:内网通过代理穿透防火墙,实现对公网的访问
- 缓存代理:当请求图片文件等资源时,先从缓存代理获取,若获取不到,再到公网或数据库取,然后缓存
- 远程代理:远程对象的本地代表,通过它可以把远程对象当本地对象来调用。远程代理通过网络和真正的远程对象沟通信息
- 同步代理:主要使用在多线程编程中,完成多线程间同步工作
模板方法模式 Template Method
介绍
- 模板方法模式,又叫模板模式(Template Pattern),在一个抽象类公开定义了执行它的方法的模板。 它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行
- 简单说,模板方法模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤
- 这种类型的设计模式属于行为型模式
模板方法模式的钩子方法
- 在模板方法模式的父类中,可以定义一个方法,它默认不做任何事,子类可以视情况覆盖它,该方法被称为“钩子”
命令模式 Command
介绍
- 命令模式:在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个, 我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计
- 命名模式使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活
- 在命名模式中,会将一个请求封装为一个对象,以便使用不同参数来表示不同的请求(即命名),同时命令模式也支持可撤销的操作
- 通俗易懂的理解:将军发布命令,士兵去执行。 其中有几个角色:将军(命令发布者)、士兵(命令的具体执行者)、命令(连接将军和士兵)
注意事项和细节
- 将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象的 execute() 方法就可以让接收者工作, 而不必知道具体的接收者对象是谁、是如何实现的,命令对象会负责让接收者执行请求的动作, 也就是说:“请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到纽带桥梁的作用
- 容易设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令
- 容易实现对请求的撤销和重做
- 命令模式不足:可能导致某些系统有过多的具体命令类,增加了系统的复杂度,这点在在使用的时候要注意
- 空命令也是一种设计模式,它为我们省去了判空的操作
- 命令模式经典的应用场景
- 界面的一个按钮都是一条命令
- 模拟CMD(DS命令)
- 订单的撤销/恢复、触发-反馈机制
访问者模式 Visitor
介绍
- 访问者模式,封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作
- 主要将数据结构与数据操作分离,解决数据结构和操作耦合性问题
- 访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
- 访问者模式主要应用场景是:需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联), 同时需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决
- 用到“双分派”
注意事项和细节
- 优点
- 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
- 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统
- 缺点
- 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的,这样造成了具体元素变更比较困难
- 违背了依赖倒转原则,访问者依赖的是具体元素,而不是抽象元素
- 如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的
迭代器模式 Iterator
介绍
- 迭代器模式是常用的设计模式,属于行为型模式
- 如果我们的集合元素是用不同的方式实现的,有数组,还有 java 的集合类或者还有其他方式, 当客户端要遍历这些集合元素的时候就要使用多种遍历方式,而且还会暴露元素的内部结构,可以考虑使用迭代器模式解决
- 迭代器模式,提供一种遍历集合元素的统一接口,用一致的方法遍历集合元素, 不需要知道集合对象的底层表示,即:不暴露其内部的结构
注意事项和细节
- 优点
- 提供一个统一的方法遍历对象,客户不用再考虑聚合的类型,使用一种方法就可以遍对象
- 隐藏了聚合的内部结构,客户端要遍历聚合的时侯只能取到迭代器,而不会知道聚合的具体组成
- 提供了一种设计思想,就是一个类应该只有一个引起变化的原因(叫做单一责任原则)。 在聚合类中,我们把迭代器分开,就是要把管理对象集合和遍历对象集合的责任分开,这样一来集合改变的话,只影响到聚合对象。 而如果遍历方式改变的话,只影响到了迭代器
- 当要展示一组相似对象,或者遍历一组相同对象时,适合使用迭代器模式
- 缺点
- 每个聚合对象都要一个迭代器,会生成多个迭代器
观察者模式 Observer
中介者模式 Mediator
介绍
- 中介者模式,用一个中介对象来封装一系列的对象交互。 中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
- 中介者模式属于行为型模式,使代码易于维护
- 比如 MVC 模式,C(Controller控制器)是 M(Mode模型)和 V(view视图)的中介者,在前后端交互时起到了中间人的作用
注意事项和细节
- 多个类相互耦合,会形成网状结构,使用中介者模式将网状结构分离为星型结构进行解耦
- 减少类间依赖,降低了耦合,符合迪米特原则
- 中介者承担了较多的责任,一旦中介者出现了问题,整个系统就会受到影响
- 如果设计不当,中介者对象本身变得过于复杂,这点在实际使用时要特别注意
备忘录模式 Memento
介绍
- 备忘录模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态
- 可以这样理解备忘录模式: 现实生活中的备忘录是用来记录某些要去做的事情或者是记录已经达成的共同意见的事情,以防忘记。 而在软件层面,备忘录模式有着相同的含义,备忘录对象主要用来记录一个对象的某种状态,或者某些数据, 当要做回退时,可以从备忘录对象里获取原来的数据进行恢复操作
- 备忘录模式属于行为型模式
解释器模式 Interpreter
介绍
- 在编译原理中,一个算术表达式通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器构建语法分析树, 最终形成一颗抽象的语法分析树。这里的词法分析器和语法分析器都可以看做是解释器
- 解释器模式是指给定一个语言(表达式),定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子(表达式)
- 应用场景
- 应用可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
- 一些重复出现的问题可以用一种简单的语言来表达
- 一个简单语法需要解释的场景
- 这样的例子还有,比如编译器、运算表达式计算、正则表达式、机器人等
注意事项和细节
- 当有一个语言需要解释执行,如果可将该语言中的句子表示为一个抽象语法树,就可以考虑使用解释器模式,让程序具有良好的扩展性
- 应用场景:编译器、运算表达式计算、正则表达式、机器人等
- 使用解释器可能带来的问题:解释器模式会引起类膨胀、解释器模式采用递归调用方法,将会导致调试非常复杂、效率可能降低
状态模式 State
介绍
- 状态模式主要用来解决对象在多种状态转换时,需要对外输出不同的行为的问题。状态和行为是一一对应的,状态之间可以相互转换
- 当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类
注意事项和细节
- 代码有很强的可读性。状态模式将每个状态的行为封装到对应的一个类中
- 方便维护。将容易产生问题的else语句删除了,如果把每个状态的行为都放到一个类中,每次调用方法时都要判断当前是什么状态,不但会产出很多else语句,而且容易出错
- 符合“开闭原则”。容易增删状态
- 会产生很多类。每个状态都要一个对应的类,当状态过多时会产生很多类,加大维护难度
- 应用场景:当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候,可以考虑使用状态模式
策略模式 Strategy
介绍
- 策略模式中,定义算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户
- 该算法体现了几个设计原则
- 把变化的代码从不变的代码中分离出来
- 针对接口编程而不是具体类(定义了策略接口)
- 多用组合/聚合,少用继承(客户通过组合方式使用策略)
注意事项和细节
- 策略模式的关键是:分析项目中变化部分与不变部分
- 策略模式的核心思想是:多用组合/聚合少用继承;用行为类组合,而不是行为的继承更有弹性
- “对修改关闭,对扩展开放”原则,客户端增加行为不用修改原有代码,只要添加一种策略(或者行为)即可,避免了使用多重转移语句(if.else)
- 提供了可以替换继承关系的办法:策略模式将算法封装在独立的 Strategy 类中使得你可以独立于其 Context 改变它,使它易于切换、易于理解、易于扩展
- 需要注意的是:每添加一个策略就要增加一个类,当策略过多是会导致类数目庞大
职责链模式 Chain of Responsibility
介绍
- 职责链模式又叫责任链模式,为请求创建了一个接收者对象的链。这种模式对请求的发送者和接收者进行解耦
- 职责链模式通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依次类推
注意事项和细节
- 将请求和处理分开,实现解耦,提高系统的灵活性
- 简化了对象,使对象不需要知道链的结构
- 性能会受到影响,特别是在链比较长的时候,因此需控制链中最大节点数量,一般通过在 Handler 中设置一个最大节点数量, 在 setNext() 方法中判断是否已经超过阀值,超过则不允许该链建立,避免出现超长链无意识地破坏系统性能
- 测试不方便。采用了类似递归的方式,调试时逻辑可能比较复杂
- 最佳应用场景:有多个对象可以处理同一个请求时,比如:多级请求、请假/加薪等审批流程、 Java Web 中 Tomcat 对 Encoding 的处理、拦截器
附Java/C/C++/机器学习/算法与数据结构/前端/安卓/Python/程序员必读/书籍书单大全:
(点击右侧 即可打开个人博客内有干货):技术干货小栈
=====>>①【Java大牛带你入门到进阶之路】<<====
=====>>②【算法数据结构+acm大牛带你入门到进阶之路】<<===
=====>>③【数据库大牛带你入门到进阶之路】<<=====
=====>>④【Web前端大牛带你入门到进阶之路】<<====
=====>>⑤【机器学习和python大牛带你入门到进阶之路】<<====
=====>>⑥【架构师大牛带你入门到进阶之路】<<=====
=====>>⑦【C++大牛带你入门到进阶之路】<<====
=====>>⑧【ios大牛带你入门到进阶之路】<<====
=====>>⑨【Web安全大牛带你入门到进阶之路】<<=====
=====>>⑩【Linux和操作系统大牛带你入门到进阶之路】<<=====天下没有不劳而获的果实,望各位年轻的朋友,想学技术的朋友,在决心扎入技术道路的路上披荆斩棘,把书弄懂了,再去敲代码,把原理弄懂了,再去实践,将会带给你的人生,你的工作,你的未来一个美梦。