备战面试日记(3.4) - (设计模式.23种设计模式之行为型模式)

本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试。

记录日期:2022.1.12

大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习。

设计模式-23种设计模式之结构型模式

这部分我之前的学习,是通过博客,以及《Spring 5核心原理与30个类手写实战》这本书学习的,这本书前面会介绍设计模式多一点。

当然比较推荐的是《Java设计模式》。

有一篇比较详细的博客链接参考:史上最全设计模式导学目录(完整版)

设计模式分类

设计模式介绍

设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。

设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。

正确使用设计模式具有以下优点。

  • 可以提高程序员的思维能力、编程能力和设计能力。
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

一般主要讲一下23种设计模式。

创建型模式

创建型设计模式,是指在创建对象的同时隐藏创建逻辑,不使⽤ new 直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括下面五种模式:

  • 工厂方法模式(Factory Method)
  • 抽象工厂模式(Abstract Factory)
  • 单例模式(Singleton)
  • 建造者模式(Builder)
  • 原型模式(Prototype)

一般说二四种设计模式的时候,是指23个GoF设计模式+简单工厂模式,简单工厂模式属于创建型模式,待会也可以提一下。

科普一下GOF:GOF是设计模式的经典名著Design Patterns: Elements of Reusable Object-Oriented Software(中译本名为《设计模式——可复用面向对象软件的基础》)的四位作者,他们分为是:Elich Gamma、Richard Helm、Ralph Johnson、以及John Vlissides。这四个人常被称为Gang of Four, 即四人组,简称Gof。 他们总结了23个设计模式。

结构型模式

结构型设计模式,是指通过类和接⼝间的继承和引⽤实现创建复杂结构的对象。包括下面七种模式:

  • 适配器模式(Adapter)

  • 装饰器模式(Decorator)

  • 代理模式(Proxy)

  • 外观模式(Facade)

  • 桥接模式(Bridge)

  • 组合模式(Composite)

  • 享元模式(Flyweight)

行为型模式

行为型设计模式,是指通过类之间不同通信方式实现不同行为。包括下面十一种模式:

  • 策略模式(Strategy)

  • 模板方法模式(Template Method)

  • 观察者模式(Observer)

  • 迭代器模式(Iterator)

  • 责任链模式(Chain of Responsibility)

  • 命令模式(Command)

  • 备忘录模式(Memento)

  • 状态模式(State)

  • 访问者模式(Visitor)

  • 中介者模式(Mediator)

  • 解释器模式(Interpreter)

上面二十三种中重点学习的设计模式我用加粗标出来,大部分是平时有用到的设计模式。

行为型模式

策略模式
概念

策略模式,是指将对象和行为分开,将行为定义为 一个行为接口具体行为的实现。策略模式最大的特点是行为的变化,行为之间可以相互替换。可以是对一系列的算法定义,并将每一个算法封装起来,而且使它们还可以相互替换。此模式让算法的变化独立于使用算法的客户。

模型结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gX3ixTL5-1641917725061)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220110214116483.png)]

策略模式包含如下几个角色:

  • 环境类(Context):环境类是使用算法的角色,它在解决某个问题(即实现某个功能)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
  • 抽象策略类(Strategy):抽象策略类为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
  • 具体策略类(ConcreteStrategy):具体策略类实现了在抽象策略类中声明的算法,在运行时具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务功能。
示例代码

假设我们去新华书店买书,书店推出会员服务,对初级会员不打折,对中级会员打九折,对高级会员打八折,模拟代码如下:

传统代码
// 在书店购买一本书
public Double buyBook(String type, Double price) {
    //中级会员计费
    if (type.equals("intermediateMember")) {
        return price * 0.9;
    }
    //高级会员计费
    if (type.equals("advanceMember")) {
        return price * 0.8;
    }
    //普通会员计费
    return price;
}

传统的实现方式,通过传统if代码判断。这样就会导致后期的维护性非常差。当后期需要新增计费方式,还需要在这里再加上if(),也不符合设计模式的开闭原则。

使用策略模式代码
// 编写抽象策略类
public interface MemberStrategy {
    // 一个计算价格的抽象方法
    //price商品的价格
    public double calcPrice(double price);
}

// 普通会员
public class PrimaryMemberStrategy implements MemberStrategy { // 实现策略A
    //重写策略方法具体实现功能
    @Override
    public double calcPrice(double price) {
        return price;
    }
}

// 中级会员 打百分之10的折扣
public class IntermediateMemberStrategy implements MemberStrategy {
    @Override
    public double calcPrice(double price) {
        return price * 0.9;
    }
}

// 高级会员类 20%折扣
public class AdvanceMemberStrategy implements MemberStrategy {
    @Override
    public double calcPrice(double price) {
        return price * 0.8;
    }
}

我们再来编写上下文类也叫环境类,用来起承上启下的作用:

/**
 * 负责和具体的策略类交互
 * 这样的话,具体的算法和直接的客户端调用分离了,使得算法可以独立于客户端独立的变化。
 */
// 上下文类/环境类
public class MemberContext {
    // 用户折扣策略接口
    private MemberStrategy memberStrategy;

    // 注入构造方法
    public MemberContext(MemberStrategy memberStrategy) {
        this.memberStrategy = memberStrategy;
    }

    // 计算价格
    public double qoutePrice(double price){
        // 通过接口变量调用对应的具体策略
        return memberStrategy.calcPrice(goodsPrice);
    }
}

我们来最后编写测试代码:

public static void main(String[] args) {
    // 具体行为策略
    MemberStrategy primaryMemberStrategy = new PrimaryMemberStrategy(); // 接口回调(向上转型)
    MemberStrategy intermediateMemberStrategy = new IntermediateMemberStrategy();
    MemberStrategy advanceMemberStrategy = new AdvanceMemberStrategy();

    //计算一本300块钱的书
    // 用户选择不同策略
    MemberContext context = new MemberContext(primaryMemberStrategy);
    System.out.println("普通会员的价格:"+ primaryContext.qoutePrice(300));// 普通会员:300
    context = new MemberContext(intermediateMemberStrategy);
    System.out.println("中级会员的价格:"+ intermediateContext.qoutePrice(300));// 中级会员 270
    context = new MemberContext(advanceMemberStrategy);
    System.out.println("高级会员的价格:"+ advanceContext.qoutePrice(300));// 高级会员240 
}
模式优点
  • 策略模式提供了对“开闭原则”的完美支持,用户可以在不 修改原有系统的基础上选择算法或行为,也可以灵活地增加 新的算法或行为。
  • 策略模式提供了管理相关的算法族的办法。
  • 策略模式提供了可以替换继承关系的办法。
  • 使用策略模式可以避免使用多重条件转移语句。
模式缺点
  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一 定程度上减少对象的数量。
使用场景
  1. 如果在一个系统里面有许多类,它们之间的区别仅在于它们 的行为,那么使用策略模式可以动态地让一个对象在许多行 为中选择一种行为。
  2. 一个系统需要动态地在几种算法中选择一种。
  3. 如果一个对象有很多的行为,如果不用恰当的模式,这些行 为就只好使用多重的条件选择语句来实现。
  4. 不希望客户端知道复杂的、与算法相关的数据结构,在具体 策略类中封装算法和相关的数据结构,提高算法的保密性与 安全性。

现实生活中举例说明:

电商网站支付方式,一般分为银联、微信、支付宝,可以采用策略模式
电商网站活动方式,一般分为满减送、限时折扣、包邮活动,拼团等可以采用策略模式

最佳示例

jdk的Arrays数组工具类中的sort()方法运用了策略模式。

//相当于context上下文角色
public class Arrays {
	//sort方法中,参数Comparator<? super T> c传进来的据相当于一个具体的策略,看客户端调用,我们是如何传进去一个具体的策略的
	public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
}

里面通过传递Comparator接口的实现类来使用对应策略的sort方式:

//相当于抽象策略类
public interface Comparator<T> {
	int compare(T o1, T o2);
}

调用案例:

public static void main(String[] args){
    Integer[] data={9,1,2,8,4,3};
    //实现一个Comparator接口,相当于创建了一个具体的策略类
    Comparator<Integer> comparator=new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            if (o1>o2){
                return 1;
            }else {
                return -1;
            }
        }
    };
    //在Arrays中传入了具体的策略类
    Arrays.sort(data,comparator);
    System.out.println(Arrays.toString(data));
}
模板方法模式
概念

