总体分为三大类:
创建型模式:工厂方法模式、单例模式。
结构型模式:适配器模式、代理模式、装饰器模式。
行为型模式:观察者模式、策略模式、模板方法模式。
一、简单工厂模式(Simple Factory)
概述:
工厂模式就是工厂负责创建对象,使用者不关心对象的创建过程;简单工厂模式就是定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类或实现了共同的接口。在简单工厂模式中用于创建对象的方法通常都是静态(static)的,因此简单工厂模式又被称为静态工厂方法。
简单工厂模式有一个具体的工厂类,可以生成多个不同的产品,属于创建型设计模式。简单工厂模式不在 GoF 23 种设计模式之列。在这种模式下每增加一个产品都要对工厂类进行修改,这增加了系统的复杂度,违背了“开闭原则”。
应用场景:
对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。
优点:
1-工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确;
2-客户端无需知道所创建具体产品的类名,只需知道参数即可;
3-也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类;
缺点:
1-简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则;
2-使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度;
3-系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂;
4-简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构;
主要角色
简单工厂(Simple Factory):是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象;
抽象产品(Product):是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口;
具体产品(Concrete Product):是简单工厂模式的创建目标;
代码案例
二、工厂方法模式(Factory Method)
介绍
1.1 定义
工厂方法模式,又称工厂模式、多态工厂模式和虚拟构造器模式,通过定义工厂父类负责定义创建对象的公共接口,而子类则负责生成具体的对象。
1.2 主要作用
将类的实例化(具体产品的创建)延迟到工厂类的子类(具体工厂)中完成,即由子类来决定应该实例化(创建)哪一个类。
1.3 解决的问题
工厂一旦需要生产新产品就需要修改工厂类的方法逻辑,违背了“开放 - 关闭原则
之所以可以解决简单工厂的问题,是因为工厂方法模式把具体产品的创建推迟到工厂类的子类(具体工厂)中,此时工厂类不再负责所有产品的创建,而只是给出具体工厂必须实现的接口,这样工厂方法模式在添加新产品的时候就不修改工厂类逻辑而是添加新的工厂子类,符合开放封闭原则,克服了简单工厂模式中缺点。
模式原理
模式组成
使用步骤
步骤1: 创建抽象工厂类,定义具体工厂的公共接口;
步骤2: 创建抽象产品类 ,定义具体产品的公共接口;
步骤3: 创建具体产品类(继承抽象产品类) & 定义生产的具体产品;
步骤4:创建具体工厂类(继承抽象工厂类),定义创建对应具体产品实例的方法;
步骤5:外界通过调用具体工厂类的方法,从而创建不同具体产品类的实例;
实例讲解
用一个实例来对工厂方法模式进行更深一步的介绍。
实例概况
- 背景:小成有一间塑料加工厂(仅生产A类产品);随着客户需求的变化,客户需要生产B类产品;
- 冲突:改变原有塑料加工厂的配置和变化非常困难,假设下一次客户需要再发生变化,再次改变将增大非常大的成本;
- 解决方案:小成决定置办塑料分厂B来生产B类产品;即工厂方法模式
优点
-
更符合开-闭原则
新增一种产品时,只需要增加相应的具体产品类和相应的工厂子类即可简单工厂模式需要修改工厂类的判断逻辑
-
符合单一职责原则
每个具体工厂类只负责创建对应的产品简单工厂中的工厂类存在复杂的switch逻辑判断
-
不使用静态工厂方法,可以形成基于继承的等级结构。
简单工厂模式的工厂类使用静态工厂方法
总结:工厂模式可以说是简单工厂模式的进一步抽象和拓展,在保留了简单工厂的封装优点的同时,让扩展变得简单,让继承变得可行,增加了多态性的体现。
缺点
- 添加新产品时,除了增加新产品类外,还要提供与之对应的具体工厂类,系统类的个数将成对增加,在一定程度上增加了系统的复杂度;同时,有更多的类需要编译和运行,会给系统带来一些额外的开销;
- 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
- 虽然保证了工厂方法内的对修改关闭,但对于使用工厂方法的类,如果要更换另外一种产品,仍然需要修改实例化的具体工厂类;
- 一个具体工厂只能创建一种具体产品
应用场景
在了解了优缺点后,我总结了工厂方法模式的应用场景:
- 当一个类不知道它所需要的对象的类时
在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可; - 当一个类希望通过其子类来指定创建对象时
在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。 - 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
使用步骤
step 1
创建抽象工厂类,定义具体工厂的公共接口
abstract class Factory{public abstract Product Manufacture();
}
step 2
创建抽象产品类 ,定义具体产品的公共接口;
abstract class Product{public abstract void Show();
}
step 3
创建具体产品类(继承抽象产品类), 定义生产的具体产品;
//具体产品A类
class ProductA extends Product{@Overridepublic void Show() {System.out.println("生产出了产品A");}
}//具体产品B类
class ProductB extends Product{@Overridepublic void Show() {System.out.println("生产出了产品B");}
}
step 4
创建具体工厂类(继承抽象工厂类),定义创建对应具体产品实例的方法;
//工厂A类 - 生产A类产品
class FactoryA extends Factory{@Overridepublic Product Manufacture() {return new ProductA();}
}//工厂B类 - 生产B类产品
class FactoryB extends Factory{@Overridepublic Product Manufacture() {return new ProductB();}
}
step 5
外界通过调用具体工厂类的方法,从而创建不同具体产品类的实例;
//生产工作流程
public class FactoryPattern {public static void main(String[] args){//客户要产品AFactoryA mFactoryA = new FactoryA();mFactoryA.Manufacture().Show();//客户要产品BFactoryB mFactoryB = new FactoryB();mFactoryB.Manufacture().Show();}
}
step 6
执行程序,输出结果:
生产出了产品A
生产出了产品C
三、单例模式(Singleton)
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
概要
意图:确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
主要解决:频繁创建和销毁全局使用的类实例的问题。
何时使用:当需要控制实例数目,节省系统资源时。
如何解决:检查系统是否已经存在该单例,如果存在则返回该实例;如果不存在则创建一个新实例。
关键代码:构造函数是私有的。
应用实例:
1、一个班级只有一个班主任。
2、Windows 在多进程多线程环境下操作文件时,避免多个进程或线程同时操作一个文件,需要通过唯一实例进行处理。
3.设备管理器设计为单例模式,例如电脑有两台打印机,避免同时打印同一个文件。
优点:
- 内存中只有一个实例,减少内存开销,尤其是频繁创建和销毁实例时(如管理学院首页页面缓存)。
- 避免资源的多重占用(如写文件操作)。
缺点:
- 没有接口,不能继承。
- 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心实例化方式。
使用场景:
- 生成唯一序列号。
- WEB 中的计数器,避免每次刷新都在数据库中增加计数,先缓存起来。
- 创建消耗资源过多的对象,如 I/O 与数据库连接等。
注意事项:
- 线程安全:
getInstance()
方法中需要使用同步锁synchronized (Singleton.class)
防止多线程同时进入造成实例被多次创建。 - 延迟初始化:实例在第一次调用
getInstance()
方法时创建。 - 序列化和反序列化:重写
readResolve
方法以确保反序列化时不会创建新的实例。 - 反射攻击:在构造函数中添加防护代码,防止通过反射创建新实例。
- 类加载器问题:注意复杂类加载环境可能导致的多个实例问题。
结构:
单例模式包含以下几个主要角色:
- 单例类:包含单例实例的类,通常将构造函数声明为私有。
- 静态成员变量:用于存储单例实例的静态成员变量。
- 获取实例方法:静态方法,用于获取单例实例。
- 私有构造函数:防止外部直接实例化单例类。
- 线程安全处理:确保在多线程环境下单例实例的创建是安全的。
实现
创建一个 SingleObject 类。SingleObject 类有它的私有构造函数和本身的一个静态实例。
SingleObject 类提供了一个静态方法,供外界获取它的静态实例。SingletonPatternDemo 类使用 SingleObject 类来获取 SingleObject 对象。
step1:创建一个 Singleton 类。
public class SingleObject {//创建 SingleObject 的一个对象private static SingleObject instance = new SingleObject();//让构造函数为 private,这样该类就不会被实例化private SingleObject(){}//获取唯一可用的对象public static SingleObject getInstance(){return instance;}public void showMessage(){System.out.println("Hello World!");}
}
step2:从 singleton 类获取唯一的对象。
public class SingletonPatternDemo {public static void main(String[] args) {//不合法的构造函数//编译时错误:构造函数 SingleObject() 是不可见的//SingleObject object = new SingleObject();//获取唯一可用的对象SingleObject object = SingleObject.getInstance();//显示消息object.showMessage();}
}
用单例模式的目的:
public class Printer {private static Printer printer =null;//创建一个私有的全局变量/** 如果有多线程并发访问时,上锁,让其排队等候,一次只能一人用。*/public static synchronized Printer getPrinter(){if(printer==null){//如果为空,创建本实例printer = new Printer();}return printer;}/** 构造私有化,保证在系统的使用中,只有一个实例*/private Printer(){}
}
从代码上来分析,单例模式,首先向外提供了一个可被访问的实例化的对象,如果没有此对象时,该printer类创建一个。如果遇到多线程并发访问,加上关键字Synchronized,上锁让没有持有该对象的类处于等待状态。当前持有该printer的线程任务结束之后,处于等待中的线程才能逐个去持有该实例,去操作其方法。这样的一个过程在编程中被称为单例模式。
如果在系统中不使用单例模式的话,在碰到多线程访问的时候,printer就会给要请求的类,分别在内存中new出一个printer对象,让这些请求的类去做print方法。这样大量占有内存,就会导致系统运行变慢,像电脑的CPU一样,占有量极高,电脑卡死不动的感觉。因为系统的硬件设施需求变动量小,所以只能想出一个节约成本 的方法就是,单例模式,让多线程处于等待的状态,一个 一个的去解决,这样,即可节约内存,降低运行的成本。也就是单例存在的意义。
单例模式的几种实现方式
1、懒汉式,线程不安全
是否 Lazy 初始化:是
是否多线程安全:否
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加synchronized锁 ,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}
接下来介绍的几种实现方式都支持多线程,但是在性能上有所差异。
2、懒汉式,线程安全
是否 Lazy 初始化:是
是否多线程安全:是
描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}
3、饿汉式
是否 Lazy 初始化:否
是否多线程安全:是
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){}public static Singleton getInstance() { return instance; }
}
4、双检锁/双重校验锁(DCL,即 double-checked locking)
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多线程安全:是
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }
}
5、登记式/静态内部类
是否 Lazy 初始化:是
是否多线程安全:是
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; }
}
6、枚举
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多线程安全:是
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum Singleton { INSTANCE; public void whateverMethod() { }
}
单例模式总结:
一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。
四、适配器模式(Adapter)
适配器模式(Adapter Pattern)充当两个不兼容接口之间的桥梁,属于结构型设计模式。它通过一个中间件(适配器)将一个类的接口转换成客户期望的另一个接口,使原本不能一起工作的类能够协同工作。
这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。
假设有一个音频播放器,它只能播放 MP3 文件。现在,我们需要播放 VLC 和 MP4 文件,可以通过创建一个适配器来实现:
- 目标接口:定义一个可以播放多种格式文件的音频播放器接口。
- 适配者类:现有的音频播放器,只能播放 MP3 文件。
- 适配器类:创建一个新的类,实现目标接口,并在内部使用适配者类来播放 MP3 文件,同时添加对 VLC 和 MP4 文件的支持。
概述
适配器模式是一种软件设计模式,旨在解决不同接口之间的兼容性问题。
目的:将一个类的接口转换为另一个接口,使得原本不兼容的类可以协同工作。
主要解决的问题:在软件系统中,需要将现有的对象放入新环境,而新环境要求的接口与现有对象不匹配。
使用场景
- 需要使用现有类,但其接口不符合系统需求。
- 希望创建一个可复用的类,与多个不相关的类(包括未来可能引入的类)一起工作,这些类可能没有统一的接口。
- 通过接口转换,将一个类集成到另一个类系中。
实现方式
- 继承或依赖:推荐使用依赖关系,而不是继承,以保持灵活性。
关键代码
适配器通过继承或依赖现有对象,并实现所需的目标接口。
应用实例
- 电压适配器:将 110V 电压转换为 220V,以适配不同国家的电器标准。
- 接口转换:例如,将 Java JDK 1.1 的 Enumeration 接口转换为 1.2 的 Iterator 接口。
- 跨平台运行:在Linux上运行Windows程序。
- 数据库连接:Java 中的 JDBC 通过适配器模式与不同类型的数据库进行交互。
优点
- 促进了类之间的协同工作,即使它们没有直接的关联。
- 提高了类的复用性。
- 增加了类的透明度。
- 提供了良好的灵活性。
缺点
- 过度使用适配器可能导致系统结构混乱,难以理解和维护。
- 在Java中,由于只能继承一个类,因此只能适配一个类,且目标类必须是抽象的。
使用建议
- 适配器模式应谨慎使用,特别是在详细设计阶段,它更多地用于解决现有系统的问题。
- 在考虑修改一个正常运行的系统接口时,适配器模式是一个合适的选择。
通过这种方式,适配器模式可以清晰地表达其核心概念和应用,同时避免了不必要的复杂性。
结构
适配器模式包含以下几个主要角色:
- 目标接口(Target):定义客户需要的接口。
- 适配者类(Adaptee):定义一个已经存在的接口,这个接口需要适配。
- 适配器类(Adapter):实现目标接口,并通过组合或继承的方式调用适配者类中的方法,从而实现目标接口。
实现
我们有一个 MediaPlayer 接口和一个实现了 MediaPlayer 接口的实体类 AudioPlayer。默认情况下,AudioPlayer 可以播放 mp3 格式的音频文件。
我们还有另一个接口 AdvancedMediaPlayer 和实现了 AdvancedMediaPlayer 接口的实体类。该类可以播放 vlc 和 mp4 格式的文件。
我们想要让 AudioPlayer 播放其他格式的音频文件。为了实现这个功能,我们需要创建一个实现了 MediaPlayer 接口的适配器类 MediaAdapter,并使用 AdvancedMediaPlayer 对象来播放所需的格式。
AudioPlayer 使用适配器类 MediaAdapter 传递所需的音频类型,不需要知道能播放所需格式音频的实际类。AdapterPatternDemo 类使用 AudioPlayer 类来播放各种格式。
step 1
为媒体播放器和更高级的媒体播放器创建接口。
public interface MediaPlayer {public void play(String audioType, String fileName);
}
AdvancedMediaPlayer.java
public interface AdvancedMediaPlayer { public void playVlc(String fileName);public void playMp4(String fileName);
}
step 2
创建实现了 AdvancedMediaPlayer 接口的实体类。
public class VlcPlayer implements AdvancedMediaPlayer{@Overridepublic void playVlc(String fileName) {System.out.println("Playing vlc file. Name: "+ fileName); }@Overridepublic void playMp4(String fileName) {//什么也不做}
}
Mp4Player.java
public class Mp4Player implements AdvancedMediaPlayer{@Overridepublic void playVlc(String fileName) {//什么也不做}@Overridepublic void playMp4(String fileName) {System.out.println("Playing mp4 file. Name: "+ fileName); }
}
step 3
创建实现了 MediaPlayer 接口的适配器类。
public class MediaAdapter implements MediaPlayer {AdvancedMediaPlayer advancedMusicPlayer;public MediaAdapter(String audioType){if(audioType.equalsIgnoreCase("vlc") ){advancedMusicPlayer = new VlcPlayer(); } else if (audioType.equalsIgnoreCase("mp4")){advancedMusicPlayer = new Mp4Player();} }@Overridepublic void play(String audioType, String fileName) {if(audioType.equalsIgnoreCase("vlc")){advancedMusicPlayer.playVlc(fileName);}else if(audioType.equalsIgnoreCase("mp4")){advancedMusicPlayer.playMp4(fileName);}}
}
step 4
创建实现了 MediaPlayer 接口的实体类。
public class AudioPlayer implements MediaPlayer {MediaAdapter mediaAdapter; @Overridepublic void play(String audioType, String fileName) { //播放 mp3 音乐文件的内置支持if(audioType.equalsIgnoreCase("mp3")){System.out.println("Playing mp3 file. Name: "+ fileName); } //mediaAdapter 提供了播放其他文件格式的支持else if(audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")){mediaAdapter = new MediaAdapter(audioType);mediaAdapter.play(audioType, fileName);}else{System.out.println("Invalid media. "+audioType + " format not supported");}}
}
step 5
使用 AudioPlayer 来播放不同类型的音频格式。
public class AdapterPatternDemo {public static void main(String[] args) {AudioPlayer audioPlayer = new AudioPlayer();audioPlayer.play("mp3", "beyond the horizon.mp3");audioPlayer.play("mp4", "alone.mp4");audioPlayer.play("vlc", "far far away.vlc");audioPlayer.play("avi", "mind me.avi");}
}
step 6
执行程序,输出结果:
五、代理模式(Proxy)
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能,这种类型的设计模式属于结构型模式。
代理模式通过引入一个代理对象来控制对原对象的访问。代理对象在客户端和目标对象之间充当中介,负责将客户端的请求转发给目标对象,同时可以在转发请求前后进行额外的处理。
在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
介绍
意图
为其他对象提供一种代理以控制对这个对象的访问。
主要解决的问题
- 代理模式解决的是在直接访问某些对象时可能遇到的问题,例如对象创建成本高、需要安全控制或远程访问等。
使用场景
- 当需要在访问一个对象时进行一些控制或额外处理时。
实现方式
- 增加中间层:创建一个代理类,作为真实对象的中间层。
- 代理与真实对象组合:代理类持有真实对象的引用,并在访问时进行控制。
关键代码
- 代理类:实现与真实对象相同的接口,并添加额外的控制逻辑。
- 真实对象:实际执行任务的对象。
应用实例
- 快捷方式:Windows系统中的快捷方式作为文件或程序的代理。
- 角色扮演:孙悟空作为高翠兰的代理,猪八戒无法区分。
- 代售点:购买火车票时,代售点作为火车站的代理。
- 支票:作为银行账户资金的代理,控制资金的访问。
- Spring AOP:使用代理模式来实现面向切面编程。
优点
- 职责分离:代理模式将访问控制与业务逻辑分离。
- 扩展性:可以灵活地添加额外的功能或控制。
- 智能化:可以智能地处理访问请求,如延迟加载、缓存等。
缺点
- 性能开销:增加了代理层可能会影响请求的处理速度。
- 实现复杂性:某些类型的代理模式实现起来可能较为复杂。
使用建议
- 根据具体需求选择合适的代理类型,如远程代理、虚拟代理、保护代理等。
- 确保代理类与真实对象接口一致,以便客户端透明地使用代理。
注意事项
- 与适配器模式的区别:适配器模式改变接口,而代理模式不改变接口。
- 与装饰器模式的区别:装饰器模式用于增强功能,代理模式用于控制访问。
结构
主要涉及到以下几个核心角色:
-
抽象主题(Subject):
- 定义了真实主题和代理主题的共同接口,这样在任何使用真实主题的地方都可以使用代理主题。
-
真实主题(Real Subject):
- 实现了抽象主题接口,是代理对象所代表的真实对象。客户端直接访问真实主题,但在某些情况下,可以通过代理主题来间接访问。
-
代理(Proxy):
- 实现了抽象主题接口,并持有对真实主题的引用。代理主题通常在真实主题的基础上提供一些额外的功能,例如延迟加载、权限控制、日志记录等。
-
客户端(Client):
- 使用抽象主题接口来操作真实主题或代理主题,不需要知道具体是哪一个实现类。
实现
我们将创建一个 Image 接口和实现了 Image 接口的实体类。ProxyImage 是一个代理类,减少 RealImage 对象加载的内存占用。
ProxyPatternDemo 类使用 ProxyImage 来获取要加载的 Image 对象,并按照需求进行显示。
step 1
创建一个接口。
public interface Image {void display();
}
step 2
创建实现接口的实体类。
public class RealImage implements Image {private String fileName;public RealImage(String fileName){this.fileName = fileName;loadFromDisk(fileName);}@Overridepublic void display() {System.out.println("Displaying " + fileName);}private void loadFromDisk(String fileName){System.out.println("Loading " + fileName);}
}
ProxyImage.java
public class ProxyImage implements Image{private RealImage realImage;private String fileName;public ProxyImage(String fileName){this.fileName = fileName;}@Overridepublic void display() {if(realImage == null){realImage = new RealImage(fileName);}realImage.display();}
}
step 3
当被请求时,使用 ProxyImage 来获取 RealImage 类的对象。
public class ProxyPatternDemo {public static void main(String[] args) {Image image = new ProxyImage("test_10mb.jpg");// 图像将从磁盘加载image.display(); System.out.println("");// 图像不需要从磁盘加载image.display(); }
}
step 4
执行程序,输出结果:
六、装饰器模式(Decorator Pattern)
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
装饰器模式通过将对象包装在装饰器类中,以便动态地修改其行为。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
我们通过下面的实例来演示装饰器模式的用法。其中,我们将把一个形状装饰上不同的颜色,同时又不改变形状类。
概要
意图
动态地给一个对象添加额外的职责,同时不改变其结构。装饰器模式提供了一种灵活的替代继承方式来扩展功能。
主要解决的问题
- 避免通过继承引入静态特征,特别是在子类数量急剧膨胀的情况下。
- 允许在运行时动态地添加或修改对象的功能。
使用场景
- 当需要在不增加大量子类的情况下扩展类的功能。
- 当需要动态地添加或撤销对象的功能。
实现方式
- 定义组件接口:创建一个接口,规定可以动态添加职责的对象的标准。
- 创建具体组件:实现该接口的具体类,提供基本功能。
- 创建抽象装饰者:实现同样的接口,持有一个组件接口的引用,可以在任何时候动态地添加功能。
- 创建具体装饰者:扩展抽象装饰者,添加额外的职责。
关键代码
- Component接口:定义了可以被装饰的对象的标准。
- ConcreteComponent类:实现Component接口的具体类。
- Decorator抽象类:实现Component接口,并包含一个Component接口的引用。
- ConcreteDecorator类:扩展Decorator类,添加额外的功能。
应用实例
- 孙悟空的72变:孙悟空(ConcreteComponent)通过变化(Decorator)获得新的能力。
- 画框装饰画:一幅画(ConcreteComponent)可以通过添加玻璃(ConcreteDecorator)和画框(ConcreteDecorator)来增强其展示效果。
优点
- 低耦合:装饰类和被装饰类可以独立变化,互不影响。
- 灵活性:可以动态地添加或撤销功能。
- 替代继承:提供了一种继承之外的扩展对象功能的方式。
缺点
- 复杂性:多层装饰可能导致系统复杂性增加。
使用建议
- 在需要动态扩展功能时,考虑使用装饰器模式。
- 保持装饰者和具体组件的接口一致,以确保灵活性。
注意事项
- 装饰器模式可以替代继承,但应谨慎使用,避免过度装饰导致系统复杂。
结构
装饰器模式包含以下几个核心角色:
- 抽象组件(Component):定义了原始对象和装饰器对象的公共接口或抽象类,可以是具体组件类的父类或接口。
- 具体组件(Concrete Component):是被装饰的原始对象,它定义了需要添加新功能的对象。
- 抽象装饰器(Decorator):继承自抽象组件,它包含了一个抽象组件对象,并定义了与抽象组件相同的接口,同时可以通过组合方式持有其他装饰器对象。
- 具体装饰器(Concrete Decorator):实现了抽象装饰器的接口,负责向抽象组件添加新的功能。具体装饰器通常会在调用原始对象的方法之前或之后执行自己的操作。
装饰器模式通过嵌套包装多个装饰器对象,可以实现多层次的功能增强。每个具体装饰器类都可以选择性地增加新的功能,同时保持对象接口的一致性。
实现
我们将创建一个 Shape 接口和实现了 Shape 接口的实体类。然后我们创建一个实现了 Shape 接口的抽象装饰类 ShapeDecorator,并把 Shape 对象作为它的实例变量。
RedShapeDecorator 是实现了 ShapeDecorator 的实体类。
DecoratorPatternDemo 类使用 RedShapeDecorator 来装饰 Shape 对象。
step 1
创建一个接口:
public interface Shape {void draw();
}
step 2
创建实现接口的实体类。
Rectangle.java
public class Rectangle implements Shape {@Overridepublic void draw() {System.out.println("Shape: Rectangle");}
}
Circle.java
public class Circle implements Shape {@Overridepublic void draw() {System.out.println("Shape: Circle");}
}
step 3
创建实现了 Shape 接口的抽象装饰类。
public abstract class ShapeDecorator implements Shape {protected Shape decoratedShape;public ShapeDecorator(Shape decoratedShape){this.decoratedShape = decoratedShape;}public void draw(){decoratedShape.draw();}
}
step 4
创建扩展了 ShapeDecorator 类的实体装饰类。
public class RedShapeDecorator extends ShapeDecorator {public RedShapeDecorator(Shape decoratedShape) {super(decoratedShape); }@Overridepublic void draw() {decoratedShape.draw(); setRedBorder(decoratedShape);}private void setRedBorder(Shape decoratedShape){System.out.println("Border Color: Red");}
}
step 5
使用 RedShapeDecorator 来装饰 Shape 对象。
public class DecoratorPatternDemo {public static void main(String[] args) {Shape circle = new Circle();ShapeDecorator redCircle = new RedShapeDecorator(new Circle());ShapeDecorator redRectangle = new RedShapeDecorator(new Rectangle());//Shape redCircle = new RedShapeDecorator(new Circle());//Shape redRectangle = new RedShapeDecorator(new Rectangle());System.out.println("Circle with normal border");circle.draw();System.out.println("\nCircle of red border");redCircle.draw();System.out.println("\nRectangle of red border");redRectangle.draw();}
}
step 6
执行程序,输出结果:
七、观察者模式(Observer)
观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
介绍
意图
创建了对象间的一种一对多的依赖关系,当一个对象状态改变时,所有依赖于它的对象都会得到通知并自动更新。
主要解决的问题
- 观察者模式解决的是一个对象状态改变时,如何自动通知其他依赖对象的问题,同时保持对象间的低耦合和高协作性。
使用场景
- 当一个对象的状态变化需要同时更新其他对象时。
实现方式
- 定义观察者接口:包含一个更新方法。
- 创建具体观察者:实现观察者接口,定义接收到通知时的行为。
- 定义主题接口:包含添加、删除和通知观察者的方法。
- 创建具体主题:实现主题接口,管理观察者列表,并在状态改变时通知它们。
关键代码
- 观察者列表:在主题中维护一个观察者列表。
应用实例
- 拍卖系统:拍卖师作为主题,竞价者作为观察者,拍卖价格更新时通知所有竞价者。
- 西游记故事:菩萨洒水作为状态改变,老乌龟作为观察者,观察到这一变化。
优点
- 抽象耦合:观察者和主题之间是抽象耦合的。
- 触发机制:建立了一套状态改变时的触发和通知机制。
缺点
- 性能问题:如果观察者众多,通知过程可能耗时。
- 循环依赖:可能导致循环调用和系统崩溃。
- 缺乏变化详情:观察者不知道主题如何变化,只知道变化发生。
使用建议
注意事项
- 避免循环引用:注意观察者和主题之间的依赖关系,避免循环引用。
- 异步执行:考虑使用异步通知避免单点故障导致整个系统卡壳。
结构
观察者模式包含以下几个核心角色:
- 主题(Subject):也称为被观察者或可观察者,它是具有状态的对象,并维护着一个观察者列表。主题提供了添加、删除和通知观察者的方法。
- 观察者(Observer):观察者是接收主题通知的对象。观察者需要实现一个更新方法,当收到主题的通知时,调用该方法进行更新操作。
- 具体主题(Concrete Subject):具体主题是主题的具体实现类。它维护着观察者列表,并在状态发生改变时通知观察者。
- 具体观察者(Concrete Observer):具体观察者是观察者的具体实现类。它实现了更新方法,定义了在收到主题通知时需要执行的具体操作。
观察者模式通过将主题和观察者解耦,实现了对象之间的松耦合。当主题的状态发生改变时,所有依赖于它的观察者都会收到通知并进行相应的更新。
实现
观察者模式使用三个类 Subject、Observer 和 Client。Subject 对象带有绑定观察者到 Client 对象和从 Client 对象解绑观察者的方法。我们创建 Subject 类、Observer 抽象类和扩展了抽象类 Observer 的实体类。
ObserverPatternDemo,我们的演示类使用 Subject 和实体类对象来演示观察者模式。
step 1
创建 Subject 类
import java.util.ArrayList;
import java.util.List;public class Subject {private List<Observer> observers = new ArrayList<Observer>();private int state;public int getState() {return state;}public void setState(int state) {this.state = state;notifyAllObservers();}public void attach(Observer observer){observers.add(observer); }public void notifyAllObservers(){for (Observer observer : observers) {observer.update();}}
}
step 2
创建 Observer 类。
public abstract class Observer {protected Subject subject;public abstract void update();
}
step 3
创建实体观察者类。
BinaryObserver.java
public class BinaryObserver extends Observer{public BinaryObserver(Subject subject){this.subject = subject;this.subject.attach(this);}@Overridepublic void update() {System.out.println( "Binary String: " + Integer.toBinaryString( subject.getState() ) ); }
}
OctalObserver.java
public class OctalObserver extends Observer{public OctalObserver(Subject subject){this.subject = subject;this.subject.attach(this);}@Overridepublic void update() {System.out.println( "Octal String: " + Integer.toOctalString( subject.getState() ) ); }
}
HexaObserver.java
public class HexaObserver extends Observer{public HexaObserver(Subject subject){this.subject = subject;this.subject.attach(this);}@Overridepublic void update() {System.out.println( "Hex String: " + Integer.toHexString( subject.getState() ).toUpperCase() ); }
}
step 4
使用 Subject 和实体观察者对象。
public class ObserverPatternDemo {public static void main(String[] args) {Subject subject = new Subject();new HexaObserver(subject);new OctalObserver(subject);new BinaryObserver(subject);System.out.println("First state change: 15"); subject.setState(15);System.out.println("Second state change: 10"); subject.setState(10);}
}
step 5
执行程序,输出结果:
八、策略模式(Strategy)
在策略模式(Strategy Pattern)中一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
介绍
意图
将每个算法封装起来,使它们可以互换使用。
主要解决的问题
- 解决在多种相似算法存在时,使用条件语句(如if...else)导致的复杂性和难以维护的问题。
使用场景
- 当一个系统中有许多类,它们之间的区别仅在于它们的行为时。
实现方式
- 定义策略接口:所有策略类都将实现这个统一的接口。
- 创建具体策略类:每个策略类封装一个具体的算法或行为。
- 上下文类:包含一个策略对象的引用,并通过该引用调用策略。
关键代码
- 策略接口:规定了所有策略类必须实现的方法。
- 具体策略类:实现了策略接口,包含具体的算法实现。
应用实例
- 锦囊妙计:每个锦囊代表一个策略,包含不同的计策。
- 旅行方式选择:骑自行车、坐汽车等,每种方式都是一个可替换的策略。
- Java AWT的LayoutManager:不同的布局管理器实现了相同的接口,但提供了不同的布局算法。
优点
- 算法切换自由:可以在运行时根据需要切换算法。
- 避免多重条件判断:消除了复杂的条件语句。
- 扩展性好:新增算法只需新增一个策略类,无需修改现有代码。
缺点
- 策略类数量增多:每增加一个算法,就需要增加一个策略类。
- 所有策略类都需要暴露:策略类需要对外公开,以便可以被选择和使用。
使用建议
- 当系统中有多种算法或行为,且它们之间可以相互替换时,使用策略模式。
- 当系统需要动态选择算法时,策略模式是一个合适的选择。
注意事项
- 如果系统中策略类数量过多,考虑使用其他模式或设计技巧来解决类膨胀问题。
结构
策略模式包含以下几个核心角色:
- 环境(Context):维护一个对策略对象的引用,负责将客户端请求委派给具体的策略对象执行。环境类可以通过依赖注入、简单工厂等方式来获取具体策略对象。
- 抽象策略(Abstract Strategy):定义了策略对象的公共接口或抽象类,规定了具体策略类必须实现的方法。
- 具体策略(Concrete Strategy):实现了抽象策略定义的接口或抽象类,包含了具体的算法实现。
策略模式通过将算法与使用算法的代码解耦,提供了一种动态选择不同算法的方法。客户端代码不需要知道具体的算法细节,而是通过调用环境类来使用所选择的策略。
实现
我们将创建一个定义活动的 Strategy 接口和实现了 Strategy 接口的实体策略类。Context 是一个使用了某种策略的类。
StrategyPatternDemo,我们的演示类使用 Context 和策略对象来演示 Context 在它所配置或使用的策略改变时的行为变化。
step 1
创建一个接口。
OperationAdd.java
public interface Strategy {public int doOperation(int num1, int num2);
}
step 2
创建实现接口的实体类。
public class OperationAdd implements Strategy{@Overridepublic int doOperation(int num1, int num2) {return num1 + num2;}
}
public class OperationSubtract implements Strategy{@Overridepublic int doOperation(int num1, int num2) {return num1 - num2;}
}
public class OperationMultiply implements Strategy{@Overridepublic int doOperation(int num1, int num2) {return num1 * num2;}
}
step 3
创建 Context 类。
public class Context {private Strategy strategy;public Context(Strategy strategy){this.strategy = strategy;}public int executeStrategy(int num1, int num2){return strategy.doOperation(num1, num2);}
}
step 4
使用 Context 来查看当它改变策略 Strategy 时的行为变化。
public class StrategyPatternDemo {public static void main(String[] args) {Context context = new Context(new OperationAdd()); System.out.println("10 + 5 = " + context.executeStrategy(10, 5));context = new Context(new OperationSubtract()); System.out.println("10 - 5 = " + context.executeStrategy(10, 5));context = new Context(new OperationMultiply()); System.out.println("10 * 5 = " + context.executeStrategy(10, 5));}
}
step 5
执行程序,输出结果:
九、模板模式(Template Method)
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
介绍
意图
在父类中定义了算法的骨架,并允许子类在不改变算法结构的前提下重定义算法的某些特定步骤。
主要解决的问题
- 解决在多个子类中重复实现相同的方法的问题,通过将通用方法抽象到父类中来避免代码重复。
使用场景
- 当存在一些通用的方法,可以在多个子类中共用时。
实现方式
- 定义抽象父类:包含模板方法和一些抽象方法或具体方法。
- 实现子类:继承抽象父类并实现抽象方法,不改变算法结构。
关键代码
- 模板方法:在抽象父类中定义,调用抽象方法和具体方法。
- 抽象方法:由子类实现,代表算法的可变部分。
- 具体方法:在抽象父类中实现,代表算法的不变部分。
应用实例
- 建筑流程:地基、走线、水管等步骤相同,后期建筑如加壁橱、栅栏等步骤不同。
- 西游记的81难:菩萨定好的81难代表一个顶层逻辑骨架。
- Spring对Hibernate的支持:封装了如开启事务、获取Session、关闭Session等通用方法。
优点
- 封装不变部分:算法的不变部分被封装在父类中。
- 扩展可变部分:子类可以扩展或修改算法的可变部分。
- 提取公共代码:减少代码重复,便于维护。
缺点
- 类数目增加:每个不同的实现都需要一个子类,可能导致系统庞大。
使用建议
- 当有多个子类共有的方法且逻辑相同时,再考虑使用模板方法模式。
- 对于重要或复杂的方法,可以考虑作为模板方法定义在父类中。
注意事项
- 为了防止恶意修改,模板方法通常使用
final
关键字修饰,避免被子类重写。
包含的几个主要角色
-
抽象父类(Abstract Class):
- 定义了模板方法和一些抽象方法或具体方法。
-
具体子类(Concrete Classes):
- 继承自抽象父类,并实现抽象方法。
-
钩子方法(Hook Method)(可选):
- 在抽象父类中定义,可以被子类重写,以影响模板方法的行为。
-
客户端(Client)(可选):
- 使用抽象父类和具体子类,无需关心模板方法的细节。
实现
-
我们将创建一个定义操作的 Game 抽象类,其中,模板方法设置为 final,这样它就不会被重写。Cricket 和 Football 是扩展了 Game 的实体类,它们重写了抽象类的方法。
TemplatePatternDemo,我们的演示类使用 Game 来演示模板模式的用法。
step1
创建一个抽象类,它的模板方法被设置为 final。
Game.java
public abstract class Game {abstract void initialize();abstract void startPlay();abstract void endPlay();//模板public final void play(){//初始化游戏initialize();//开始游戏startPlay();//结束游戏endPlay();}
}
step2
创建扩展了上述类的实体类。
Cricket.java
public class Cricket extends Game {@Overridevoid endPlay() {System.out.println("Cricket Game Finished!");}@Overridevoid initialize() {System.out.println("Cricket Game Initialized! Start playing.");}@Overridevoid startPlay() {System.out.println("Cricket Game Started. Enjoy the game!");}
}
Football.java
public class Football extends Game {@Overridevoid endPlay() {System.out.println("Football Game Finished!");}@Overridevoid initialize() {System.out.println("Football Game Initialized! Start playing.");}@Overridevoid startPlay() {System.out.println("Football Game Started. Enjoy the game!");}
}
step 3
使用 Game 的模板方法 play() 来演示游戏的定义方式。
public class TemplatePatternDemo {public static void main(String[] args) {Game game = new Cricket();game.play();System.out.println();game = new Football();game.play(); }
}
step 4
执行程序,输出结果: