java 23种设计模式讲解跟实例
- 什么是设计模式
- 设计模式分类
- 六大设计原则
- 创建型模式
- 单例模式(singleton)
- 工厂方法模式
- 建造者模式
- 抽象工厂模式
- 原型模式
- 结构型模式
- 适配器模式
- 桥接模式
- 组合模式
- 装饰模式
- 外观模式
- 享元模式
- 代理模式
- 行为型模式
- 责任链模式
- 命令模式
- 迭代器模式
- 中介者模式
- 备忘录模式
- 观察者模式
- 状态模式
- 策略模式
- 模板模式
- 访问者模式
- 解释器模式
什么是设计模式
设计模式,即DesignPatterns,是指在软件设计中,被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性,设计模式的核心在于提供了相关
问题的解决方案,使得人们可以更加简单方便地复用成功的设计和体系结构。
设计模式分类
创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式对类或对象
怎样交互和怎样分配职责进行描述。
![在这里插
六大设计原则
-
开闭原则
当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。
所谓开放封闭原则就是软件实体应该对扩展开放,而对修改封闭(不需要修改接口,就能实现新的需求)。开放封闭原则是所有面向对象原则的核心。软件设计本身所追求的目标就是封装变化,降低耦合,而开放封闭原则正是对这一目标的最直接体现。
开放封闭原则主要体现在两个方面:
- 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展(根据接口添加新的实现类),以适应新的情况。
- 对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改也能满足新的需求。
- 里氏替换原则
所有引用基类(父类)的地方必须能透明地使用其子类的对象。
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
-
依赖倒置原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象;
抽象不应该依赖细节;细节应该依赖抽象。依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。 传递依赖的三种写法: 构造函数传递依赖对象 Setter方法传递依赖对象 接口声明传递依赖对象
-
迪米特法则
一个类应该应该对其他类尽可能了解得最少,类只与直接的朋友通信等等。但是其最终目的只有一个,就是让类间解耦。 -
接口隔离原则
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中方法尽量少,使用多个隔离的接口,比使用单个接口要好。 -
单一职责原则
一个类只负责一项职责。 当超过一项职责需要负责时,需要增加新的类来负责新的职责,而不是在类中个性代码。
创建型模式
关注对象的实例化过程,包括了如何实例化对象、隐藏对象的创建细节等。常见的创建型模式有单例模式、工厂模式、抽象工厂模式等。
单例模式(singleton)
某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。
优点:
- 单例模式可以保证内存里只有一个共用的实例对象,减少了内存的开销。
- 可以避免对资源的多重占用,节省系统资源。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
缺点:
- 系统扩展困难,如果有修改就必须修改原来的代码,容易造成系统出现bug
- 不适用与并发
- 针对一个功能可能出现其他逻辑兼容性差
场景:
- 系统中要求共用一个实例的时候,比如一个共用的配置
代码实现:
- 饿汉式实例
类加载就会导致该单实例对象被创建,保证在调用里面的方法之前对象已经存在
/**
* decription:饿汉式
*/
public class HungryHanStyleSingleton {
private static HungryHanStyleSingleton hungryHanStyleSingleton = new HungryHanStyleSingleton();
/*将构造方法私有化这样就不能在外部进行实例化*/
private HungryHanStyleSingleton(){
}
/*提供一个公共的方法,外部通过这个方法获取该对象*/
public static HungryHanStyleSingleton getHungryHanStyleSingleton(){
return hungryHanStyleSingleton;
}
}
- 懒汉式实例
/**
* decription:懒汉式
*/
public class LazeyStyleSingleton {
private static volatile LazeyStyleSingleton lazeyStyleSingleton=null; //保证 lazeyStyleSingleton 在所有线程中同步
private LazeyStyleSingleton(){} //private 避免类在外部被实例化
public static synchronized LazeyStyleSingleton getLazeyStyleSingleton()
{
//getLazeyStyleSingleton 方法前加同步
if(lazeyStyleSingleton == null)
{
lazeyStyleSingleton = new LazeyStyleSingleton();
}
return lazeyStyleSingleton;
}
}
调用
public static void main(String[] args) {
HungryHanStyleSingleton hanStyleSingleton = HungryHanStyleSingleton.getHungryHanStyleSingleton();
System.out.println("我是饿汉式:" + hanStyleSingleton);
LazeyStyleSingleton lazeyStyleSingleton = LazeyStyleSingleton.getLazeyStyleSingleton();
System.out.println("我是懒汉式:" + lazeyStyleSingleton);
}
工厂方法模式
工厂方法模式提供了一个创建对象的接口,但是将具体的对象创建延迟到子类中。这样,客户端代码不需要知道要创建的具体对象的类,只需要通过工厂方法来创建对象。这使得客户端代码与具体对象的创建解耦,提高了代码的灵活性和可维护性。
在工厂方法模式中,通常会定义一个抽象工厂类,其中包含一个创建对象的抽象方法,而具体的对象创建则由具体的子类实现。这样,每个具体的子类都可以根据需要创建不同类型的对象,而客户端代码只需要通过抽象工厂类来调用工厂方法,而不需要关心具体的对象创建细节。
优点:
- 松耦合:客户端代码与具体对象的创建解耦,使得系统更具弹性和可维护性。
- 扩展性:通过添加新的具体工厂和产品子类,可以很容易地扩展系统以支持新的对象类型。
- 封装性:将对象的创建集中在工厂类中,封装了对象的创建细节,使得客户端代码更简洁。
缺点:
- 工厂类以及产品子类的增加,增加系统的复杂性
- 增加了系统的抽象性和代码的理解难度
场景:
- 当一个类不知道它所必须创建的对象的类的时候
- 客户只知道产品工厂,不知道产品名
- 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。
代码实现:
工厂方法模式由抽象工厂、具体工厂、抽象产品和具体产品等4个要素构成。
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
实列:这里用工厂方法模式设计一个多类型的汽车工厂,有越野车制造工厂(suv)来制造越野车,卡车制造工厂(truck)来制造卡车。
/*提供创建产品的接口*/
public interface Product {
/*创建产品接口*/
public void carProduct();
}
/*具体产品创建实现类1 suv(越野车)创建*/
public class SuvProduct implements Product {
@Override
public void carProduct() {
System.out.println("Build a suv");
}
}
/*具体产品创建实现类2 truck(卡车)创建*/
public class TruckProduct implements Product {
@Override
public void carProduct() {
System.out.println("Build a truck");
}
}
/*创建一个抽象工厂类 ProductFactory,它定义了一个抽象的工厂方法 createProduct,子类将实现这个方法来创建具体的产品对象*/
public interface ProductFactory {
public Product createProduct();
}
/*具体工厂类1 实现了 ProductFactory 并重写了 createProduct 方法来返回相应的汽车产品对象*/
public class SuvFactory implements ProductFactory {
@Override
public Product createProduct() {
return new SuvProduct();
}
}
/*具体工厂类2 实现了 ProductFactory 并重写了 createProduct 方法来返回相应的汽车产品对象*/
public class TruckFactory implements ProductFactory {
@Override
public Product createProduct() {
return new TruckProduct();
}
}
测试
public class FactoryTestClass {
public static void main(String[] args) {
/*使用工厂类,来制造产品*/
ProductFactory suvFactory = new SuvFactory();
Product suvProduct = suvFactory.createProduct();
suvProduct.carProduct();
ProductFactory truckFactory = new TruckFactory();
Product truckProduct = truckFactory.createProduct();
truckProduct.carProduct();
}
}
建造者模式
指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
建造者模式将复杂产品的创建步骤分解在在不同的方法中,使得创建过程更加清晰,从而更精确控制复杂对象的产生过程;通过隔离复杂对象的构建与使用,也就是将产品的创建与产品本身分离开来,使得同样的构建过程可以创建不同的对象;并且每个具体建造者都相互独立,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
优点:
- 分离构建过程和表示:封装性好,构建和表示分离,通过建造者模式,可以将复杂对象的构建过程与其最终表示分离,使得构建过程更加清晰可控。
- 支持不同的表示:扩展性好,各个具体的建造者相互独立,有利于系统的解耦,通过使用不同的具体建造者,可以创建不同的产品表示,而不改变客户端的代码。
- 更好的可扩展性:客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险,如果需要添加新的产品变体,只需创建一个新的具体建造者即可,而无需修改已有的代码。
- 隐藏产品的内部结构:客户端只需与抽象建造者和指导者交互,无需关心产品的内部构建细节。
缺点: - 产品的组成部分必须相同,这限制了其使用范围。
- 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。
结构:
- 产品(Product):表示正在构建的复杂对象。建造者模式的目标是构建这个产品。
- 抽象建造者(Abstract Builder):定义了构建产品的步骤和方法,但没有具体的实现。不同的具体建造者可以实现不同的构建步骤,从而创建不同的产品变体。
- 具体建造者(Concrete Builder):实现了抽象建造者定义的方法,完成了产品的构建过程。每个具体建造者负责构建特定的产品变体。
- 指导者(Director):负责控制建造的过程。它通过将客户端与具体建造者分离,确保产品的构建是按照一定顺序和规则进行的。
场景:
- 相同的方法,不同的执行顺序,产生不同的结果。
- 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
- 产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
- 初始化一个对象特别复杂,参数多,而且很多参数都具有默认值。
实例:
这里使用建造者模式组装一台电脑,组成要素有机箱、主板、cpu、显卡、硬盘、显示器、键盘
、鼠标。
代码:
- 产品角色
/*产品角色*/
@Data
public class ComputerProduct {
/*机箱、主板、cpu、显卡、硬盘、显示器、键盘
、鼠标。*/
private String chassis;//机箱
private String motherboard;//主板
private String cpu;//cpu
private String gpu;//显卡
private String hardDisk;//硬盘
private String monitor;//显示器
private String keyboard;//键盘
private String mouse;//鼠标
}
- 抽象建造者
/*抽象建造者*/
public interface ComputerBuilder {
public void buildChassis();//机箱
public void buildMotherboard();//主板
public void buildCpu();//cpu
public void buildGpu();//显卡
public void buildHardDisk();//硬盘
public void buildMonitor();//显示器
public void buildKeyboard();//键盘
public void buildMouse();//鼠标
//返回产品对象
public ComputerProduct getResult();
}
- 具体建造者
/*具体建造者 实现抽象建造者接口*/
public class ComputerConcreteBuilder implements ComputerBuilder{
private ComputerProduct computerProduct = new ComputerProduct();
@Override
public void buildChassis() {
computerProduct.setChassis("机箱");
}
@Override
public void buildMotherboard() {
computerProduct.setMotherboard("主板");
}
@Override
public void buildCpu() {
computerProduct.setCpu("cpu");
}
@Override
public void buildGpu() {
computerProduct.setGpu("显卡");
}
@Override
public void buildHardDisk() {
computerProduct.setHardDisk("硬盘");
}
@Override
public void buildMonitor() {
computerProduct.setMonitor("显示器");
}
@Override
public void buildKeyboard() {
computerProduct.setKeyboard("键盘");
}
@Override
public void buildMouse() {
computerProduct.setMouse("鼠标");
}
@Override
public ComputerProduct getResult() {
return computerProduct;
}
}
- 指导者
/*指导者 调用建造者中的方法实现产品的组成创建*/
public class Director {
private ComputerBuilder builder;
public Director(ComputerBuilder builder) {
this.builder = builder;
}
public void construct() {
builder.buildChassis();
builder.buildMotherboard();
builder.buildCpu();
builder.buildGpu();
builder.buildHardDisk();
builder.buildMonitor();
builder.buildKeyboard();
builder.buildMouse();
}
}
测试:
实例化一个ComputerConcreteBuilder对象(具体的建造者),并将其传递给Director对象(指导者)。然后,调用Director对象的construct方法,该方法将使用ComputerBuilder对象(抽象建造者)构建ComputerProduct对象(产品),这将构建一个ComputerProduct对象(产品),并将其存储在ComputerProduct变量中。
public static void main(String[] args) {
ComputerBuilder computerBuilder = new ComputerConcreteBuilder();
Director direcotor = new Director(computerBuilder);
direcotor.construct();
ComputerProduct computerProduct = computerBuilder.getResult();
System.out.println(computerProduct);
}
抽象工厂模式
抽象工厂模式是一种创建型模式,是工厂模式的一种变体,它提供了一个接口来创建相关或依赖对象的系列,而不需要指定它们的具体类。当您需要创建一组相关对象,但希望将这些对象的实现与使用它们的代码分离时,此模式非常有用
优点:
- 产品族一致性:抽象工厂确保创建的产品是一组相关的产品族,保证了这些产品之间的一致性。
- 松耦合:客户端代码不需要直接依赖于具体产品,只需要通过抽象工厂接口创建产品,从而降低了代码的耦合度。
- 可扩展性:增加新的产品族或产品变得相对容易,只需要添加新的具体工厂和产品类即可,不需要修改现有代码。
缺点:
抽象工厂模式要求系统中的每个产品族都必须有一个对应的具体工厂,当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的复杂性。
结构:
抽象工厂模式同工厂方法模式一样,也是由抽象工厂、具体工厂、抽象产品和具体产品等 4 个要素构成,但抽象工厂中方法个数不同,抽象产品的个数也不同。现在我们来分析其基本结构和实现方法。
- 抽象工厂(Abstract Factory):声明了一组用于创建不同产品的抽象方法。具体的工厂类必须实现这些方法来创建具体的产品对象。
- 具体工厂(Concrete Factory):实现抽象工厂接口,负责创建特定种类的产品对象。
- 抽象产品(Abstract Product):定义了产品的通用接口,具体产品必须实现这个接口。
- 具体产品(Concrete Product):实现抽象产品接口,是抽象工厂创建的实际对象。
场景:
- 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如计算机软件工厂中的操作系统,应用程序,相互关联相互依赖等。
- 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢使用Windows系统中的应用。
- 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
实例:
在这个实例中,抽象工厂模式通过SoftwareFactory接口和其实现类来创建不同类型的操作系统和应用程序。
客户端代码可以根据需要选择不同的工厂实例来创建不同的产品组合。
- 分析
- 创建多个抽象产品类接口,这里创建了连个操作系统接口,应用程序接口,
- 根据抽象产品类接口创建多个具体实现类,这里每个产品类接口创建了各自创建了两个具体实现类操作系统(Windows实现类,Linux实现类),应用程序(钉钉应用实现类,word文档应用实现类),
- 创建工厂接口,接口中有创建抽象产品类的接口,创建操作系统接口和创建应用程序接口的方法
- 创建多个具体工厂实现类,这里根据需要创建了两个具体工厂Windows工厂,Linux工厂,各自实现了创建系统以及创建应用程序的方法,这里仅供参考,可以自已选择创建产品组合,如Windows系统跟word,Linux系统跟钉钉,反过来亦是如此。
- 代码
/*抽象产品接口1:操作系统*/
public interface OperatingSystemProduct {
public void run();
}
/*具体产品1-1:Windows 操作系统*/
public class WindowsOS implements OperatingSystemProduct{
@Override
public void run() {
System.out.println("Running Windows OS");
}
}
/*具体产品1-2:Linux 操作系统*/
public class LinuxOS implements OperatingSystemProduct{
@Override
public void run() {
System.out.println("Running Linux OS");
}
}
/*抽象产品接口2:应用程序*/
public interface ApplicationProgramProduct {
public void open();
}
/*具体产品2-1:钉钉应用程序*/
public class DingDingApplication implements ApplicationProgramProduct{
@Override
public void open() {
System.out.println("Opening DingDing Application");
}
}
/*具体产品2-2:Word应用程序*/
public class WordApplication implements ApplicationProgramProduct{
@Override
public void open() {
System.out.println("Opening Word Application");
}
}
/*抽象工厂接口:计算机软件工厂*/
public interface SoftwareFactory {
OperatingSystemProduct createOperatingSystemProduct();//创建操作系统
ApplicationProgramProduct createApplicationProgramProduct();//创建应用程序
}
/*具体工厂1:Windows工厂*/
public class WindowsFactory implements SoftwareFactory{
@Override
public OperatingSystemProduct createOperatingSystemProduct() {
return new WindowsOS();
}
@Override
public ApplicationProgramProduct createApplicationProgramProduct() {
return new DingDingApplication();
}
}
/*具体工厂2:Linux工厂*/
public class LinuxFactory implements SoftwareFactory{
@Override
public OperatingSystemProduct createOperatingSystemProduct() {
return new LinuxOS();
}
@Override
public ApplicationProgramProduct createApplicationProgramProduct() {
return new WordApplication();
}
}
- 测试
// 在这个实例中,抽象工厂模式通过SoftwareFactory接口和其实现类来创建不同类型的操作系统和应用程序。
// 客户端代码可以根据需要选择不同的工厂实例来创建不同的产品组合。
public static void main(String[] args) {
SoftwareFactory windowsFactory = new WindowsFactory();
OperatingSystemProduct windowsFactoryOperatingSystemProduct = windowsFactory.createOperatingSystemProduct();
ApplicationProgramProduct windowsFactoryApplicationProgramProduct = windowsFactory.createApplicationProgramProduct();
windowsFactoryOperatingSystemProduct.run();
windowsFactoryApplicationProgramProduct.open();
SoftwareFactory linuxFactory = new LinuxFactory();
OperatingSystemProduct linuxFactoryOperatingSystemProduct = linuxFactory.createOperatingSystemProduct();
ApplicationProgramProduct linuxFactoryApplicationProgramProduct = linuxFactory.createApplicationProgramProduct();
linuxFactoryOperatingSystemProduct.run();
linuxFactoryApplicationProgramProduct.open();
}
原型模式
原型模式也是用于对象的创建,通过将一个对象作为原型,对其进行复制克隆,产生一个与源对象类似的新对象。
优点:
- 减少对象创建的成本:Java 自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良,同事避免了复杂对象的重复初始化过程,提高了创建对象的效率。
- 避免与具体类耦合:客户端可以通过克隆方法创建新对象,而无需知道具体类的细节,降低了耦合度。
- 灵活性增加:可以在运行时动态地添加或删除原型,适应不同的对象创建需求。 支持动态配置:可以通过克隆来定制对象的不同配置,而无需修改其代码。
缺点:
- 需要为每一个类都配置一个 clone 方法
- clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
- 深克隆问题,原型模式默认进行浅克隆,即复制对象本身和其引用。如果对象内部包含其他对象的引用,可能需要实现深克隆来复制整个对象结构。当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。
结构:
在 Java 中,原型模式的核心是就是原型类 Prototype,Prototype 类需要具备以下两个条件:实现 Cloneable 接口;重写 Object 类中的 clone() 方法,用于返回对象的拷贝。
Object 类中的 clone() 方法默认是浅拷贝,如果想要深拷贝对象,则需要在 clone() 方法中自定义自己的复制逻辑:
浅复制:将一个对象复制后,基本数据类型的变量会重新创建,新对象的属性和原来对象完全相同,而引用类型指向的还是原对象所指向的内存地址。
深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
原型模式包含以下主要角色:
- 抽象原型类:规定了具体原型对象必须实现的接口(Cloneable接口)。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
场景:
原型模式的目的是 降低实例对象个数 , 减少构造函数的调用次数 ;
- 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值,比如在某一个逻辑中循环创建了很多该实例对象。
- 对象之间相同或相似,即只是个别的几个属性不同的时候。
- 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
- 如果类初始化时消耗过多的资源 , 如这个类中某个成员占用大量内存 , 为了节省开销 。
实例:
- 浅克隆
Java 中的 Object 类提供了浅克隆的 clone() 方法,具体原型类只要实现 Cloneable 接口就可实现对象的浅克隆(默认浅克隆),这里的 Cloneable 接口就是抽象原型类。
现在我们新建一个学校类来实现 Cloneable 接口,并实现他的clone()方法,然后测试对比新建原型类跟克隆的原型类。
/*具体原型类*/
@Data
public class School implements Cloneable{
private String name;
//实现克隆的方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
//测试浅克隆
School school = new School();
School school1 = (School) school.clone();
System.out.println(school == school1);
}
测试对比
通过输出结果为false,我们可以知道。通过克隆出来的对象已经成为了一个新的对象,并且拥有自己的内存空间,这是因为我们在我们的学校类之中只定义了基本类型,所以在使用 clone() 方法创建对象的时候就将基本类型的值复制了过来。
这种浅拷贝的作用类似于我们new一个对象,但是克隆的执行效率要比new高的多,因为调用 clone() 方法无需调用构造器就可以创建对象,所以在所克隆的类中没有引用类型的情况下,一次性的克隆大量的对象相比于new一个对象可以大大的提高效率。
如果有引用类型变量的存在比如引用了其他的实体类,那么克隆的具体原型类中引用的实体类指向的还是原对象所指向的内存地址,也就是说对比具体原型类跟克隆的类对比是不同的两个对象,但是引用的实体类确实相同的。下面新建一个班级类在学校类中引用:
/*具体原型类引用类型 班级类*/
@Data
public class ClassInfo implements Cloneable{
private String name;
//实现克隆的方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
/*具体原型类*/
@Data
public class School implements Cloneable{
private String name;
private ClassInfo classInfo;
//实现克隆的方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
//测试浅克隆
School school = new School();
ClassInfo classInfo = new ClassInfo();
school.setClassInfo(classInfo);
School school1 = (School) school.clone();
System.out.println(school == school1);
System.out.println(school.getClassInfo() == school1.getClassInfo());
}
- 深克隆(使用重写clone方法的方式)
在以上的输出结果中我们可以分析到,在学校类中引用的班级类并没有完成克隆,只是复制了一份内存地址给新的学校类对象。
所以这也是浅克隆的弊端,如果使用这种方式克隆了多个学校类,但是班级对象始终只有一个,那么就会导致所有的学校对象共享这一个班级对象,无论那个学校对象修改了班级对象,所有学校对象中的学生班级都会发生改变。
解决办法:
我们可以通过重写clone()方法的方式,在方法中克隆班级类赋值给克隆的学校类中,完成学校对象的深克隆。
引用的班级类在上面有代码
/*具体原型类*/
@Data
public class School implements Cloneable{
private String name;
private ClassInfo classInfo;
//实现克隆的方法
@Override
protected Object clone() throws CloneNotSupportedException {
//浅克隆
//return super.clone();
//深克隆 重写clone
School school = (School) super.clone();
school.setClassInfo((ClassInfo) classInfo.clone());
return school;
}
}
测试
public static void main(String[] args) throws CloneNotSupportedException {
//测试浅克隆
School school = new School();
ClassInfo classInfo = new ClassInfo();
school.setClassInfo(classInfo);
School school1 = (School) school.clone();
System.out.println(school == school1);
System.out.println(school.getClassInfo() == school1.getClassInfo());
}
通过结果所示,深克隆方式的原型类引用的实体类跟克隆原型类引用的实体类是不同的,完成了对学校类的深克隆。
结构型模式
关注对象之间的组合方式,以达到构建更大结构的目标。这些模式帮助你定义对象之间的关系,从而实现更大的结构。常见的结构型模式有适配器模式、装饰器模式、代理模式等。
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。
适配器模式
适配器模式将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
优点:
- 客户端通过适配器可以透明地调用目标接口。
- 将现有的代码跟新的代码无缝协同工作,复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类,也可以提高代码的可重用性。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
缺点:
- 对类适配器来说,更换适配器的实现过程比较复杂。
- 适配器模式也可能引入一些复杂性,因为你需要维护适配器类和处理不同接口之间的映射关系,增加了维护的复杂性。
结构:
主要的构成角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
场景:
-
接口不兼容:
当系统中的某个类的接口与客户端期望的接口不兼容时,可以使用适配器模式将其转换为客户端期望的接口。 -
类的复用:
当需要复用一些现有的类,但其接口与系统要求的接口不匹配时,可以通过适配器模式将这些类适配到系统中。 -
封装有关适配的逻辑:
当需要封装与具体业务逻辑无关的适配逻辑时,可以使用适配器模式将适配的细节封装在适配器中,使客户端代码更简洁。 -
协同工作:
当两个类需要协同工作,但它们的接口不一致时,适配器模式可以使它们协同工作,无需修改它们的代码。 -
旧系统升级:
当需要将旧系统升级为新系统时,由于接口的变化,可以使用适配器模式在不修改旧系统代码的情况下使其与新系统协同工作。 -
希望使用某类,但与接口或其他代码不兼容时使用
实例:
适配器有三种形式,分别为,类适配器,对象适配器,接口适配器。
- 类适配器模式
类适配器模式使用继承来实现接口转换。在类适配器模式中,适配器类继承了一个已存在的类并实现了目标接口。通过继承,适配器类既可以使用已存在的类的功能,又可以实现目标接口,从而使得原本由于接口不匹配而无法在一起工作的两个类可以协同工作。
//目标接口
public interface Target {
/*当前接口所需要的接口*/
public void request();
}
/*已存在的类(适配者)*/
public class Adapter {
//已存在需求
public void existingResquest(){
System.out.println("Adapter Existing Resquest");//已存在的需求
}
}
/* 适配器类(继承已存在的类并实现目标接口)*/
public class ClassAdapter extends Adapter implements Target {
@Override
public void request() {
System.out.println("Execute current requirements");//执行当前的需求
existingResquest();//调用已存在的类的方法
}
}
调用测试
public static void main(String[] args) {
Target target = new ClassAdapter();
target.request();
}
Adapter 是已存在的类,Target是目标接口。ClassAdapter继承了Adaptee类,并实现了Target接口。通过继承,ClassAdapter可以调用Adaptee的方法,并在实现的request方法中适配调用。这样,客户端就可以通过Target接口调用ClassAdapter的request方法,可以协同工作实现了适配器模式的效果。
- 对象适配器模式
对象适配器模式使用对象组合的方式实现接口转换。在对象适配器模式中,适配器类持有一个已存在的类的实例,并实现目标接口。通过组合,适配器类既可以使用已存在的类的功能,又可以实现目标接口,从而使得原本由于接口不匹配而无法在一起工作的两个类可以协同工作。
//目标接口
public interface Target {
/*当前接口所需要的接口*/
public void request();
}
/*已存在的类(适配者)*/
public class Adapter {
//已存在需求
public void existingResquest(){
System.out.println("Adapter Existing Resquest");//已存在的需求
}
}
/* 适配器类(持有被适配者的实例并实现目标接口)*/
public class ObjectAdapter implements Target {
private Adapter adapter;
public ObjectAdapter(Adapter adapter){
this.adapter = adapter;
}
@Override
public void request() {
System.out.println("Execute current requirements");//执行当前的需求
adapter.existingResquest();//调用被适配者的方法
}
}
调用测试
public static void main(String[] args) {
//对象适配器模式
Adapter adapter = new Adapter();
Target target1 = new ObjectAdapter(adapter);
target1.request();
}
Adaptee是已存在的类,Target是目标接口。ObjectAdapter类持有Adapter的实例,并实现了Target接口。通过组合,ObjectAdapter可以调用Adapter的方法,并在实现的request方法中适配调用。这样,客户端就可以通过Target接口调用ObjectAdapter的request方法,将已存在的类以及方法跟当前需求相结合,实现了适配器模式的效果。
- 接口适配器模式
接口适配器模式也称为缺省适配器模式。在实际应用中,我们有时候可能会遇到一个接口定义了多个方法,但我们只想要实现其中的一部分方法,而不是全部。接口适配器模式通过提供一个抽象类,该抽象类实现了接口的所有方法,但是这些方法的实现为空,然后我们只需要继承这个抽象类并重写我们感兴趣的方法,从而达到只实现部分接口方法的目的。
/*目标接口*/
public interface Target {
void method1();
void method2();
void method3();
}
/*接口适配器抽象类*/
public abstract class InterfaceAdapter implements Target{
@Override
public void method1() {
}
@Override
public void method2() {
// 默认实现为空
}
@Override
public void method3() {
// 默认实现为空
}
}
/*接口适配器的抽象类的具体实现类*/
public class ConcreteInterfaceAdapter extends InterfaceAdapter {
@Override
public void method2() {
// 实现自己感兴趣的方法
System.out.println("ConcreteAdapter method2");
}
}
调用测试
public static void main(String[] args) {
//接口适配器模式
Target target2 = new ConcreteInterfaceAdapter();
target2.method2();
Target是目标接口,InterfaceAdapter是接口适配器的抽象类,提供了接口的默认实现。ConcreteInterfaceAdapter是具体的实现类,只需要关注自己感兴趣的方法。这样,通过继承InterfaceAdapter,我们可以实现目标接口的部分方法,而不需要实现全部方法,从而达到了接口适配器的效果。
桥接模式
桥接模式将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
桥接模式通过将抽象部分和具体部分分离,使它们可以独立地变化。在桥接模式中,通过创建一个桥接接口(或抽象类),其中包含一个指向具体实现的引用,将抽象部分和具体部分连接起来。这样,抽象部分和具体部分可以独立地进行扩展,而不会相互影响。这种方式也被称为“组合优于继承”。
优点:
- 由于抽象与实现分离,所以能够提供更好的灵活性和可扩展性。
- 它允许抽象部分和具体部分独立变化,避免了类层次结构的爆炸式增长。这样可以更容易地添加新的抽象部分和具体部分
缺点:
- 由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,这增加了系统的理解与设计难度。
- 可能会引入一些复杂性,因为你需要管理更多的类和对象。
结构:
可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。
桥接(Bridge)模式包含以下主要角色:
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用(构造函数规定子类要传入一个实现对象)。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(ConcreteImplementor)角色:给出实现化角色接口的具体实现
场景:
- 系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时,想要拆分或重组一个具有多重功能的庞杂类(例如多个能与数据交互的类)可使用。
- 想在几个纬度上扩展类。
- 系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时,想要在运行是切换不同实现方法。
实例:
这里使用桥接模式模拟协议的数据通过不同的方式获取对应的数据;
/*数据解析 实现化接口*/
public interface DataAnalysis {
public void analysisMethod();
}
/*数据解析 具体实现1*/
public class Method1 implements DataAnalysis {
@Override
public void analysisMethod() {
System.out.println("Data Analysis Method 1");
}
}
/*数据解析 具体实现2*/
public class Method2 implements DataAnalysis {
@Override
public void analysisMethod() {
System.out.println("Data Analysis Method 2");
}
}
/*数据解析 抽象化*/
public abstract class AnalyzeProtocol {
protected DataAnalysis dataAnalysis;
public AnalyzeProtocol(DataAnalysis dataAnalysis){
this.dataAnalysis = dataAnalysis;
}
public abstract void operation();
}
/*数据解析 扩展抽象化1*/
public class RefinedAnalyzeProtocol1 extends AnalyzeProtocol {
public RefinedAnalyzeProtocol1(DataAnalysis dataAnalysis){
super(dataAnalysis);
}
public void operation(){
System.out.println("实现解析方法1");
dataAnalysis.analysisMethod();
};
}
/*数据解析 扩展抽象化2*/
public class RefinedAnalyzeProtocol2 extends AnalyzeProtocol {
public RefinedAnalyzeProtocol2(DataAnalysis dataAnalysis){
super(dataAnalysis);
}
public void operation(){
System.out.println("实现解析方法2");
dataAnalysis.analysisMethod();
};
}
调用测试
public static void main(String[] args) {
DataAnalysis dataAnalysis1 = new Method1();
DataAnalysis dataAnalysis2 = new Method2();
AnalyzeProtocol analyzeProtocol1 = new RefinedAnalyzeProtocol1(dataAnalysis1);
AnalyzeProtocol analyzeProtocol2 = new RefinedAnalyzeProtocol2(dataAnalysis2);
analyzeProtocol1.operation();
analyzeProtocol2.operation();
}
在这个示例中,DataAnalysis接口代表数据解析的实现部分,Method1和 Method2分别是实现了表数据解析接口的具体解析类。
AnalyzeProtocol是协议数据的抽象部分,具有一个数据解析方式引用,而 RefinedAnalyzeProtocol1和 RefinedAnalyzeProtocol2是继承自 AnalyzeProtocol的具体协议数据类,相当于作为AnalyzeProtocol的扩展类。
/这种设计允许我们在不改变数据的情况下,独立地对它们进行扩展解析方式。
组合模式
有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。
在组合模式中,存在两种主要的对象类型:叶节点(Leaf)和容器节点(Composite)。叶节点表示树结构中的最终节点,它们没有子节点。容器节点表示树结构中的分支节点,它们可以包含其他叶节点和容器节点。
组合模式的关键是通过定义共同的接口或抽象类,使得叶节点和容器节点都可以被一致地对待。这样,客户端可以递归遍历整个树结构,而无需关心当前处理的节点是叶节点还是容器节点。
优点:
- 提供了统一的操作接口:组合模式定义了一组统一的操作接口,使得对对象的操作更加一致和方便。
- 灵活性:更容易在组合体内加入新的对象叶子节点或者容器节点,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;
- 简化客户端代码: 客户端不需要判断操作的对象是单个对象还是对象组合,从而简化了客户端的代码。
缺点:
- 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
- 不容易限制容器中的构件;
- 不容易用继承的方法来增加构件的新功能;
结构:
- 抽象构件(Component)角色:这是一个抽象类或接口,定义了单个对象和对象组合共同的操作。它可以有一些默认实现,也可以有抽象方法需要在具体子类中实现。它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。
- 树叶构件(Leaf)角色:继承自组件,表示单个对象。它没有子对象。是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中 声明的公共接口。
- 树枝构件(Composite)角色:继承自组件,表示对象组合。它包含了一组子对象,这些子对象可以是叶子,也可以是复合。是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,它的主要作用是存储和管理子部件,通常包含
Add()、Remove()、GetChild() 等方法
场景:
- 维护和展示部分-整体关系的场景。
- 需要对对象实施一组操作,无论是叶节点还是容器节点。
- 需要灵活性和可扩展性:当需要灵活地增加新的叶节点或容器节点,并且无需修改现有代码时,可以使用组合模式。组合模式通过统一的接口和递归结构,使得添加新的节点变得非常方便,不会影响现有代码。
- 希望客户端以统一的方式处理单个对象和组合对象。
- 想要使用递归操作的优化时
例如: 如树形菜单、 文件和文件夹管理,商品结算,
实例:
这里举例一个通用的代码结构,具体代码根据业务自行扩展比如自定操作逻辑,扩展叶子节点或者容器节点(树枝节点)。
/*抽象构件 定义单个对象跟组合对象共有的操作*/
public interface Component {
/*通用业务逻辑*/
public void operation();
}
/*叶子节点 */
public class Leaf implements Component{
private String name;
public Leaf(String name){
this.name = name;
}
@Override
public void operation() {
System.out.println("叶节点 " + name + " 执行操作");
}
}
/*容器节点 也就是树枝节点*/
public class Composite implements Component{
private List<Component> children = new ArrayList<>();
/*管理叶子节点 新增*/
public void add(Component component){
children.add(component);
}
/*管理叶子节点 删除*/
public void remove(Component component){
children.remove(component);
}
@Override
public void operation() {
System.out.println("容器节点执行操作(自定义逻辑):");
for (Component component : children) {
component.operation();
}
}
}
客户端调用
public static void main(String[] args) {
Component leaf1 = new Leaf("leaf1");
Component leaf2 = new Leaf("leaf2");
Composite composite = new Composite();
composite.add(leaf1);
composite.add(leaf2);
composite.operation();
}
在上述示例中,我们定义了Component接口作为组件的通用接口,其中包括operation()方法。Leaf类表示叶节点,实现了Component接口。Composite类表示容器节点,包含一个List来存储其子节点。该类实现了Component接口,并在operation()方法中递归调用其子节点的operation()方法。
在客户端main()方法中,我们创建了一个叶节点leaf1和leaf2,以及一个容器节点composite。然后,我们将叶节点添加到容器节点中,并调用容器节点的operation()方法。执行结果将递归执行容器节点和叶节点的操作。
装饰模式
装饰模式在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
装饰模式提供了一种在运行时动态地为对象添加新功能的方法,通过创建一个装饰类来包装原始类。装饰类具有与原始类相同的接口,它内部包含一个指向原始对象的引用,并且可以根据需要包装额外的功能
优点:
- 装饰类和被装饰者可以独立进行实现,可以动态扩展装饰者类的诸多功能。
- 可以通过组合多个装饰器来实现不同的组合效果,更灵活。
缺点:
- 随着功能的增多,装饰者模式也会出现多个功能类代码的增多,增加代码复杂性。
结构:
- 组件(Component):定义了一个抽象的接口,可以是具体对象或装饰器所共有的接口。
- 具体组件(ConcreteComponent):实现了组件接口,是被装饰的原始对象。
- 装饰器(Decorator):持有一个指向组件对象的引用,并实现了组件的接口。它可以包含额外的功能,也可以将请求传递给组件对象。
- 具体装饰器(Concrete Decorator):扩展了装饰器类,通过添加额外的功能来装饰具体组件。
通过这种方式,装饰模式允许你将功能嵌套地堆叠在一起,以实现各种不同的功能组合,同时保持代码的灵活性和可维护性。
场景:
- 通常我们向新对象中加入一个功能时,需要其类继承父类或者实现相应的接口,但是随着种类的增多,和功能的特征增加,会使原对象变得比较臃肿,这个时候就可以使用装饰者模式来解决。
实例:
下面以奶茶做一个实例,实现在单品奶茶的基础上添加其他调料并计算价格:
/*组件 奶茶接口*/
public interface MilkTea {
public double cost();//价格
public String description();//奶茶描述
}
/*具体组件 实现单品奶茶(最基本的奶茶)*/
public class SimpleMilkTea implements MilkTea {
@Override
public double cost() {
return 10.2;
}
@Override
public String description() {
return "单品奶茶";
}
}
/*装饰器抽象类 */
public abstract class MilkDecorator implements MilkTea {
protected MilkTea decoratedMilk;
public MilkDecorator(MilkTea decoratedMilk){
this.decoratedMilk = decoratedMilk;
}
@Override
public double cost() {
return decoratedMilk.cost();
}
@Override
public String description() {
return decoratedMilk.description();
}
}
/*具体装饰类 扩展装饰类 芋泥奶茶*/
public class PoiMilkDecorator extends MilkDecorator{
public PoiMilkDecorator(MilkTea decoratedMilk) {
super(decoratedMilk);
}
@Override
public double cost() {
return super.cost() + 1.0;
}
@Override
public String description() {
return super.description() + "+ 芋泥";
}
}
/*具体装饰类 扩展装饰类 珍珠奶茶*/
public class PearlMilkDecorator extends MilkDecorator{
public PearlMilkDecorator(MilkTea decoratedMilk) {
super(decoratedMilk);
}
@Override
public double cost() {
return super.cost() + 1.0;
}
@Override
public String description() {
return super.description() + "+ 珍珠";
}
}
调用测试
public static void main(String[] args) {
MilkTea milkTea = new SimpleMilkTea();
System.out.println("奶茶描述 :"+milkTea.description()+",价格: $ "+milkTea.cost());
MilkTea poiMilkTea = new PoiMilkDecorator(milkTea);
System.out.println("奶茶描述 :"+poiMilkTea.description()+",价格: $ "+poiMilkTea.cost());
MilkTea pearlMilkTea = new PearlMilkDecorator(milkTea);
System.out.println("奶茶描述 :"+pearlMilkTea.description()+",价格: $ "+pearlMilkTea.cost());
}
在上面示例中,MilkTea接口定义了基本的单品奶茶。SimpleMilkTea类实现了单品奶茶。 MilkDecorator是装饰器的抽象类,它维护一个被装饰的单品奶茶对象。 PoiMilkDecorator和 PearlMilkDecorator分别实现了具体的装饰器,通过在原始的单品奶中上添加了其他的调料品(添加新的功能)。
这里只是简单的举例,在实际业务中根据需求实现业务逻辑以及新功能的添加。
外观模式
外观模式提供了一个统一的接口(外观)来访问复杂系统的子系统集合。通过外观模式,我们可以隐藏系统的复杂性,并提供一个简洁的接口供客户端使用。外观模式通过将客户端与子系统之间的通信和调用逻辑封装在外观类中,实现了解耦和简化。这样,客户端只需要与外观类进行交互,而无需直接与子系统进行复杂的交互。
优点:
- 简化接口:客户端只需要与外观类交互,无需了解底层子系统的复杂性。
- 降低耦合:外观模式将客户端与子系统解耦,使得系统的变化不会影响客户端代码。
- 提高可维护性:由于外观模式将子系统封装起来,修改子系统的实现不会影响客户端代码,从而提高了系统的可维护性。
- 支持松散耦合:外观模式可以帮助系统中的不同模块之间实现松散耦合,从而支持模块的独立开发和测试。
缺点:
- 不能很好地限制客户使用子系统类。
- 修改麻烦, 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
结构:
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(SubSystem)角色:实现系统的部分功能,客户可以通过外观角色访问它。
场景:
- 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
例如:实现家庭影院一键开启关闭音响、投影仪、灯光。实现智能家居一键开启灯光、电视剧、窗帘、空调等。
实例:
这里举例实现一个家庭影院观影的实例,与其配合的有音响、投影仪、灯光、,实现观影,结束观影。
/*子系统 音响*/
public class StereoSystem {
public void turnOn() {
System.out.println("音响打开");
}
public void turnOff() {
System.out.println("音响关闭");
}
}
/*子系统 投影仪*/
public class Projector {
public void turnOn() {
System.out.println("投影仪打开");
}
public void turnOff() {
System.out.println("投影仪关闭");
}
}
/*子系统 灯光*/
public class Lights {
public void turnOn() {
System.out.println("灯光打开");
}
public void turnOff() {
System.out.println("灯光关闭");
}
}
/*外观类 家庭影院*/
public class HomeTheaterFacade {
private StereoSystem stereoSystem;//子系统 音响
private Projector projector;//子系统 投影仪
private Lights lights;//子系统 灯光
public HomeTheaterFacade(){
stereoSystem = new StereoSystem();
projector = new Projector();
lights = new Lights();
}
/*开始观影*/
public void watchMovie(){
System.out.println("准备观影...");
stereoSystem.turnOn();
projector.turnOn();
lights.turnOff();
System.out.println("开始观影");
}
/*结束观影*/
public void endMovie(){
System.out.println("准备结束观影");
stereoSystem.turnOff();
projector.turnOff();
lights.turnOn();
System.out.println("结束观影");
}
}
调用测试
public static void main(String[] args) {
HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade();
homeTheaterFacade.watchMovie();
homeTheaterFacade.endMovie();
}
上面的实例,HomeTheaterFacade充当了一个外观类,封装了StereoSystem(音响)、Projector(投影仪)和Lights(灯光)等子系统的复杂操作,以便客户端可以通过简单的调用来完成观影过程。
这样,客户端不需要了解各个子系统的具体操作,只需通过外观类的方法来控制整个家庭影院系统的行为。
享元模式
通过共享对象来减少系统中的对象数量,从而节省内存和系统资源。它将对象的属性分为内部状态(Intrinsic State)和外部状态(Extrinsic State)。内部状态是对象可共享的部分,存储在享元对象内部;外部状态是对象的变化部分,由客户端在使用时传递给享元对象。通过共享内部状态和传递外部状态,可以实现对大量相似对象的共享使用。
享元模式通过一个享元工厂(Flyweight Factory)来管理和创建共享对象。当需要一个对象时,工厂会检查是否已经有相同内部状态的对象存在,如果存在则返回已有的对象,否则创建一个新的对象并将其添加到内部对象池中。
优点:
- 享元模式可以显著减少内存消耗,因为共享对象的内部状态只有一份。这可以在需要大量相似对象的情况下节省内存。同时,由于共享对象已经存在于池中,创建时间和性能开销也会降低。
缺点:
- 享元模式可以显著减少内存消耗,因为共享对象的内部状态只有一份。这可以在需要大量相似对象的情况下节省内存。同时,由于共享对象已经存在于池中,创建时间和性能开销也会降低。
结构:
享元模式中存在以下两种状态:
内部状态,即不会随着环境的改变而改变的可共享部分;
外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。下面来分析其基本结构和实现方法。
- Flyweight(享元抽象类):一般是接口或者抽象类,定义了享元类的公共方法。这些方法可以分享内部状态的数据,也可以调用这些方法修改外部状态。
- ConcreteFlyweight(具体享元类):具体享元类实现了抽象享元类的方法,为享元对象开辟了内存空间来保存享元对象的内部数据,同时可以通过和单例模式结合只创建一个享元对象
- Unsharable Flyweight(非享元类) :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建,飞非享元类一般不会出现在享元工厂中。
- FlyweightFactory(享元工厂类):享元工厂类创建并且管理享元类,享元工厂类针对享元类来进行编程,通过提供一个享元池来进行享元对象的管理。一般享元池设计成键值对,或者其他的存储结构来存储。当客户端进行享元对象的请求时,如果享元池中有对应的享元对象则直接返回对应的对象,否则工厂类创建对应的享元对象并保存到享元池。
场景:
- 系统中存在大量相似对象,且创建和销毁这些对象会消耗大量内存和系统资源。
- 对象的状态可以分为内部状态和外部状态,且内部状态可以共享。
- 外部状态可以通过参数传递给对象,且不影响对象的内部状态。
实例:
这里举例获取指定IP的设备对象
/*享元抽象类 */
public interface DeviceFlyweight {
public void showInfo(String state);
}
/*具体享元类 */
public class PlcDeviceFlyweight implements DeviceFlyweight{
private String ip;
public PlcDeviceFlyweight(String ip){
this.ip = ip;
}
@Override
public void showInfo(String state) {
System.out.println("Plc设备ip:" + ip);
System.out.println("Plc设备状态:" + state);
}
}
/*享元工厂 */
public class DeviceFlyweightFactory {
private Map<String,DeviceFlyweight> deviceFlyweightMap = new HashMap<>();
public DeviceFlyweight getFlyweight(String ip){
DeviceFlyweight flyweight = deviceFlyweightMap.get(ip);
if(flyweight == null){
flyweight = new PlcDeviceFlyweight(ip);
deviceFlyweightMap.put(ip, flyweight);
}else {
System.out.println("具体享元"+ip+"已经存在,已从缓存中获取");
}
return flyweight;
}
}
调用测试
public static void main(String[] args) {
DeviceFlyweightFactory flyweightFactory = new DeviceFlyweightFactory();
DeviceFlyweight flyweight1 = flyweightFactory.getFlyweight("192.168.1");
flyweight1.showInfo("在线");
DeviceFlyweight flyweight2 = flyweightFactory.getFlyweight("192.168.2");
flyweight2.showInfo("离线");
DeviceFlyweight flyweight3 = flyweightFactory.getFlyweight("192.168.3");
flyweight3.showInfo("在线");
DeviceFlyweight flyweight33 = flyweightFactory.getFlyweight("192.168.3");
flyweight33.showInfo("在线");
}
在上面实例中,我们定义了一个享元接口DeviceFlyweight,并实现了具体的享元类PlcDeviceFlyweight。享元工厂DeviceFlyweightFactory负责创建和管理享元对象。客户端通过享元工厂获取享元对象,并传递外部状态给享元对象进行渲染。
代理模式
代理模式提供了一个代理对象,它充当了原始对象的替代品,以控制对原始对象的访问。代理对象与原始对象实现相同的接口,使得客户端可以无缝地切换和使用。代理对象可以对客户端的请求进行拦截、修改或增强,然后将请求传递给原始对象。
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用。
- 代理对象可以扩展目标对象的功能。
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性。
缺点:
- 代理模式会造成系统设计中类的数量增加。
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢。
- 增加了系统的复杂度;
结构:
静态代理:代理对象在编译期,就已经被编译成class,在运行之前就已经存在了,代理和被代理对象在代理之前是确定的,他们都实现相同的接口或者继承相同的抽象类。
动态代理:代理对象是在运行期间通过反射生成的。
代理模式的主要角色:
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(RealSubject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
场景:
- 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
- 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
- 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
- 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
- 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。
实例:
- 静态代理
代理和被代理对象在代理之前是确定的,他们都实现相同的接口或者继承相同的抽象类。
/*抽象主题类 电脑*/
public interface ComputerAbstractObject {
/*电脑的操作*/
public void operation();
}
/*真实主题 实现抽象主题的方法*/
public class RealComputerObject implements ComputerAbstractObject {
@Override
public void operation() {
System.out.println("打开浏览器可以查资料");
}
}
/*代理主题*/
public class ProxyObject implements ComputerAbstractObject {
private ComputerAbstractObject computerAbstractObject;
public ProxyObject(ComputerAbstractObject computerAbstractObject){
this.computerAbstractObject = computerAbstractObject;
}
@Override
public void operation() {
//可以在调用目标对象之前做一些别的操作
System.out.println("打开音乐");
//调用目标对象的操作
computerAbstractObject.operation();
//可以在调用目标对象之后做一些别的操作
System.out.println("开始视频应用追剧");
}
}
调用测试
public static void main(String[] args) {
ComputerAbstractObject computerAbstractObject = new RealComputerObject();
System.out.println("未使用代理");
computerAbstractObject.operation();
System.out.println("使用代理后");
ProxyObject proxyObject = new ProxyObject(computerAbstractObject);
proxyObject.operation();
}
在上面的实例中可以看出代理对象将客户端的调用委派给目标对象,在调用目标对象的方法之前跟之后都可以执行特定的操作。
这就是静态代理的实现,静态代理中,一个目标对象对应一个代理对象,代理类在编译时期就已经确定了。
- 动态代理
创建动态代理类时,需要实现InvocationHandler接口,并实现其invoke方法,这个方法在代理对象调用其方法函数时,会被触发;
InvocationHandler接口(事务处理器)中仅定义了一个方法public object invoke(Object obj,Method method,Object[] args),在实际使用中,第一个参数obj一般是指代理类,method是被代理的方法,args为该方法的参数数组,这个抽象方法在代理类中动态实现。
在上面的抽象主题类跟真实主题类的基础上新增一个动态代理类
/*动态代理主题*/
public class DynamicProxyObject implements InvocationHandler {
private Object object;
//生成并返回代理对象
public Object getObject(Object object) {
this.object =object;
return Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(),this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置处理。。。");
Object result = method.invoke(object, args);
System.out.println("后置处理。。。");
return result;
}
}
调用测试
public static void main(String[] args) {
//动态代理
ComputerAbstractObject computerAbstractObject1 = new RealComputerObject();
DynamicProxyObject dynamicProxyObject = new DynamicProxyObject();
ComputerAbstractObject computerAbstractObject2= (ComputerAbstractObject) dynamicProxyObject.getObject(computerAbstractObject1);
computerAbstractObject2.operation();
}
在上面实例中,代理类的invoke方法被执行了,并对被代理类进行了增强,如果想要实现切换目标对象可以新增目标对象新建一个抽象主题类跟真实抽象主题的类,调用动态代理的类传入新的目标对象即可。
行为型模式
关注对象之间的通信方式,以及如何合作共同完成任务。这些模式涉及到对象之间的交互、责任分配等。常见的行为型模式有观察者模式、策略模式、命令模式等。
责任链模式
如果有多个对象有机会处理请求,责任链可使请求的发送者和接受者解耦,请求沿着责任链传递,直到有一个对象处理了它为止,就是将链中的每一个结点看做是一个对象,每个结点处理请求均不同,且内部自动维护一个下一个结点对象。当请求从链条的首端出发时,会沿着链的路径依次传递给每一个结点的对象,直到有对象处理这个请求为止。
优点:
- 降低耦合度:发送者不需要知道哪个对象会处理请求,只需将请求发送到链的起始点。
- 灵活性:当工作流程发生变化,可以根据需要动态地改变处理链中处理者的顺序,以及每个处理者的职责。
- 可扩展性:可以根据业务需求添加新的处理者,而不会影响现有代码。
- 可维护性:明确责任范围,每个处理者关注单一的责任,使得代码更易于理解和维护,符合类的单一责任原则。
缺点:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
结构:
职责链模式主要包含以下角色:
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者(ConcreteHandler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
场景:
- 有多个对象可以处理一个请求,哪个对象处理该请求由运行时刻自动确定。
- 可动态指定一组对象处理请求,或添加新的处理者。
- 在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。
实例:
这里举例一个报销的请求,首先创建一个报销请求,然后根据结构创建抽象处理类,具体处理类以及客户端创建处理链(调用)。
/*责任链模式 首先创建一个请求类 (报销对象)*/
public class ReimbursementRequest {
/*报销金额*/
private double amount;
/*报销内容*/
private String description;
public ReimbursementRequest(double amount, String description) {
this.amount = amount;
this.description = description;
}
public double getAmount() {
return amount;
}
public String getDescription() {
return description;
}
}
/*抽象处理者 具体处理者设置三个ManagerHandler(主管),DepartmentHeadHandler(部门主管)FinanceHandler(财务部门)*/
public abstract class ReimbursementHandler {
protected ReimbursementHandler successor;
public void setSuccessor(ReimbursementHandler successor) {
this.successor = successor;
}
/*处理请求*/
public abstract void handleRequest(ReimbursementRequest request);
}
/*具体处理者 (经理)*/
public class ManagerHandler extends ReimbursementHandler{
@Override
public void handleRequest(ReimbursementRequest request) {
if (request.getAmount() <= 5000) {
System.out.println("经理处理报销请求:" + request.getDescription());
} else if (successor != null) {
successor.handleRequest(request);
}
}
}
/*具体处理者 (部门主管)*/
public class DepartmentHeadHandler extends ReimbursementHandler{
@Override
public void handleRequest(ReimbursementRequest request) {
if (request.getAmount() <= 1000) {
System.out.println("经理处理报销请求:" + request.getDescription());
} else if (successor != null) {
successor.handleRequest(request);
}
}
}
/*具体处理者 (财务部门)*/
public class FinanceHandler extends ReimbursementHandler {
@Override
public void handleRequest(ReimbursementRequest request) {
System.out.println("财务部门处理报销请求:" + request.getDescription());
}
}
客户端调用测试
/*Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。*/
public static void main(String[] args) {
ReimbursementHandler manager = new ManagerHandler();
ReimbursementHandler departmentHead = new DepartmentHeadHandler();
ReimbursementHandler finance = new FinanceHandler();
manager.setSuccessor(departmentHead);
departmentHead.setSuccessor(finance);
ReimbursementRequest request1 = new ReimbursementRequest(800, "购买办公用品");
ReimbursementRequest request2 = new ReimbursementRequest(3000, "部门聚会");
ReimbursementRequest request3 = new ReimbursementRequest(10000, "举办团建活动");
manager.handleRequest(request1);
manager.handleRequest(request2);
manager.handleRequest(request3);
}
在上面实例中,可以看到报销请求会依次被经理、部门主管和财务部门处理。根据报销金额的不同,请求会被传递到适当的处理者。
命令模式
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。
优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。 方便实现 Undo 和 Redo操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
缺点:
可能产生大量具体命令类。因为设计每一个具体操作都需要设计一个具体命令类,这将增加系统的复杂性。
结构:
- 命令(Command):抽象命令类,定义了执行命令的接口。它通常包含一个执行方法,以及可能的其他方法(例如,撤消)。
- 具体命令(ConcreteCommand):实现了抽象命令类的具体子类,将一个接收者与一个动作绑定。它实现了执行方法,该方法调用接收者的特定操作。
- 接收者(Receiver):执行实际工作的类。命令模式将命令传递给接收者,由接收者执行实际的操作。
- 调用者/请求者(Invoker):负责将命令传递给合适的接收者并触发命令的执行。它并不关心具体的命令细节。
场景:
- 当系统需要将请求调用者与请求接收者解耦时,命令模式使得调用者和接收者不直接交互。
- 当系统需要随机请求命令或经常增加或删除命令时,命令模式比较方便实现这些功能。
- 当系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
- 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。
实例:
这里举例通过命令实现设备的开启以及关闭。
/*命令抽象类 */
public interface Command {
public void execute();
}
/*具体命令类 调用接收类执行关闭*/
public class DeviceOffConcreteCommand implements Command {
private DeviceReceiver deviceReceiver;
public DeviceOffConcreteCommand(DeviceReceiver deviceReceiver){
this.deviceReceiver = deviceReceiver;
}
@Override
public void execute() {
deviceReceiver.turnOff();
}
}
/*具体命令类 调用接收类执行开启*/
public class DeviceOnConcreteCommand implements Command {
private DeviceReceiver deviceReceiver;
public DeviceOnConcreteCommand(DeviceReceiver deviceReceiver){
this.deviceReceiver = deviceReceiver;
}
@Override
public void execute() {
deviceReceiver.turnOn();
}
}
/*命令接收类 执行实际的操作*/
public class DeviceReceiver {
/*开启*/
public void turnOn(){
System.out.println("设备开启");
}
/*关闭*/
public void turnOff(){
System.out.println("设备关闭");
}
}
/*调用/请求类 */
public class InvokerControl {
private Command command;
public void setCommand(Command command){
this.command = command;
}
/*传递命令*/
public void pressButton() {
command.execute();
}
}
调用测试
public static void main(String[] args) {
DeviceReceiver deviceReceiver = new DeviceReceiver();
DeviceOnConcreteCommand deviceOnConcreteCommand = new DeviceOnConcreteCommand(deviceReceiver);
DeviceOffConcreteCommand deviceOffConcreteCommand = new DeviceOffConcreteCommand(deviceReceiver);
InvokerControl invokerControl = new InvokerControl();
invokerControl.setCommand(deviceOnConcreteCommand);
invokerControl.pressButton();
invokerControl.setCommand(deviceOffConcreteCommand);
invokerControl.pressButton();
}
在上面实例中,我们使用命令模式创建了两种具体的命令:开启设备和关闭设备。
调用者可以设置不同的命令,这里仅设置啦开启和关闭,然后按下按钮触发相应的操作。
这样,命令发送者(调用类)和命令接收者(设备)之间实现了解耦。
迭代器模式
迭代器模式提供了一种统一的方法来遍历不同类型的集合,而无需暴露集合内部的表示细节。它包括两个主要组件:迭代器和集合。迭代器负责遍历集合并提供统一的访问接口,而集合负责实际存储元素。迭代器和集合之间的解耦使得可以独立地改变它们的实现,而不会影响到客户端代码。
优点:
- 访问一个聚合对象的内容而无须暴露它的内部表示。
- 遍历任务交由迭代器完成,这简化了聚合类。
- 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
- 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
- 封装性良好,为遍历不同的聚合结构提供一个统一的接口。
缺点:
在扩展性增强的同时,一旦添加新的遍历方法,则需要增加新的聚合类和迭代器类,使类的个数成倍增加,会对系统本身的程序在一定程度上增加了复杂度。
结构:
迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。
迭代器模式主要包含以下角色:
- 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
- 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
- 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
- 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。
场景:
- 当需要为聚合对象提供多种遍历方式时。
- 当需要为遍历不同的聚合结构提供一个统一的接口时。
- 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
实例:
这里举例一个通用代码
/*抽象聚合 定义一个可迭代的集合接口*/
public interface IterableCollection<T> {
public Iterator<T> createIterator();
}
/*具体聚合*/
public class ConcreteCollection<T> implements IterableCollection<T> {
private List<T> items = new ArrayList<>();
public void addItem(T item) {
items.add(item);
}
@Override
public Iterator<T> createIterator() {
return new ConcreteIterator<>(items);
}
}
/*抽象迭代器 */
public interface Iterator<T> {
public boolean hasNext();
public T next();
}
/*具体迭代器*/
public class ConcreteIterator<T> implements Iterator<T>{
private List<T> items;
private int position = 0;
public ConcreteIterator(List<T> items){
this.items = items;
}
@Override
public boolean hasNext() {
return position < items.size();
}
@Override
public T next() {
if (hasNext()){
T item = items.get(position);
position++;
return item;
}
throw new IndexOutOfBoundsException("No more elements");
}
}
调用测试
public static void main(String[] args) {
ConcreteCollection<String> collection = new ConcreteCollection<>();
collection.addItem("Item 1");
collection.addItem("Item 2");
collection.addItem("Item 3");
Iterator<String> iterator = collection.createIterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
在上面的实例中,我们定义了一个IterableCollection接口来表示可迭代的集合,一个具体的集合类ConcreteCollection实现了这个接口,并提供了一个用于创建迭代器的方法。
迭代器接口Iterator定义了hasNext(判断是否有下一个元素)和next(获取下一个元素)方法,具体的迭代器类ConcreteIterator实现了这个接口,并通过内部的位置追踪来遍历集合。根据自己的需求定义访问和遍历聚合的方法。
中介者模式
定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。
优点:
- 降低耦合度:对象之间的通信逻辑被集中在中介者中,从而降低了对象之间的直接依赖,减少了耦合度,使系统更加灵活和可维护。
- 集中管理:所有对象的交互逻辑都集中在中介者中,使得系统的交互逻辑更加清晰可见,便于管理和修改。
- 复用性:中介者模式将交互逻辑与对象本身的业务逻辑分离,可以更容易地复用这些交互逻辑。
- 可扩展性:通过增加或修改中介者对象,可以相对容易地扩展系统,而不需要修改对象之间的通信逻辑。
缺点:
中介者模式在一定程度上引入了中介者对象作为中心化的调度者,可能会增加系统的复杂性。
结构:
- 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
- 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List
- 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
- 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
- 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
场景:
- 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
- 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。
实例:
/*抽象中介者*/
public interface Mediator {
/*注册同事类*/
public void register(Colleague colleague);
/*处理接收逻辑*/
public void operation(Colleague colleague);
}
/*具体中介者*/
public class ConcreteMediator implements Mediator {
private List<Colleague> colleagues = new ArrayList<Colleague>();
@Override
public void register(Colleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMediator(this);
}
}
@Override
public void operation(Colleague colleague) {
for (Colleague coll : colleagues) {
if (!coll.equals(colleague)) {
coll.receive();
}
}
}
/*抽象同事类*/
public abstract class Colleague {
protected Mediator mediator;
public void setMediator(Mediator mediator){
this.mediator = mediator;
};
/*接收请求*/
public abstract void receive();
/*发送请求*/
public abstract void send();
}
/*具体同事类 1*/
public class ConcreteColleague1 extends Colleague{
@Override
public void receive() {
System.out.println("具体同事类 ConcreteColleague1 接收请求");
}
@Override
public void send() {
System.out.println("具体同事类 ConcreteColleague1 发送请求");
/*中介者进行转发*/
mediator.operation(this);
}
}
/*具体同事类 2*/
public class ConcreteColleague2 extends Colleague{
@Override
public void receive() {
System.out.println("具体同事类 ConcreteColleague2 接收到请求");
}
@Override
public void send() {
System.out.println("具体同事类 ConcreteColleague2 发送请求");
mediator.operation(this);
}
}
调用测试
public static void main(String[] args) {
Mediator concreteMediator = new ConcreteMediator();
Colleague concreteColleague1 = new ConcreteColleague1();
Colleague concreteColleague2 = new ConcreteColleague2();
concreteMediator.register(concreteColleague1);
concreteMediator.register(concreteColleague2);
concreteColleague1.send();
concreteColleague2.send();
}
备忘录模式
又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
二、结构
优点:
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
缺点:
源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
结构:
-
发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
-
备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
-
管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
备忘录有两个等效的接口:
窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror
Interface),这个窄接口只允许他把备忘录对象传给其他的对象。 •
宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide
Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
场景:
- 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
- 需要提供一个可回滚操作的场景,如Word、记事本、Photoshop,idea等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。
实例:
/*备忘录*/
public class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
/*发起人*/
public class Originator {
private String state;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
/*创建备忘录*/
public Memento createMemento(){
return new Memento(state);
}
public void restoreMemento(Memento memento){
state = memento.getState();
}
}
/*管理者*/
public class Caretaker {
private Memento memento;
public Memento getMemento(){
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
调用测试
public static void main(String[] args) {
Originator originator = new Originator();
Caretaker caretaker = new Caretaker();
/*设置初始化状态*/
originator.setState("State 1");
System.out.println("Current State: " + originator.getState());
/*创建备忘录并保存*/
Memento memento = originator.createMemento();
caretaker.setMemento(memento);
// 修改状态
originator.setState("State 2");
System.out.println("Updated State: " + originator.getState());
// 恢复之前的状态
originator.restoreMemento(caretaker.getMemento());
System.out.println("Restored State: " + originator.getState());
}
在这个示例中,Originator 类表示原始对象,它具有状态并能够创建和恢复备忘录。
Memento 类表示备忘录对象,保存了特定时刻的状态。Caretaker 类负责保存和获取备忘录对象。
通过设置初始状态、创建备忘录、修改状态、然后恢复状态。
也就是将原始类当前的属性值保存到备忘录类里面,并将备忘录类放到备忘录管理类中,当改变当前原始属性值后想要恢复之前的属性值则通过备忘录管理类中获取备忘录将值重新赋值给原始类的属性,我们可以看到备忘录模式的工作方式。
观察者模式
指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
优点:
- 松耦合:主题和观察者之间的耦合度降低,使得它们可以独立地进行变化。
- 可扩展性:可以方便地增加新的观察者,而不会影响到已有的观察者和主题。
- 自动通知:主题状态改变时会自动通知观察者,减少手动维护通知的工作。
- 可重用性:主题和观察者可以在不同的场景中重复使用。
缺点:
- 可能引起性能问题:如果观察者过多或通知机制不合理,可能会导致性能下降。
- 更新顺序问题:观察者的更新顺序可能会影响到系统的行为,需要特别注意。
- 过度使用的风险:并不是所有的状态变化都适合使用观察者模式,过度使用可能导致代码复杂化。
结构:
- 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
- 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
- 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
- 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
场景:
- 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
实例:
/*抽象观察着接口*/
public interface Observer {
public void update(int state);
}
/*具体观察者 1*/
public class ConcreteObserver1 implements Observer{
private String name;
public ConcreteObserver1(String name){
this.name = name;
}
@Override
public void update(int state) {
System.out.println(name + " 收到更新,新状态为: " + state);
}
}
/*具体观察者 2*/
public class ConcreteObserver2 implements Observer{
private String name;
public ConcreteObserver2(String name){
this.name = name;
}
@Override
public void update(int state) {
System.out.println(name + " 收到更新,新状态为: " + state);
}
}
/*抽象主题接口*/
public interface Subject {
/*添加观察者*/
public void addObserver(Observer observer);
/*删除观察者*/
public void removeObserer(Observer observer);
/*通知观察者*/
public void notifyObservers();
}
/*具体主题类*/
public class ConcreteSubject implements Subject {
private List<Observer> observerList = new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyObservers();
}
@Override
public void addObserver(Observer observer) {
observerList.add(observer);
}
@Override
public void removeObserer(Observer observer) {
observerList.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observerList) {
observer.update(state);
}
}
}
调用测试
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
Observer observer1 = new ConcreteObserver1("观察者 1");
Observer observer2 = new ConcreteObserver2("观察者 2");
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.setState(10);
subject.removeObserer(observer2);
subject.setState(20);
}
状态模式
对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是变成了另外一个类的对象
在状态模式中,使用类来表示状态,可以通过切换类来改变对象的状态,当需要增加新的类时,也只需要增加新的类即可
优点:
- 清晰的状态切换: 状态模式将每个状态的行为集中在各自的状态类中,使得状态切换的逻辑变得清晰,易于管理和修改。
- 可维护性:将状态相关的代码分布在不同的状态类中,使得代码更加模块化和可维护。
- 扩展性:添加新的状态只需要创建新的状态类并实现相关操作,不会影响到其他状态类或上下文类的代码。 避免条件语句:状态模式避免了大量的条件语句,从而提高了代码的可读性和可维护性。
- 复用性: 状态类之间的逻辑可以被复用,因为它们是独立的实体。
缺点:
- 状态模式的使用必然会增加系统的类与对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
结构:
状态模式包含以下主要角色:
- 环境(Context)角色:也称为上下文,它定义了客户感兴趣的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
- 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
- 具体状态(Concrete State)角色:实现抽象状态所对应的行为
场景:
- 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
- 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
实例:
这里举例一个电梯的开门状态以及关门状态
/*状态接口*/
public interface ElevatorState {
/*开门*/
void openDoors();
/*关门*/
void closeDoors();
}
/*具体状态类 开门状态时*/
public class OpenState implements ElevatorState{
@Override
public void openDoors() {
System.out.println("当前电梯门已是打开状态");
}
@Override
public void closeDoors() {
System.out.println("关闭电梯门");
}
}
/*具体状态类 关门状态时*/
public class CloseState implements ElevatorState{
@Override
public void openDoors() {
System.out.println("打开电梯门");
}
@Override
public void closeDoors() {
System.out.println("当前门已是关闭状态");
}
}
/*环境角色(上下文) 电梯*/
public class Elevator {
private ElevatorState elevatorState;
public Elevator(){
elevatorState = new CloseState();//初始化状态为关门状态
}
public ElevatorState getElevatorState() {
return elevatorState;
}
public void setElevatorState(ElevatorState elevatorState) {
this.elevatorState = elevatorState;
}
public void openDoors() {
elevatorState.openDoors();
}
public void closeDoors() {
elevatorState.closeDoors();
}
}
调用测试
public static void main(String[] args) {
Elevator elevator = new Elevator();
elevator.closeDoors();
elevator.openDoors();
elevator.setElevatorState(new OpenState());
elevator.openDoors();
elevator.closeDoors();
}
在上面实例中,我们创建了一个模拟电梯系统,其中有开门状态和关门状态两个具体状态类,以及电梯类作为上下文类。
通过切换状态,电梯在不同状态下有不同的行为表现。这就是状态模式的基本思想。
策略模式
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
优点:
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
- 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
缺点:
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 策略模式造成很多的策略类。
结构:
策略模式的主要角色:
- 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用
场景:
- 需要在不同算法之间灵活切换的情况。
- 有多个类似的条件语句,可以使用策略模式提取出这些条件逻辑。
- 需要封装一些具体的算法,使其独立于客户端而变化。
实例:
这里举例一个数据解析的策略:
/*抽象策略接口 我们定义一个接口 DataParsing,表示数据解析的策略*/
public interface DataParsing {
String dataParsing(List<String> datas);
}
/*具体策略,实现第一种数据解析方案*/
public class Parsing1 implements DataParsing{
@Override
public String dataParsing(List<String> datas) {
String data = datas.get(0);
return data;
}
}
/*具体策略,实现第二种数据解析方案*/
public class Parsing2 implements DataParsing{
@Override
public String dataParsing(List<String> datas) {
String data = datas.get(1);
return data;
}
}
/*环境类 系统环境类 它接受一个数据解析策略,并根据用户的选择执行相应的操作*/
public class SystemContext {
private DataParsing dataParsing;
public void setDataParsing(DataParsing dataParsing){
this.dataParsing = dataParsing;
}
/*执行操作*/
public String performOperation(List<String> datas) {
if (datas.size()>=2) {
return dataParsing.dataParsing(datas);
}
throw new IllegalStateException("数据不满足执行解析的条件");
}
}
public static void main(String[] args) {
DataParsing dataParsing1 = new Parsing1();
DataParsing dataParsing2 = new Parsing2();
SystemContext systemContext = new SystemContext();
List<String> list = new ArrayList<String>();
list.add("解析方式1");
list.add("解析方式2");
systemContext.setDataParsing(dataParsing1);
System.out.println(systemContext.performOperation(list));;
systemContext.setDataParsing(dataParsing2);
System.out.println(systemContext.performOperation(list));;
list.clear();
systemContext.performOperation(list);
}
在上面实例中,我们通过创建不同的数据解析策略类来实现不同解析逻辑,获取指定数据功能,这里省略具体的业务逻辑,并通过设置不同的策略来执行不同的操作,具体实现根据业务实际需求来实现不同的解析数据的逻辑。这就是策略模式的基本思想,
模板模式
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
优点:
- 代码复用: 核心算法结构在父类中定义,可以被多个子类共享,避免了重复的代码。
- 灵活性:子类可以通过实现特定的步骤来定制算法的行为,而不需要改变算法的整体结构。
- 可维护性: 将算法的核心结构集中在一个地方,易于维护和修改。
- 代码一致性: 所有子类共享相同的算法模板,确保了算法的一致性。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度
结构:
模板方法模式包含以下主要角色:
-
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下。
① 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
② 基本方法:是整个算法中的一个步骤,包含以下几种类型。
-
抽象方法:在抽象类中申明,由具体子类实现。 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。 -
具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。
场景:
- 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
- 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
- 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
实例:
/*抽象类*/
public abstract class AbstractClass {
/*模板方法 ,定义算法的骨架*/
public void templateMethod(){
step1();
step2();
step3();
}
// 基本方法,子类需要实现
abstract void step1();
abstract void step2();
abstract void step3();
}
/*具体子类实现抽象方法*/
public class ConcreteClass extends AbstractClass{
@Override
void step1() {
System.out.println("step1");
}
@Override
void step2() {
System.out.println("step2");
}
@Override
void step3() {
System.out.println("step3");
}
}
调用测试
public static void main(String[] args) {
AbstractClass template = new ConcreteClass();
template.templateMethod();
}
在上面的示例中,AbstractClass 是模板类,定义了一个包含三个步骤的模板方法 templateMethod这些步骤由抽象方法 step1、step2 和 step3 构成。ConcreteClass 是具体子类,继承自AbstractClass,它实现了基本方法来完成每个步骤的具体行为。
在 main 方法中,我们创建了一个 ConcreteClass 实例并调用了 templateMethod,这会按照模板的结构执行具体的步骤。
访问者模式
将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
优点:
- 分离关注点:访问者模式将元素类与具体操作分离,使得每个类可以专注于自身的职责,而操作则由访问者来实现。
- 易于扩展:添加新的操作只需要增加一个新的访问者,不需要修改已存在的元素类,因此对系统的扩展更加容易。
- 可维护性:由于每个操作被封装在独立的访问者中,使得代码更加清晰、易于维护。
- 灵活性:可以在不修改元素类的情况下,动态地添加新的操作。
缺点:
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
结构:
访问者模式包含以下主要角色:
- 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit(),该操作中的参数类型标识了被访问的具体元素。
- 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
- 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
- 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
- 对象结构(ObjectStructure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map等聚合类实现。
场景:
- 对象结构相对稳定,但其操作算法经常变化的程序。
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
- 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。
实例:
/*抽象元素类 这里举例使用设备作为元素*/
public interface DeviceElement {
void accept(DeviceVisitor deviceVisitor);
}
/*具体元素类 电脑*/
public class ConcreteElementComputer implements DeviceElement {
public void openComputer(){
System.out.println("打开电脑");
}
@Override
public void accept(DeviceVisitor deviceVisitor) {
deviceVisitor.visit(this);
}
}
/*具体元素类 音箱*/
public class ConcreteElementSpeaker implements DeviceElement {
public void openSpeaker(){
System.out.println("打开音箱");
}
@Override
public void accept(DeviceVisitor deviceVisitor) {
deviceVisitor.visit(this);
}
}
/*抽象访问者 启动设备操作的访问者*/
public interface DeviceVisitor {
void visit(ConcreteElementSpeaker concreteElementSpeaker);
void visit(ConcreteElementComputer concreteElementComputer);
}
/*具体访问者 主人*/
public class ConcreteDeviceVisitorOwner implements DeviceVisitor{
@Override
public void visit(ConcreteElementSpeaker concreteElementSpeaker) {
concreteElementSpeaker.openSpeaker();
System.out.println("主人打开音响");
}
@Override
public void visit(ConcreteElementComputer concreteElementComputer) {
concreteElementComputer.openComputer();
System.out.println("主人打开电脑");
}
}
/*具体访问者 其他人*/
public class ConcreteDeviceVisitorSomeOne implements DeviceVisitor{
@Override
public void visit(ConcreteElementSpeaker concreteElementSpeaker) {
concreteElementSpeaker.openSpeaker();
System.out.println("其他人打开音响");
}
@Override
public void visit(ConcreteElementComputer concreteElementComputer) {
concreteElementComputer.openComputer();
System.out.println("其他人打开电脑");
}
}
/*对象结构类 作为一个元素的容器并进行管理,提供让访问者对象遍历容器中的所有元素的方法*/
public class ObjectStructure {
//声明一个集合对象,用来存储元素对象
private List<DeviceElement> deviceElements = new ArrayList<>();
//添加元素
public void add(DeviceElement element) {
deviceElements.add(element);
}
public void action(DeviceVisitor deviceVisitor) {
//遍历集合,获取每一个元素,让访问者访问每一个元素
for (DeviceElement animal : deviceElements) {
animal.accept(deviceVisitor);
}
}
}
调用测试
public static void main(String[] args) {
//创建 ObjectStructure 对象
ObjectStructure objectStructure = new ObjectStructure();
//添加元素到 ObjectStructure 对象中
objectStructure.add(new ConcreteElementComputer());
objectStructure.add(new ConcreteElementSpeaker());
//创建主人对象
ConcreteDeviceVisitorOwner owner = new ConcreteDeviceVisitorOwner();
//让主人打开所有的设备
objectStructure.action(owner);
System.out.println("===============");
//创建其他人对象
ConcreteDeviceVisitorSomeOne someOne = new ConcreteDeviceVisitorSomeOne();
//让其他人打开所有的设备
objectStructure.action(someOne);
}
在上面的实例种,访问者模式允许我们在不修改设备类的情况下,通过实现不同的访问者来执行不同的操作,例如打开设备。
这样,我们可以轻松地添加新的访问者来执行其他操作,同时保持设备类的不变。
解释器模式
给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。
这里提到的文法和句子的概念同编译原理中的描述相同,“文法”指语言的语法规则,而“句子”是语言集中的元素。例如,汉语中的句子有很多,“我是中国人”是其中的一个句子,可以用一棵语法树来直观地描述语言中的句子。
优点:
- 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
- 容易实现。在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。
缺点:
- 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
- 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
- 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。
结构:
解释器模式包含以下主要角色。
- 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法interpret()。
- 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
- 非终结符表达式(NonterminalExpression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
- 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
- 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
场景:
- 当语言的文法较为简单,且执行效率不是关键问题时。
- 当问题重复出现,且可以用一种简单的语言来进行表达时。
- 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候,如 XML 文档解释。
实例:
这里举例数字的加减用表达式表示。
/*抽象表达式类*/
public interface AbstractExpression {
public int interpret(Context context);
}
/*终结符表达式类 //用于封装变量的类*/
public class TerminalExpression implements AbstractExpression{
//声明存储变量名的成员变量
private String name;
public TerminalExpression(String name) {
this.name = name;
}
@Override
public int interpret(Context context) {
//直接返回变量的值
return context.getValue(this);
}
@Override
public String toString() {
return name;
}
}
/*非终结表达式 加法表达式*/
public class Plus implements AbstractExpression{
// + 左边的表达式
private AbstractExpression left;
// + 右边的表达式
private AbstractExpression right;
public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
//将左边表达式的结果和右边的进行相加
return left.interpret(context) + right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + "+" + right.toString() + ")";
}
}
/*非终结表达式 减法表达式*/
public class Minus implements AbstractExpression {
// - 左边的表达式
private AbstractExpression left;
// - 右边的表达式
private AbstractExpression right;
public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
//将左边表达式的结果和右边的进行相减
return left.interpret(context) - right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + "-" + right.toString() + ")";
}
}
/*环境类*/
public class Context {
//定义一个 map 集合,用来存储变量以及对应的值
private Map<TerminalExpression,Integer> map = new HashMap<>();
//添加变量的功能
public void assign(TerminalExpression var, Integer value){
map.put(var, value);
}
//根据变量获取对应的值
public int getValue(TerminalExpression var){
return map.get(var);
}
}
调用测试
public static void main(String[] args) {
//创建环境对象
Context context = new Context();
//创建多个变量对象
TerminalExpression a = new TerminalExpression("a");
TerminalExpression b = new TerminalExpression("b");
TerminalExpression c = new TerminalExpression("c");
TerminalExpression d = new TerminalExpression("d");
//将变量存储到环境变量中
context.assign(a, 1);
context.assign(b, 2);
context.assign(c, 3);
context.assign(d, 4);
//获取抽象语法树 a + b - c + d
AbstractExpression expression = new Minus(a, new Plus(new Minus(b, c), d));
//解释,即计算
int result = expression.interpret(context);
System.out.println(expression + "=" + result);
}