模板方法模式,即定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

模型结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OojHwPcg-1641917725062)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220110230649028.png)]

模板方法模式涉及到的角色有两个角色:

  1. 抽象模板角色(AbstractClass):定义一组基本方法供子类实现,定义并实现组合了基本方法的模板方法。
  2. 具体模板角色(ConcreteClass):实现抽象模板角色定义的基本方法。

模板方法模式还涉及到以下方法的概念:

  • 基本方法

    • 抽象方法:由抽象模板角色声明,abstract修饰,具体模板角色实现。

    • 钩子方法:由抽象模板角色声明并实现,具体模板角色可实现加以扩展。

    • 具体方法:由抽象模板角色声明并实现,而子类并不实现。

  • 模板方法

    • 抽象模板角色声明并实现,负责对基本方法的调度,一般以final修饰,不允许具体模板角色重写。模板方法一般也是一个具体方法。
示例代码

我们以汽车为例子,对于不同的车型,肯定有不同的操作,我们来模拟一下场景:

// 汽车模板抽象类
public abstract class DriveTemplate {
    public final void drive() {
        openDoor();
        startEngine();
        gear();
        go();
        brake();
        stop();
    }
    
    protected abstract void openDoor();
    
    protected void startEngine() {
        System.out.println("engine started !");
    }
    
    protected abstract void gear();
    
    protected void go() {
        System.out.println("running...");
    }
    
    protected abstract void brake();
    
    protected void stop() {
        System.out.println("stopped !");
    }
}

我们再来编写几种不同汽车的实现:

// 小型巴士
public class Minibus extends DriveTemplate {

    @Override
    protected void openDoor() {
        System.out.println("keyless entry");
    }

    @Override
    protected void gear() {
        System.out.println("gear with hand");
    }

    @Override
    protected void brake() {
        System.out.println("brake with foot");
    }
}
// 摩托车
public class Motorcycle extends DriveTemplate {

    @Override
    protected void openDoor() {
        System.out.println("no door actually");
    }

    @Override
    protected void gear() {
        System.out.println("gear with foot");
    }

    @Override
    protected void brake() {
        System.out.println("brake with hand");
    }
}

编写测试代码:

public class MyTest {
    
    @Test
    public void test() {
//        DriveTemplate template = new SuzukiStrom1000();
        DriveTemplate template = new SuzukiScross();
        template.drive();
    }
}

我们不希望子类改变算法的结构或顺序,但是在某种场景中,我们希望子类能有一些自主权,虽然它们不能覆盖drive方法,但是我们依然希望子类可以自己决定一些东西,那么模板方法模式能否满足这一需求呢?

答案是肯定的,我们来设想这种场景,当我们在开小型巴士的时候,我希望可以打开车子的MP3功能来听歌,但是骑摩托车的时候则不需要。

我们可以在超类中定义了一个music的方法,但是它并不是一个抽象方法,这样子类可以自己决定是否覆盖该方法,该方法返回值是一个布尔值的标志位,默认为false。子类Minibus覆盖了该方法但是Motorcycle则没有,我们再来看看具体的实现:

public abstract class DriveTemplate {
    
    public final void drive() {
        openDoor();
        startEngine();
        gear();
        go();
        if (music()) {
            mp3();
        }
        brake();
        stop();
    }
    
    protected abstract void openDoor();
    
    protected void startEngine() {
        System.out.println("engine started !");
    }
    
    protected abstract void gear();
    
    protected void go() {
        System.out.println("running...");
    }
    
    private void mp3() {
        System.out.println("music is good");
    }
    
    protected boolean music() {
        return false;
    }
    
    protected abstract void brake();
    
    protected void stop() {
        System.out.println("stopped !");
    }
}

编写不同类型的汽车实现:

public class Minibus extends DriveTemplate {
    
    @Override
    protected void openDoor() {
        System.out.println("keyless entry");
    }

    @Override
    protected void gear() {
        System.out.println("gear with hand");
    }

    @Override
    protected void brake() {
        System.out.println("brake with foot");
    }

    @Override
    protected boolean music() {
        return true;
    }
}
模式优点
  • 良好的封装性。把公有的不变的方法封装在父类,而子类负责实现具体逻辑。
  • 良好的扩展性:增加功能由子类实现基本方法扩展,符合单一职责原则和开闭原则。
  • 复用代码。
模式缺点
  • 由于是通过继承实现代码复用来改变算法,灵活度会降低。
  • 子类的执行影响父类的结果,增加代码阅读难度。
使用场景
  1. 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  2. 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  3. 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
最佳示例
JDBC

每次数据库都要设置连接信息,就是DataSource这个接口,这个接口是javax.sql包下的,是jdk带的接口,意思是说哪种数据库想通过java语言进行连接,就要实现DataSource这个官方的接口。比如如果引入mysql的驱动,那么就会有一个MysqlDataSource实现了DataSource这个接口。

JdbcTemplate有一个传入DataSource的的构造方法。

private DataSource dataSource;

public JdbcTemplate(DataSource dataSource) {
    this.setDataSource(dataSource);
    this.afterPropertiesSet();
}

通过构造方法来设置DataSource的实现。设置完成之后,即可使用JdbcTemplate,最多的方法可能就是execute方法。

这个方法可以分为三步:

  1. 建立连接
  2. 传输信息
  3. 关闭连接

我们来看一下具体代码:

public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    Assert.notNull(action, "Callback object must not be null");
    //1 获取连接,通过con来接收连接信息
    Connection con = DataSourceUtils.getConnection(this.getDataSource());
    Statement stmt = null;

    Object var7;
    try {
        Connection conToUse = con;
        if(this.nativeJdbcExtractor != null && this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
            conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
        }

        stmt = conToUse.createStatement();
        this.applyStatementSettings(stmt);
        Statement stmtToUse = stmt;
        if(this.nativeJdbcExtractor != null) {
            stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
        }
        //2 传输并且接收返回的信息
        T result = action.doInStatement(stmtToUse);
        this.handleWarnings(stmt);
        var7 = result;
    } catch (SQLException var11) {
        JdbcUtils.closeStatement(stmt);
        stmt = null;
        DataSourceUtils.releaseConnection(con, this.getDataSource());
        con = null;
        throw this.getExceptionTranslator().translate("StatementCallback", getSql(action), var11);
    } finally {
        //3.关闭连接
        JdbcUtils.closeStatement(stmt);
        DataSourceUtils.releaseConnection(con, this.getDataSource());
    }

    return var7;
}

由以上代码发现,不管使用mysql还是Oracle,如果想使用JdbcTemplate,必须要实现DataSource接口,通过传入DataSource创建JdbcTemplate对象。JdbcTemplate的方法已经固定,不管什么数据库都要遵循这种执行流程。

观察者模式
概念

观察者模式(Observer)又称作发布-订阅模式,就是指多个对象间存在一对多的关系,当被观察者状态发生改变时,所有观察者都得到通知并自动更新

模型结构图
image-20220111192601244

观察者模式由以下几部分组成:

  • 抽象目标角色(Subject):目标角色知道它的观察者,可以有任意多个观察者观察同一个目标。并且提供注册和删除观察者对象的接口。目标角色往往由抽象类或者接口来实现。
  • 抽象观察者角色(Observer):为那些在目标发生改变时需要获得通知的对象定义一个更新接口。抽象观察者角色主要由抽象类或者接口来实现。
  • 具体目标角色(ConcreteSubject):将有关状态存入各个Concrete Observer 对象。当它的状态发生改变时, 向它的各个观察者发出通知。
  • 具体观察者角色(ConcreteObserver):存储有关状态,这些状态应与目标的状态保持一致。实现Observer 的更新接口以使自身状态与目标的状态保持一致。在本角色内也可以维护一个指向Concrete Subject 对象的引用。

Subject 这个抽象类中,提供了上面提到的功能,而且存在一个通知方法:notify()

还可以看到SubjectConcreteSubject 之间可以说是使用了模板模式(这个模式真是简单普遍到一不小心就用到了)。

这样当具体目标角色的状态发生改变,按照约定则会去调用通知方法,在这个方法中则会根据目标角色中注册的观察者名单来逐个调用相应的update 方法来调整观察者的状态。

示例代码

假设一个微信公众号,被多个微信用户订阅,当它发布一篇新文章时,所有微信用户都受到它的推送信息,我们来模拟一下场景。

先编写抽象观察者角色的代码:

// 抽象观察者角色,用于接收信息推送
public interface Observer {
    public void update(String message);
}

再编写具体观察者角色的代码:

// 具体观察者角色,微信用户类
public class WeixinUser implements Observer {
    // 微信用户名
    private String name;
    public WeixinUser(String name) {
        this.name = name;
    }
    @Override
    public void update(String message) {
        System.out.println(name + "接收到了" + message);
    }
}

现在已经有了微信用户,再来编写公众号类。

首先先编写抽象目标角色,提供了attach、detach、notify三个方法:

// 抽象目标角色
public interface Subject {
    /**
     * 增加订阅者
     * @param observer
     */
    public void attach(Observer observer);
    /**
     * 删除订阅者
     * @param observer
     */
    public void detach(Observer observer);
    /**
     * 通知订阅者更新消息
     */
    public void notify(String message);
}

微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法:

// 微信公众号类
public class SubscriptionSubject implements Subject {
    //储存订阅公众号的微信用户
    private List<Observer> weixinUserlist = new ArrayList<Observer>();

    @Override
    public void attach(Observer observer) {
        weixinUserlist.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        weixinUserlist.remove(observer);
    }

    @Override
    public void notify(String message) {
        for (Observer observer : weixinUserlist) {
            observer.update(message);
        }
    }
}

最后,我们来测试一波代码:

public class Client {
    public static void main(String[] args) {
        SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
        //创建微信用户
        WeixinUser user1=new WeixinUser("张三");
        WeixinUser user2=new WeixinUser("李四");
        WeixinUser user3=new WeixinUser("王五");
        //订阅公众号
        mSubscriptionSubject.attach(user1);
        mSubscriptionSubject.attach(user2);
        mSubscriptionSubject.attach(user3);
        //公众号更新发出消息给订阅的微信用户
        mSubscriptionSubject.notify("备战面试日记更新了!");
    }
}
模式优点
  • 被观察者和观察者之间是抽象耦合的。
  • 耦合度较低,两者之间的关联仅仅在于消息的通知。
  • 被观察者无需关心他的观察者。
  • 支持广播通信。
模式缺点
  • 观察者只知道被观察对象发生了变化,但不知变化的过程和缘由。
  • 观察者同时也可能是被观察者,消息传递的链路可能会过长,完成所有通知花费时间较多。
  • 如果观察者和被观察者之间产生循环依赖,或者消息传递链路形成闭环,会导致无限循环。
使用场景
  1. 需要在系统中建立一个单项广播的触发机制。
  2. 系统中某个对象的行为会影响若干其他对象。
  3. 对象之间的关联关系可以在运行时动态的建立与撤销。
  4. 对象之间的关联关系呈现出一种树状结构。
最佳示例
netty的AbstractChannel

AbstractChannel#writeAndFlush()

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
	...
	public ChannelFuture writeAndFlush(Object msg) {
		return this.pipeline.writeAndFlush(msg);
	}
	public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
		return this.pipeline.writeAndFlush(msg, promise);
	}
	...
}
迭代器模式
概念

迭代器(Iterator)模式又叫作游标(Cursor)模式。可以提供一种方法来顺序访问一个聚合(指一组对象的组合结构,如:Java中的集合、数组等)对象中各个元素,而又不需暴露该对象的内部表示

迭代器模式的本质:控制访问聚合对象中的元素。

迭代器模式的设计意图:无须暴露聚合对象的内部实现,就能够访问到聚合对象中的各个元素。

模式结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hKNE5n7Q-1641917725062)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220111202114000.png)]

迭代器模式由以下几部分组成:

  • 抽象迭代器角色(Iterator): 一般定义为接口,用来定义访问和遍历元素的接口。
  • 具体迭代器角色(ConcreteIterator): 实现对聚合对象的遍历,并跟踪遍历时的当前位置。
  • 抽象聚合角色(Aggregate): 定义创建相应迭代器对象的接口。
  • 具体聚合角色(ConcreteAggregate): 实现创建相应的迭代器对象。
示例代码

我们使用迭代器模式来管理多个学生的信息。

我们先来编写要用到的迭代器类:

// 抽象迭代器角色
public interface StudentIterator {

    // 获取下一个学生对象
    Student nextStudent();
    
    // 是否是最后一个
    boolean isLast();
}

// 具体迭代器角色,学生迭代器类
public class StudentIteratorImpl implements StudentIterator{
    /**
     * 学生集合, 通过构造函数注入
     */
    private ArrayList<Student> list;

    /**
     * 当前处理的集合索引
     */
    private int position;

    /**
     * 当前处理的学生对象
     */
    private Student student;

    public StudentIteratorImpl(ArrayList<Student> list) {
        this.list = list;
    }

    @Override
    public Student nextStudent() {
        System.out.println("返回 " + position + " 位置的学生对象 : " + student);
        student = list.get(position);
        position++;
        return student;
    }

    @Override
    public boolean isLast() {
        return position < list.size() ? false : true;
    }
}

再来补充学生实体类:

// 学生实体类
public class Student {
    String name;

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}

再来编写学生管理的抽象聚合类和具体聚合类:

// 集合的管理接口
public interface StudentAggregate {

    // 增加学生
    void addStudent(Student student);

    // 删除学生
    void removeStudent(Student student);

    // 获取学生集合的迭代器
    StudentIterator getStudentIterator();
}


// 维护集合的管理类
public class StudentAggregateImpl implements StudentAggregate{
    // 学生集合
    private ArrayList<Student> list;

    public StudentAggregateImpl() {
        this.list = new ArrayList<>();
    }

    @Override
    public void addStudent(Student student) {
        this.list.add(student);
    }

    @Override
    public void removeStudent(Student student) {
        this.list.remove(student);
    }

    @Override
    public StudentIterator getStudentIterator() {
        return new StudentIteratorImpl(this.list);
    }
}

现在我们来编写具体测试代码:

public static void main(String[] args) {
    // 创建 3 个学生对象
    Student tom = new Student("Tom");
    Student jerry = new Student("Jerry");
    Student trump = new Student("Trump");

    // 构造学生对象集合
    StudentAggregate studentAggregate = new StudentAggregateImpl();
    studentAggregate.addStudent(tom);
    studentAggregate.addStudent(jerry);
    studentAggregate.addStudent(trump);

    // 获取学生对象的迭代器
    StudentIterator studentIterator = studentAggregate.getStudentIterator();
    // 判断是否是最后一个对象 , 如果不是 , 获取下一个对象 , 并打印
    while (!studentIterator.isLast()) {
        Student student = studentIterator.nextStudent();
        System.out.println(student);
    }

    // 删除一个对象
    studentAggregate.removeStudent(trump);
    System.out.println("删除 Trump" );

    studentIterator = studentAggregate.getStudentIterator();
    // 判断是否是最后一个对象 , 如果不是 , 获取下一个对象 , 并打印
    while (!studentIterator.isLast()) {
        Student student = studentIterator.nextStudent();
        System.out.println(student);
    }
}
模式优点
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。
  • 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
模式缺点
  • 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
  • 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展,例如JDK内置迭代器Iterator就无法实现逆向遍历,如果需要实现逆向遍历,只能通过其子类ListIterator等来实现,而ListIterator迭代器无法用于操作Set类型的聚合对象。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情。
使用场景
  1. 如果你希望为遍历不同的聚合对象提供一个统一的接口,可以使用迭代器模式。
  2. 如果你希望有多种遍历方式可以访问聚合对象,可以使用迭代器模式。
  3. 如果你希望提供访问一个聚合对象的内容,但是又不想暴露它的内部表示的时候,可以使用迭代器模式来提供迭代器接口,从而让客户端只是通过迭代器的接口来访问聚合对象,而无须关心聚合对象的内部实现。
最佳示例
java.lang.Iterable

所有实现了java.lang.Iterable接口的类对象,都可以被应用for-each loop语句。
建议使用 for-each loop 遍历此接口的类对象,除非需要对元素做更新(增加、删除)操作。

public interface Iterable<T>{

	default void foreach(Consumer<T> consumer){
		for (T t : this) {		// for-each loop
            consumer.accept(t);
        }
	}
	
	Iterator<T> iterator();
}
java.util.Iterator

开发者通过调用next()接口方法,可以遍历集合中所有元素、可以更新元素的数据、也可以remove()移除元素。

public interface Iterator<T>{
	boolean hasNext();
	T next();
	default void remove() {
        throw new UnsupportedOperationException("remove");
    }
	default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

可以自己去看各个Collection实现类对Iterable接口的实现。

责任链模式
概念

责任链模式,是指避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止

模型结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vV7ONTMi-1641917725063)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220111202601727.png)]

职责链模式主要包含以下几个角色:

  • 抽象处理者角色(Handler):定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者角色(Concrete Handler):实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类角色(Client):创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
示例代码

公司底层员工想要请假,需要将请假条向上级领导审批,先由主管审批,再由经理审批,最后由总经理审批。

其中主管审批三天内假期;经理审批七天内假期;总经理审批十五天内假期。

使用代码模拟如下:

// 请假条类
@Data
@AllArgsConstructor
public class LeaveRequest {
    // 请假人姓名
    private String name;
    // 请假天数
    private int days;
}

编写抽象处理者以及具体处理者实现,包括主管、经理、总经理:

// 抽象处理者类
@Data
public abstract class Handler {
    // 处理者姓名
    protected String name;
    // 下一个处理者
    protected Handler nextHandler;

    Handler(String name) {
        this.name = name;
    }
    //处理请假
    public abstract boolean process(LeaveRequest leaveRequest);
}

// 主管类
public class Director extends Handler {
    public Director(String name) {
        super(name);
    }

    @Override
    public boolean process(LeaveRequest leaveRequest) {
        boolean result = (new Random().nextInt(10)) > 3;
        // 不批准
        if (result == false) {
            return false;
        } else if (leaveRequest.getDays() < 3) { // 批准且天数小于3,返回true
            return true;
        }
        // 批准且天数大于等于3,提交给下一个处理者处理
        return nextHandler.process(leaveRequest);
    }
}

// 经理类
public class Manager extends Handler {
    public Manager(String name) {
        super(name);
    }

    @Override
    public boolean process(LeaveRequest leaveRequest) {
        // 随机数大于3则为批准,否则不批准
        boolean result = (new Random().nextInt(10)) > 3;
        if (result == false) {
            return false;
        } else if (leaveRequest.getDays() < 7) { // 批准且天数小于7
            return true;
        }
        // 批准且天数大于等于7,提交给下一个处理者处理
        return nextHandler.process(leaveRequest);
    }
}

// 总经理
public class GeneralManager extends Handler {
    public GeneralManager(String name) {
        super(name);
    }

    @Override
    public boolean process(LeaveRequest leaveRequest) {
        // 随机数大于3则为批准,否则不批准
        boolean result = (new Random().nextInt(10)) > 3;
        if (result == false) { // 总经理不批准
            return false;
        }
        // 总经理最后批准
        return true;
    }
}

最后编写测试代码进行测试:

public static void main(String[] args) {
    Handler director = new Director("主管");
    Handler manager = new Manager("经理");
    Handler generalManager = new GeneralManager("总经理");

    // 创建责任链
    director.setNextHandler(manager);
    manager.setNextHandler(generalManager);

    // 发起请假申请
    boolean result1 = director.process(new LeaveRequest(" 王五", 2));
    System.out.println("最终结果:" + result1);

    boolean result2 = director.process(new LeaveRequest("张三", 4));
    System.out.println("最终结果:" + result2);

    boolean result3 = director.process(new LeaveRequest("李四", 8));
    System.out.println("最终结果:" + result3);
}
模式优点
  • 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
  • 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
  • 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
  • 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
  • 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
模式缺点
  • 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  • 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  • 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
使用场景
  1. 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。

  2. 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。

  3. 可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。

最佳示例
Tomcat 过滤器

Servlet 过滤器是可用于 Servlet 编程的 Java 类,可以实现以下目的:在客户端的请求访问后端资源之前,拦截这些请求;在服务器的响应发送回客户端之前,处理这些响应。

Servlet 定义了过滤器接口 Filter 和过滤器链接口 FilterChain 的源码如下:

public interface Filter {
    public void init(FilterConfig filterConfig) throws ServletException;
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
    public void destroy();
}

public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

我们可以通过实现接口来编写自定义过滤器,并在web.xml中配置该过滤器。

第一步:写一个过滤器类,实现 javax.servlet.Filter 接口。

public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        // 做一些自定义处理....
        System.out.println("执行doFilter()方法之前...");
        chain.doFilter(request, response);              // 传递请求给下一个过滤器
        System.out.println("执行doFilter()方法之后...");
    }

    @Override
    public void destroy() {
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
}

第二步:在 web.xml 文件中增加该过滤器的配置,譬如下面是拦截所有请求。

<filter>  
        <filter-name>MyFilter</filter-name>  
        <filter-class>com.jcx.filter.MyFilter</filter-class>  
</filter>
  
<filter-mapping>  
        <filter-name>MyFilter</filter-name>  
        <url-pattern>/*</url-pattern>  
</filter-mapping>

此时,我们启动 Tomcat 时我们的过滤器就可以发挥作用了。

具体调用链路就不解析了。

命令模式
概念

命令(Command)模式,即将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理

模型结构图

命令模式可以将系统中的相关操作抽象成命令,使调用者与实现者相关分离。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jnhKnMD6-1641917725063)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220111204304117.png)]

命令模式包含以下主要角色。

  • 抽象命令类角色(Command):声明执行命令的接口,拥有执行命令的抽象方法 execute()。
  • 具体命令角色角色(Concrete Command):是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
  • 实现者/接收者角色(Receiver):执行命令功能的相关操作,是具体命令对象业务的真正实现者。
  • 调用者/请求者角色(Invoker):是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
示例代码

首先我们来编写一个电视机类,电视机可以播放不同的频道。

//电视机对象:提供了播放不同频道的方法
public class Television {
    
    public void playCctv1() {
        System.out.println("--CCTV1--");
    }

    public void playCctv2() {
        System.out.println("--CCTV2--");
    }

    public void playCctv3() {
        System.out.println("--CCTV3--");
    }

    public void playCctv4() {
        System.out.println("--CCTV4--");
    }

    public void playCctv5() {
        System.out.println("--CCTV5--");
    }

    public void playCctv6() {
        System.out.println("--CCTV6--");
    }

}

传统代码

在传统代码中,实现看电视的功能很容易,首先我们编写一个看电视的人的类Watcher,内部持有电视机的引用即可。

//观看电视的人的类
public class Watcher {
    //持有一个
    public Television tv;

    public Watcher(Television tv) {
        this.tv = tv;
    }

    public void playCctv1() {
        tv.playCctv1();
    }

    public void playCctv2() {
        tv.playCctv2();
    }

    public void playCctv3() {
        tv.playCctv3();
    }

    public void playCctv4() {
        tv.playCctv4();
    }

    public void playCctv5() {
        tv.playCctv5();
    }

    public void playCctv6() {
        tv.playCctv6();
    }
}

这样我们就可以简单调用它一下:

public static void main(String args[]) {
    Watcher watcher = new Watcher(new Television());
    watcher.playCctv1();
    watcher.playCctv2();
    watcher.playCctv3();
    watcher.playCctv4();
    watcher.playCctv5();
    watcher.playCctv6();
}

通过上述例子我们可以看出,Watcher 类和Television 电视机类完全的耦合在一起了,目前本文的电视机对象只能播放六个电视台,如果需要增添全国所有主流卫视的话,需要做如下改动:

  1. 修改Television对象,增加若干个的playXXTV()的方法来播放不同的卫视。
  2. 修改Watcher,也添加若干个对应的playXXTV()的方法,调用TelevisionplayXXTV()
// Television#playXXTV()
public void playXXTV() {
    tv.playXXTV();
}

但是这么修改显违背了“对修改关闭,对扩展开放“的重要设计原则。

别的不说,就拿本看电视来说,比如调用playXXTV()的顺序是随即的,也就是你胡乱切换了一通:比如你沿着cctv1、cctv2、cctv3、cctv4、xxtv、yytv…nntv的顺序来看电视,也就是发生了如下调用:

watcher.playCctv1();
watcher.playCctv2();
watcher.playCctv3();
watcher.playCctv4();
watcher.playCctv5();
watcher.playCctv6();
watcher.playXXtv();
watcher.playYYtv();
watcher.playNNtv();

当前你在看nntv,如果你想看上一个看过的台也就是yytv,这很简单,只要在playNNtv()后面,调用watcher.playYYtv()即可,但是如果你想要一直回退到cctv1呢?那么你就话发生如下可怕的调用:

watcher.playCctv1();
watcher.playCctv2();
watcher.playCctv3();
watcher.playCctv4();
watcher.playCctv5();
watcher.playCctv6();
watcher.playXXtv();
watcher.playYYtv();
watcher.playNNtv();


watcher.playYYtv();
watcher.playXXtv();
watcher.playCctv6();
watcher.playCctv5();
watcher.playCctv4();
watcher.playCctv3();
watcher.playCctv2();
watcher.playCctv1();

这样子的原因是因为对于之前播放的哪个卫视并没有记录功能。这时候通过命令模式的实现,对比下就能体会到它的好处。

使用命令模式

设计一个抽象的命令类:

在本系统中,命令的接收者对象就是电视机Tevevision了。

// 抽象电视机命令类
public abstract class Command {
    //命令接收者:电视机
    protected Television television;

    public Command(Television television) {
        this.television = television;
    }

    //命令执行
    abstract void execute();
}

将播放各个卫视的操作封装成一个一个命令,实现如下:

//播放cctv1的命令
public class CCTV1Command extends Command {
    @Override
    void execute() {
        television.playCctv1();
    }
}

//播放cctv2的命令
public class CCTV6Command extends Command {
    @Override
    void execute() {
        television.playCctv2();
    }
}
// ......

//播放cctv6的命令
public class CCTV1Command extends Command {
    @Override
    void execute() {
        television.playCctv6();
    }
}

抽象类Command的几个子类实现也很简单,就是将电视机TeleVision对象的playXxTV()方法分布于不同的命令对象中,且因为不同的命令对象拥有共同的抽象类,我们很容易将这些名利功能放入一个数据结构(比如数组)中来存储执行过的命令。

命令对象设计好了,那么就引入命令的调用着Invoker对象了,在此例子中电视遥控器TeleController就是扮演的这个角色:

// 电视遥控器
public class TeleController {
    //播放记录
    List<Command> historyCommand = new ArrayList<Command>();

    //切换卫视
    public void switchCommand(Command command) {
        historyCommand.add(command);
        command.execute();
    }

    //遥控器返回命令
    public void back() {
        if (historyCommand.isEmpty()) {
            return;
        }
        int size = historyCommand.size();
        int preIndex = size-2 <= 0 ? 0 : size - 2;

        //获取上一个播放某卫视的命令
        Command preCommand = historyCommand.remove(preIndex);

        preCommand.execute();
    }
}

很简答,遥控器对象持有一个命令集合,用来记录已经执行过的命令。新的命令对像作为switchCommand参数来添加到集合中,注意在这里就体现出了让上文所术的请求参数化的目的。且遥控器类也提供了back方法用来模拟真实遥控器的返回功能。

现在我们来编写一个具体的测试类:

public static void main(String[] args) {       
    //创建一个电视机
    Television tv = new Television();
    //创建一个遥控器
    TeleController teleController = new TeleController();

    teleController.switchCommand(new CCTV1Command(tv));
    teleController.switchCommand(new CCTV2Command(tv));
    teleController.switchCommand(new CCTV4Command(tv));
    teleController.switchCommand(new CCTV3Command(tv));
    teleController.switchCommand(new CCTV5Command(tv));
    teleController.switchCommand(new CCTV1Command(tv));
    teleController.switchCommand(new CCTV6Command(tv));
    System.out.println("------返回上一个卫视--------");
    //模拟遥控器返回键
    teleController.back();
    teleController.back();
}

从上面的例子我们可以看出,命令模式的主要特点就是将请求封装成一个个命令,以命令为参数来进行切换,达到请求参数化的目的,且还能通过集合这个数据结构来存储已经执行的请求,进行回退操作。而且如果需要添加新的电视频道,只需要添加新的命令类即可。

而非命令模式中,看电视的人和电视耦合在一起;而新的命令模式则使用一个遥控器就将人和电视机解耦。

模式优点
  • 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
  • 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  • 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
模式缺点
  • 可能产生大量具体命令类。因为计对每一个具体操作都需要设计一个具体命令类,这将增加系统的复杂性。
使用场景
  1. 当系统需要将请求调用者与请求接收者解耦时,命令模式使得调用者和接收者不直接交互。
  2. 当系统需要随机请求命令或经常增加或删除命令时,命令模式比较方便实现这些功能。
  3. 当系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
  4. 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。
最佳示例
JdbcTemplate中的命令模式

在Spring 5.2.10中的JdbcTemplate中使用到了命令模式:

public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
    
    public void execute(final String sql) throws DataAccessException {
		if (logger.isDebugEnabled()) {
			logger.debug("Executing SQL statement [" + sql + "]");
		}

		/**
		 * Callback to execute the statement.
		 */
		class ExecuteStatementCallback implements StatementCallback<Object>, SqlProvider {
			@Override
			@Nullable
			public Object doInStatement(Statement stmt) throws SQLException {
				stmt.execute(sql);
				return null;
			}
			@Override
			public String getSql() {
				return sql;
			}
		}

		execute(new ExecuteStatementCallback());
	}
	
}

这个方法创建了一个 ExecuteStatementCallback 内部类,它实现了 StatementCallback 接口。

这个就相当于 Command 接口。

具体其他命令实现可参考它的实现类。

备忘录模式
概念

备忘录模式(Memento Pattern)又称标记模式,即在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态

模型结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gDSZJ7gs-1641917725063)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220111211148591.png)]

备忘录模式中主要有以下几个角色:

  • 原发器角色(Originator):它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。
  • 备忘录角色(Memento):存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同。
  • 负责人角色(Caretaker):负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。
示例代码

我们使用文档时,一般都提供暂存功能,可以将文档内容暂时保存到内存当中,我们来编写示例代码:

首先提供文档类。

// 文档,需要保存的对象
@Data
public class Article {
    private String title;
    private String content;
    private String image;

    public Article(String tittle, String content, String image) {
        this.title = tittle;
        this.content = content;
        this.image = image;
    }

	// 保存信息到备忘录
    public ArticleMemento saveToMemento() {
        ArticleMemento articleMemento = new ArticleMemento(title, content, image);
        return articleMemento;
    }

    // 从备忘录恢复
    public void undoFromMemento(ArticleMemento articleMemento) {
        this.title = articleMemento.getTitle();
        this.content = articleMemento.getContent();
        this.image = articleMemento.getImage();
    }
}

再来编写文档备忘录类。

/**
 * 文档的备忘录类
 *      主要用于存储文档的各种属性状态信息
 *  备忘录 快照 没有 set 方法
 *      只能通过构造函数设置备忘录数据
 */
public class ArticleMemento {
    private String title;
    private String content;
    private String image;

    public ArticleMemento(String title, String content, String image) {
        this.title = title;
        this.content = content;
        this.image = image;
    }

    public String getTitle() {
        return title;
    }

    public String getContent() {
        return content;
    }

    public String getImage() {
        return image;
    }
}

最后我们再来编写文档备忘录管理类。

// 备忘录管理类
public class ArticleMementoManager {
    
    // 存储所有的备忘录信息,在栈数据结构中存储 , 特点后进先出
    private final Stack<ArticleMemento> mArticleMementoStack = new Stack<>();

    // 获取栈顶的备忘录信息
    public ArticleMemento getArticleMemento() {
        return mArticleMementoStack.pop();
    }

    // 备忘录信息入栈,放在栈顶
    public void setArticleMemento(ArticleMemento articleMemento) {
        mArticleMementoStack.push(articleMemento);
    }
}

我们在编写一段测试代码:

public static void main(String[] args) {
    ArticleMementoManager articleMementoManager = new ArticleMementoManager();

    // 创建并输入文档内容
    Article article = new Article("标题", "内容", "图片链接");
    // 保存备忘录信息
    ArticleMemento articleMemento = article.saveToMemento();
    // 将备忘录信息设置到 备忘录管理者
    articleMementoManager.setArticleMemento(articleMemento);
    // 打印备忘录内容
    System.out.println("文档信息 : " + article.toString());

    // 修改文档内容
    article.setTitle("标题 2");
    article.setContent("内容 2");
    article.setImage("图片链接 2");
    // 保存新的备忘录信息
    articleMemento = article.saveToMemento();
    // 将备忘录信息设置到 备忘录管理者
    articleMementoManager.setArticleMemento(articleMemento);
    // 打印备忘录内容
    System.out.println("文档信息 : " + article.toString());

    // 此时 ArticleMementoManager 中存储了 2 个存档
    // 存档 1 : Article{title='标题', content='内容', image='图片链接'}
    // 存档 2 : Article{title='标题 2', content='内容 2', image='图片链接 2'}

    // 使用备忘录回退
    // 先将栈顶的当前备忘录出栈 , 移除
    articleMementoManager.getArticleMemento();
    // 然后获取上一个备忘录 , 并设置到 Article 中
    article.undoFromMemento(articleMementoManager.getArticleMemento());
    // 打印备忘录内容
    System.out.println("文档信息 : " + article.toString());
}
模式优点
  • 提供状态恢复。对象恢复到特定的历史步骤。
  • 封装信息。备忘录保存了原发器的状态,并且只有原发器可以改动。
模式缺点
  • 资源消耗大。特别是保存的原发器类的对象较多时,需要占据内存,保存操作也需要开销。
使用场景
  1. 需要实现撤销操作。
  2. 需要封装对象的历史状态,并且避免暴露给其他对象。
最佳示例
Date类

Date类通过fastTime这一变量保存历史时间。

public class Date implements java.io.Serializable, Cloneable, Comparable<Date>
{
    private transient long fastTime;
}
状态模式
概念

状态模式,即允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

在很多情况下, 一个对象的行为取决于一个或多个动态变化的属性 ,这样的属性叫做 状态 ,这样的对象叫做 有状态的 ( stateful ) 对象 ,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。

模型结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pmKn9ixy-1641917725064)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220111212703191.png)]

状态模式的结构状态模式包含以下几个主要角色:

  • 环境角色(Context):也称为上下文,它定义了客户感兴趣的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
  • 抽象状态角色(State):定义一个接口,用以封装环境对象中的特定状态所对应的行为。
  • 具体状态角色(ConcreteState):实现抽象状态所对应的行为。
示例代码

讲状态模式一般最喜欢拿电梯举例子来说明。

电梯的状态有停止、运行、开门和关门等状态。而且每个状态还都要有特定的行为,比如在开门的状态下,电梯只能关门,而不能运行;在关门状态下,电梯可以运行、开门等。用一张表来表示这个关系:

状态\动作开门关门运行停止
开门状态XOXX
关门状态OXOO
运行状态XXXO
停止状态OXOX
传统代码

这种情况下,一般做法是先根据依赖倒置原则定义出接口,电梯一共有四种状态,所以定义了四个常量表示这几种状态,实现类中电梯的每一次动作发生都要对状态进行判断,看是否可以执行动作。

// 电梯接口
interface ILift {
     //电梯的4个状态  
     //开门状态    
     public final static int OPENING_STATE = 1; 
     //关门状态  
     public final static int CLOSING_STATE = 2;  
     //运行状态  
     public final static int RUNNING_STATE = 3; 
     //停止状态  
     public final static int STOPPING_STATE = 4; 
     
     //设置电梯的状态  
     public void setState(int state);  
     
     //电梯的动作
     public void open();  
     public void close();  
     public void run();
     public void stop();
}

现在来编写电梯的实现类:

public class Lift implements ILift {
    
	private int state;

    @Override
    public void setState(int state) {
        this.state = state;
    }

    //执行关门动作
    @Override
    public void close() {
        switch (this.state) {
            case OPENING_STATE:
                System.out.println("电梯关门了。。。");//只有开门状态可以关闭电梯门,可以对应电梯状态表来看
                this.setState(CLOSING_STATE);//关门之后电梯就是关闭状态了
                break;
            case CLOSING_STATE:
                //do nothing //已经是关门状态,不能关门
                break;
            case RUNNING_STATE:
                //do nothing //运行时电梯门是关着的,不能关门
                break;
            case STOPPING_STATE:
                //do nothing //停止时电梯也是关着的,不能关门
                break;
        }
    }

    //执行开门动作
    @Override
    public void open() {
        switch (this.state) {
            case OPENING_STATE://门已经开了,不能再开门了
                //do nothing
                break;
            case CLOSING_STATE://关门状态,门打开:
                System.out.println("电梯门打开了。。。");
                this.setState(OPENING_STATE);
                break;
            case RUNNING_STATE:
                //do nothing 运行时电梯不能开门
                break;
            case STOPPING_STATE:
                System.out.println("电梯门开了。。。");//电梯停了,可以开门了
                this.setState(OPENING_STATE);
                break;
        }
    }

    //执行运行动作
    @Override
    public void run() {
        switch (this.state) {
            case OPENING_STATE://电梯不能开着门就走
                //do nothing
                break;
            case CLOSING_STATE://门关了,可以运行了
                System.out.println("电梯开始运行了。。。");
                this.setState(RUNNING_STATE);//现在是运行状态
                break;
            case RUNNING_STATE:
                //do nothing 已经是运行状态了
                break;
            case STOPPING_STATE:
                System.out.println("电梯开始运行了。。。");
                this.setState(RUNNING_STATE);
                break;
        }
    }

    //执行停止动作
    @Override
    public void stop() {
        switch (this.state) {
            case OPENING_STATE: //开门的电梯已经是是停止的了(正常情况下)
                //do nothing
                break;
            case CLOSING_STATE://关门时才可以停止
                System.out.println("电梯停止了。。。");
                this.setState(STOPPING_STATE);
                break;
            case RUNNING_STATE://运行时当然可以停止了
                System.out.println("电梯停止了。。。");
                this.setState(STOPPING_STATE);
                break;
            case STOPPING_STATE:
                //do nothing
                break;
        }
    }
}

然后我们来编写测试代码:

public static void main(String[] agres) {
    Lift lift = new Lift();
    lift.setState(ILift.STOPPING_STATE);//电梯是停止的
    lift.open();//开门
    lift.close();//关门
    lift.run();//运行
    lift.stop();//停止
}

实现逻辑非常完美,但还有些问题:

Lift中每个动作执行都要进行switch判断,造成代码过长,代码阅读困难。
扩展性非常差,如果电梯新增一个状态,接口要改,实现方法也要大量修改,与开闭原则违背。
要解决问题首先要搞清问题,以上需求主要分为两个:状态动作
状态是如何产生的,以及这个状态怎么过渡到其他状态(执行动作)。

使用状态模式

使用状态模式,首先以状态为参考模型:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r5LDWyHh-1641917725064)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220111230722623.png)]

将电梯的每个状态都封装了起来,再通过场景类Context来切换。

定义一个LiftState抽象类,声明了一个受保护的类型Context变量,这个是串联各个状态的封装类。封装的目的很明显,就是电梯对象内部状态的变化不被调用类知晓,也就是符合迪米特法则:

// 定义一个电梯的接口 
abstract class LiftState {
 
    //定义一个环境角色,也就是封装状态的变化引起的功能变化
    protected Context context;

    public void setContext(Context context) {
        this.context = context;
    }

    //电梯开门动作
    public abstract void open();

    //电梯关门动作
    public abstract void close();

    //电梯运行动作
    public abstract void run();

    //电梯停止动作
    public abstract void stop();
}

模拟一个开门的电梯接口,其他三种就类比一下:

class OpenningState extends LiftState {
    
    //开启当然可以关闭了,我就想测试一下电梯门开关功能
    @Override
    public void open() {
        System.out.println("电梯门开启...");
    }

    @Override
    public void close() {//虽然可以关门,但这个动作不归我执行
        //状态修改
        super.context.setLiftState(Context.closeingState);
        //动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
        super.context.getLiftState().close();
    }

    //电梯门不能开着就跑,这里什么也不做
    @Override
    public void run() {
        //do nothing
    }

    //开门状态已经是停止的了
    @Override
    public void stop() {
        //do nothing
    }   
}

Openning状态是由open()方法产生的,因此,在这个 方法中有一个具体的业务逻辑,在Openning状态下,电梯能过渡到 其他什么状态呢?按照现在的定义的是只能过渡到Closing状态,因此我们在Close()中定义了 状态变更,同时把Close这个动作也委托了给CloseState类下的Close方法执行,这个可能不好 理解,我们再看看Context类可能好理解一点:

public class Context {

    //定义出所有的电梯状态
    public final static OpenningState openningState = new OpenningState();//开门状态,这时候电梯只能关闭
    public final static ClosingState closeingState = new ClosingState();//关闭状态,这时候电梯可以运行、停止和开门
    public final static RunningState runningState = new RunningState();//运行状态,这时候电梯只能停止
    public final static StoppingState stoppingState = new StoppingState();//停止状态,这时候电梯可以开门、运行

    //定义一个当前电梯状态
    private Listate liftState;

    public Listate getLiftState() {
        return this.liftState;
    }

    public void setLiftState(Listate liftState) {
        //当前环境改变
        this.liftState = liftState;
        //把当前的环境通知到各个实现类中
        this.liftState.setContext(this);
    }

    public void open() {
        this.liftState.open();
    }

    public void close() {
        this.liftState.close();
    }

    public void run() {
        this.liftState.run();
    }

    public void stop() {
        this.liftState.stop();
    }
}

总的来说,状态模式由三个角色组成:抽象状态角色具体状态角色环境角色。具体状态角色只负责处理本状态必须完成的任务和决定是否可以过渡到其他状态两个职责,Context负责状态的过渡,这样将过渡与状态分离,提高了程序可扩展性和代码的可阅读性。

模式优点
  • 状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
  • 减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
  • 有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。
模式缺点
  • 状态模式的使用必然会增加系统的类与对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
使用场景
  1. 行为随状态改变而改变的的场景,例如权限设计,人员对象权限不同执行相同的行为其结果也会不同。
  2. 条件、分支判断语句替代者,通过扩展子类实现条件判断处理。
最佳示例

暂无

访问者模式
概念

访问者模式(Visitor Pattern),封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作

访问者模式的目的:将数据结构与数据操作分离,解决数据结构和操作耦合性问题。

访问者模式的基本工作原理:在被访问的类里面加一个对外提供接待访问者的接口。

模型结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i050fNkA-1641917725065)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220111233802875.png)]

访问者模式包含以下几个重要角色:

  • 抽象访问者(Visitor):抽象访问者为对象结构中的每一个 具体元素类声明一个访问操作,从这个操作的名称或参数类型可以清楚地知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。
  • 具体访问者(ConcreteVisitor):具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中的一种类型的元素。
  • 抽象元素(Element):抽象元素一般是抽象类或者接口,它声明了一个accept()方法用于接收访问者的访问操作,该方法通常以一个抽象访问者作为参数。
  • 具体元素(ConcreteElement):具体元素实现了accept()方法,在accept()方法中调用访问方法以便完成对一个元素的操作。
  • 对象结构(ObjectStructure):对象结构是一个元素的集合,它用于存放元素对象,并且提供了变量其内部元素的方法。对象结构可以结合组合模式来实现,也可以是一个简单的集合对象。
示例代码

编写男性和女性类,他们都继承于人这个抽象类,

// 人抽象类
public abstract class Person {
	public abstract void accept(Estimate estimate);
}

// 男性
public class Man extends Person {
	@Override
	public void accept(Estimate estimate) {
		estimate.getManResult(this);
	}
}

// 女性
public class Woman extends Person {
	@Override
	public void accept(Estimate estimate) {
		estimate.getWomanResult(this);
	}
}

编写评价抽象类和实现子类

// 抽象结果类
public abstract class Estimate {

	public abstract void getManResult(Man man);

	public abstract void getWomanResult(Woman woman);
}

// 成功类
public class Success extends Estimate {

	@Override
	public void getManResult(Man man) {
		System.out.println("男-Success");
	}

	@Override
	public void getWomanResult(Woman woman) {
		System.out.println("女-Success");
	}

}

// 失败类
public class Fail extends Estimate {

	@Override
	public void getManResult(Man man) {
		System.out.println("男-Fail");
	}

	@Override
	public void getWomanResult(Woman woman) {
		System.out.println("女-Fail");
	}

}

创建数据结构类:

public class ObjectStructure {

	private List<Person> persons = new LinkedList<>();
	
	public void put(Person p) {
		persons.add(p);
	}

	public void display(Estimate estimate) {
		for(Person p: persons) {
			p.accept(estimate);
		}
	}
}

最后我们来编写一段测试代码:

public static void main(String[] args) {
    ObjectStructure objectStructure = new ObjectStructure();

    objectStructure.put(new Man());
    objectStructure.put(new Woman());

    Success success = new Success();
    objectStructure.display(success);

    Fail fail = new Fail();
    objectStructure.display(fail);
}
模式优点
  • 在访问者模式中增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合开闭原则。
  • 访问者模式将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。
  • 访问者模式让用户能够在不修改现有元素类层次结构的情况下定义作用于该层次结构的操作。
模式缺点
  • 在访问者模式中增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都以为着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了开闭原则的要求。
  • 访问者模式破坏了对象的封装性。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这以为着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法提供访问者访问。
使用场景
  1. 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以对不同的访问操作。
  2. 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得用户可以将相关的访问操作几种起来定义在访问者类中,对象结构可以被多个不同的访问者所使用,将对象本身与对象的访问操作分离。
  3. 对象结构中对象对应的类很少改表,但经常需要在此对象结构上定义新的操作。
最佳示例

参考博客链接:访问者模式在JDK以及Spring源码中的应用

中介者模式
概念

中介者模式(Mediator),是指用一个中介者对象来封装一系列的对象交互。中介者使各个对象不需要显示地互相引用,从而使其耦合松散,而且可以独立的改变他们之间的交互

模型结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p3Bkoyqn-1641917725065)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220111235236088.png)]

中介者模式包含以下几个主要角色:

  • 抽象中介者角色(Mediator):它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  • 具体中介者角色(ConcreteMediator):实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  • 抽象同事类角色(Colleague):定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  • 具体同事类角色(Concrete Colleague):是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
示例代码
// 抽象同事类
abstract class Colleague {
    
    protected Mediator mediator;

    // 构造方法得到中介者对象
    public Colleague(Mediator mediator) {
        this.mediator = mediator;
    }

    // 发送消息的抽象方法
    abstract void send(String message);

    // 接收消息之后行为的抽象方法
    abstract void notify(String message);
}


// 同事对象1
public class ConcreteColleague1 extends Colleague {
    
    // 构造方法得到中介者对象
    public ConcreteColleague1(Mediator mediator) {
        super(mediator);
    }

    @Override
    public void send(String message) {
        mediator.send(message, this);
    }

    @Override
    public void notify(String message) {
        System.out.println("同事1得到消息:" + message);
    }
}

// 同事对象2
public class ConcreteColleague2 extends Colleague {

    // 构造方法得到中介者对象

    public ConcreteColleague2(Mediator mediator) {
        super(mediator);
    }

    @Override
    public void send(String message) {
        mediator.send(message, this);
    }

    @Override
    public void notify(String message) {
        System.out.println("同事2得到消息:" + message);
    }
}

编写中介者类:

// 抽象中介者接口
interface Mediator {
    
    // 定义一个发送消息的接口,得到同事对象和发送信息
    void send(String message,Colleague colleague);
    
}


// 具体中介者类
public class ConcreteMediator implements Mediator {
    
    private ConcreteColleague1 concreteColleague1;
    private ConcreteColleague2 concreteColleague2;
    
    public void setConcreteColleague1(ConcreteColleague1 concreteColleague1) {
        this.concreteColleague1 = concreteColleague1;
    }

    public void setConcreteColleague2(ConcreteColleague2 concreteColleague2) {
        this.concreteColleague2 = concreteColleague2;
    }

    @Override
    public void send(String message, Colleague colleague) {
        if (colleague == concreteColleague1) {
            concreteColleague2.notify(message);
        } else if (colleague == concreteColleague2) {
            concreteColleague1.notify(message);
        } else {
            
        }
    }
}

编写测试代码:

public static void main(String[] args) {
    ConcreteMediator concreteMediator = new ConcreteMediator();
    //让两个具体同事类人数中介者对象
    ConcreteColleague1 concreteColleague1 = new ConcreteColleague1(concreteMediator);
    ConcreteColleague2 concreteColleague2 = new ConcreteColleague2(concreteMediator);

    //让他们认识各个具体同事类对象
    concreteMediator.setConcreteColleague1(concreteColleague1);
    concreteMediator.setConcreteColleague2(concreteColleague2);

    //具体同事类对象的发送消息都是通过中介者转发
    concreteColleague1.send("你在干什么?");
    concreteColleague2.send("我不想告诉你!");
}

// 控制台输出
// 同事2得到消息:你在干什么?
// 同事1得到消息:我不想告诉你!
模型优点
  • 降低了对象之间的耦合性,使得对象易于独立地被复用。
  • 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。
模型缺点
  • 当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
使用场景
  1. 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
  2. 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。
最佳示例

我们打开JDK源码中的Timer类。我们任意查看其中的几个schedule方法:

public class Timer {
    
 	private void sched(TimerTask task, long time, long period) {
        if (time < 0)
            throw new IllegalArgumentException("Illegal execution time.");

        if (Math.abs(period) > (Long.MAX_VALUE >> 1))
            period >>= 1;

        synchronized(queue) {
            if (!thread.newTasksMayBeScheduled)
                throw new IllegalStateException("Timer already cancelled.");

            synchronized(task.lock) {
                if (task.state != TimerTask.VIRGIN)
                    throw new IllegalStateException(
                        "Task already scheduled or cancelled");
                task.nextExecutionTime = time;
                task.period = period;
                task.state = TimerTask.SCHEDULED;
            }

            queue.add(task);
            if (queue.getMin() == task)
                queue.notify();
        }
    }
    
}

可以看到,所有的task都放入了Timer类中维护的task队列中。所以Timer就是充当了一个中介者的角色,而task队列内的任务就是具体同事对象。

解释器模式
概念

解释器模式,即给定一种语言,定义他的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中句子

模型结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GLUkmNpR-1641917725065)(C:\Users\jiangcx\AppData\Roaming\Typora\typora-user-images\image-20220112000512261.png)]

解释器模式包含以下几个主要角色:

  • 抽象表达式(AbstractExpression):声明了抽象的解释操作interpret(),是所有终结符表达式和非终结符表达式的基类。
  • 终结符表达式(TerminalExpression):终结符是文法规则的组成元素中最基本的语言单位,不能再分解。终结符表达式实现了与文法规则中终结符相关的解释操作,句子中的每一个终结符都是该类的一个实例。
  • 非终结符表达式(NonterminalExpression):实现了文法规则中非终结符的解释操作,因为非终结符表达式同样可以包含终结符表达式,所以终结符表达式可以是非终结符表达式的成员。
  • 环境类(Context):即上下文类,用于存储解释器之外的一些全局信息,通常临时存储需要解释的语句。
示例代码

实现输入字符串10 2 3 + *,将字符串使用空格切割成数组,遇到数字直接入栈;遇到运算符 , 从栈中取出两个数据 , 进行计算 , 将计算结果,再放入栈中。

遍历完毕后 , 最终的栈内数据就是最终结果。

首先编写解释器接口。

// 解释器接口
public interface Interpreter {

	// 解释方法
    int interpret();
}

这里不做过多实现,先实现加法解释器和乘法解释器。

/**
 * 加法解释器
 *      实现 Interpreter 解释器 接口
 * 用于实现加法计算
 *      加法有加数 和 被加数 , 两个加数分别也都是一个解释器
 */
public class AddInterpreter implements Interpreter {
    
    // 第一个表达式
    private Interpreter firstInterpreter;

    // 第二个表达式
    private Interpreter secondInterpreter;

    public AddInterpreter(Interpreter firstInterpreter,
                          Interpreter secondInterpreter) {
        this.firstInterpreter = firstInterpreter;
        this.secondInterpreter = secondInterpreter;
    }

    @Override
    public int interpret() {
        return this.firstInterpreter.interpret() +
                this.secondInterpreter.interpret();
    }

    @Override
    public String toString() {
        return "+";
    }
}

/**
 * 乘法解释器
 *      实现 Interpreter 解释器 接口
 * 用于实现乘法计算
 *      两个乘数分别也都是一个解释器
 */
public class MultiInterpreter implements Interpreter {
    // 第一个表达式
    private Interpreter firstInterpreter;


    // 第二个表达式

    private Interpreter secondInterpreter;

    public MultiInterpreter(Interpreter firstInterpreter,
                            Interpreter secondInterpreter) {
        this.firstInterpreter = firstInterpreter;
        this.secondInterpreter = secondInterpreter;
    }

    @Override
    public int interpret() {
        return this.firstInterpreter.interpret() *
                this.secondInterpreter.interpret();
    }

    @Override
    public String toString() {
        return "*";
    }
}

再实现整型解释器。

/**
 * 数字解释器
 *      实现 Interpreter 解释器 接口
 */
public class NumberInterpreter implements Interpreter {
    /**
     * 核心数字
     *      需要将传入的数据转为数字
     */
    private int number;

    // 直接设置数字类型

    public NumberInterpreter(int number) {
        this.number = number;
    }

    // 将字符串转为数字类型
    public NumberInterpreter(String number) {
        this.number = Integer.parseInt(number);
    }

    @Override
    public int interpret() {
        return this.number;
    }

    @Override
    public String toString() {
        return "" + this.number;
    }
}

编写语法解析类。

/**
 * 语法解析
 */
public class ExpressionParser {
    /**
     * 存放解释器的栈
     *      栈的特点是先进后出
     */
    private Stack<Interpreter> stack = new Stack<>();

    // 解析字符串语法
    public void parse(String str) {
        // 通过空格分割字符串
        String[] strArray = str.split(" ");

        // 遍历栈中的字符串
        for (String symbol : strArray) {
            if (!OperatorUtils.isOperator(symbol)) {
                // 如果不是操作符 , 说明是数字 , 则直接入栈
                Interpreter interpreter = new NumberInterpreter(symbol);
                stack.push(interpreter);
                System.out.println(symbol + " 入栈");
            } else {
                // 如果是操作符 , 则数据出栈 , 处理是操作符运算的情况

                // 取出两个需要计算的元素
                Interpreter firstInterpreter = stack.pop();
                System.out.println(firstInterpreter + " 出栈");
                Interpreter secondInterpreter = stack.pop();
                System.out.println(secondInterpreter + " 出栈");

                // 获取 运算符号 对应的解释器
                Interpreter operator = OperatorUtils.
                        getExpressionInterpretor(
                                symbol,
                                firstInterpreter,
                                secondInterpreter);
                System.out.println("运算符 " + operator + " 出栈");

                // 计算 运算符 运算结果
                int result = operator.interpret();

                // 将计算结果你年入栈
                NumberInterpreter numberInterpreter = new NumberInterpreter(result);
                stack.push(numberInterpreter);
                System.out.println("计算结果 " + result + " 入栈");
            }
        }

        // 取出最终计算结果 , 计算完毕后 , 整个栈必然只剩下一个元素
        int result = stack.pop().interpret();
        System.out.println("最终计算结果 : " + result);
    }
}

再编写一个工具类,用于判断传入符号的校验,以及根据符号获取指定解释器。

public class OperatorUtils {
    /**
     * 判断传入的符号字符串是否是操作符
     * @param symbol
     * @return
     */
    public static boolean isOperator(String symbol) {
        return symbol.equals("+") || symbol.equals("*");
    }

    public static Interpreter getExpressionInterpretor(
            String symbol,
            Interpreter firstInterpreter,
            Interpreter secondInterpreter) {
        Interpreter interpreter = null;
        if (symbol.equals("+")) {
            interpreter = new AddInterpreter(firstInterpreter, secondInterpreter);
        } else if (symbol.equals("*")) {
            interpreter = new MultiInterpreter(firstInterpreter, secondInterpreter);
        }
        return interpreter;
    }
}

我们最好再来编写一下测试代码:

public static void main(String[] args) {
    // 将字符串使用空格切割成数组
    // 遇到数字直接入栈
    // 遇到运算符 , 从栈中取出两个数据 , 进行计算 , 将计算结果再放入栈中
    // 遍历完毕后 , 最终的栈内数据就是最终结果
    String text = "10 2 3 + *";

    ExpressionParser parser = new ExpressionParser();
    parser.parse(text);
}
模型优点
  • 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
  • 容易实现。在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。
模型缺点
  • 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
  • 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
  • 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。
使用场景
  1. 当语言的文法较为简单,且执行效率不是关键问题时。
  2. 当问题重复出现,且可以用一种简单的语言来进行表达时。
  3. 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候,如 XML 文档解释。
最佳示例

博客参考链接: 解释器模式在Spring源码中的应用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

舍其小伙伴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值