天天看点

设计模式之观察者、中介者、迭代器、访问者、备忘录、解释器模式

作者:添甄

前言

这是设计模式的最后一章,包含了剩余的 行为型模式 中的 观察者模式、中介者模式、迭代器模式、访问者模式、备忘录模式、解释器模式

一、观察者模式

在现实世界中,许多对象并不是独立存在的,其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。例如,某种商品的物价上涨时会导致部分商家高兴,而消费者伤心;还有,当我们开车到交叉路口时,遇到红灯会停,遇到绿灯会行。这样的例子还有很多,例如,股票价格与股民、微信公众号与微信用户、气象局的天气预报与听众、小偷与警察等。

在软件世界也是这样,例如,Excel 中的数据与折线图、饼状图、柱状图之间的关系;MVC 模式中的模型与视图的关系;事件模型中的事件源与事件处理者。所有这些,如果用观察者模式来实现就非常方便。

观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

1.1、实现方式

1.1.1、观察者模式角色

  • 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
  • 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
  • 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
  • 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。

1.1.2、代码实现

案例:订阅文章的案例,当有文章更新时推送给所有的订阅者,其中订阅者就是观察者文章是否有新的推送

// 抽象主题类
public interface Subject {

    // 添加订阅者
    void attach(Observver observver);

    // 删除订阅者
    void detach(Observver observver);

    // 通知订阅者
    void notify(String msg);
}

// 抽象订阅者
public interface Observver {

    void update(String msg);
}

// 具体主题类
public class SubscriptionSubject implements Subject{

    private List<Observver> weixinUserList = new ArrayList<>();

    @Override
    public void attach(Observver observver) {
        weixinUserList.add(observver);
    }

    @Override
    public void detach(Observver observver) {
        weixinUserList.remove(observver);
    }

    @Override
    public void notify(String msg) {
        // 遍历通知每一个订阅者
        for (Observver observver : weixinUserList) {
            observver.update(msg);
        }
    }
}

// 具体订阅类
public class WeiXinUser implements Observver{

    private String name;

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

    @Override
    public void update(String msg) {
        System.out.println(name + "-" + msg);
    }
}

public class Client {
    public static void main(String[] args) {
        SubscriptionSubject subject = new SubscriptionSubject();

        subject.attach(new WeiXinUser("孙悟空"));
        subject.attach(new WeiXinUser("猪悟能"));
        subject.attach(new WeiXinUser("沙悟净"));

        // 发布消息
        subject.notify("您订阅的文章更新了");
    }
}           

1.2、观察者模式优缺点

优点

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
  • 目标与观察者之间建立了一套触发机制。

缺点

  • 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
  • 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

1.3、应用场景

  1. 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  2. 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
  3. 实现类似广播机制的功能,不需要知道具体收听者,只需分发广播,系统中感兴趣的对象会自动接收该广播。
  4. 多层级嵌套使用,形成一种链式触发机制,使得事件具备跨域(跨越两种观察者类型)通知。

二、中介者模式

在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵一发而动全身”,非常复杂。

如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。如前面所说的“每个人必须记住所有朋友电话”的问题,只要在网上建立一个每个朋友都可以访问的“通信录”就解决了。这样的例子还有很多,例如,你刚刚参加工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。

在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。

中介者(Mediator)模式的定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。

2.1、实现方式

2.1.1、中介者模式角色

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

2.1.2、代码实现

案例:租房例子,租客到中介那里找房子,中介有业主托管的房子

// 抽象中介者
public abstract class Mediator {

    public abstract void constact(String msg,Person person);
}

// 抽象同事类
public abstract class Person {

    protected String name;
    protected Mediator mediator;

    public Person(String name, Mediator mediator) {
        this.name = name;
        this.mediator = mediator;
    }
}

// 具体的同事角色类(租房客)
public class Tanant extends Person{

    public Tanant(String name, Mediator mediator) {
        super(name, mediator);
    }
    // 沟通类
    public void constact(String msg) {
        mediator.constact(msg,this);
    }

    // 获取信息
    public void getMsg(String msg) {
        System.out.println("租房者" + name + "获取到的信息是:" + msg);
    }
}

// 具体同事类 房东类
public class HouseOwner extends Person{

    public HouseOwner(String name, Mediator mediator) {
        super(name, mediator);
    }

    // 沟通类
    public void constact(String msg) {
        mediator.constact(msg,this);
    }

    // 获取信息
    public void getMsg(String msg) {
        System.out.println("房东" + name + "获取到的信息是:" + msg);
    }
}

// 具体中介者
public class MediatorStructure extends Mediator{

    // 房东
    private HouseOwner houseOwner;
    // 租房客
    private Tanant tanant;

    public HouseOwner getHouseOwner() {
        return houseOwner;
    }

    public void setHouseOwner(HouseOwner houseOwner) {
        this.houseOwner = houseOwner;
    }

    public Tanant getTanant() {
        return tanant;
    }

    public void setTanant(Tanant tanant) {
        this.tanant = tanant;
    }

    @Override
    public void constact(String msg, Person person) {
        // 判断如果是房东
        if(person == houseOwner) {
            houseOwner.getMsg(msg);
        }else {
            tanant.getMsg(msg);
        }
    }
}

// 测试类
public class Client {
    public static void main(String[] args) {
        // 创建中介者
        MediatorStructure structure = new MediatorStructure();

        Tanant tanant = new Tanant("小刚",structure);
        HouseOwner houseOwner = new HouseOwner("王刚", structure);

        structure.setHouseOwner(houseOwner);
        structure.setTanant(tanant);

        tanant.constact("我要租房");
        houseOwner.constact("我这有房");
    }
}           

2.2、中介者模式优缺点

优缺点

优点

  • 类之间各司其职,符合迪米特法则。
  • 降低了对象之间的耦合性,使得对象易于独立地被复用。
  • 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

缺点

  • 中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。

2.3、应用场景

  • 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

三、迭代器模式

在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如 数据结构 中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。

既然将遍历方法封装在聚合类中不可取,那么聚合类中不提供遍历方法,将遍历方法由用户自己实现是否可行呢?答案是同样不可取,因为这种方式会存在两个缺点:

  1. 暴露了聚合类的内部表示,使其数据不安全;
  2. 增加了客户的负担。

“迭代器模式”能较好地克服以上缺点,它在客户访问类与聚合类之间插入一个迭代器,这分离了聚合对象与其遍历行为,对客户也隐藏了其内部细节,且满足“单一职责原则”和“开闭原则”,如 Java 中的 Collection、List、Set、Map 等都包含了迭代器。

迭代器模式在生活中应用的比较广泛,比如:物流系统中的传送带,不管传送的是什么物品,都会被打包成一个个箱子,并且有一个统一的二维码。这样我们不需要关心箱子里是什么,在分发时只需要一个个检查发送的目的地即可。再比如,我们平时乘坐交通工具,都是统一刷卡或者刷脸进站,而不需要关心是男性还是女性、是残疾人还是正常人等信息。

迭代器(Iterator)模式的定义:提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。

3.1、实现方式

3.1.1、迭代器模式角色

  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

3.1.2、代码实现

案例:自己实现一个迭代器,来遍历结合

public class Student {
    private String name;
    private String num;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNum() {
        return num;
    }

    public void setNum(String num) {
        this.num = num;
    }

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

// 抽象迭代器
public interface StudentIterator{

    // 判断是否还有元素
    boolean hasNext();

    // 获取下一个元素
    Student next();
}

// 具体迭代器
public class StudentIteratorImpl implements StudentIterator{

    private List<Student> list;
    private int position = 0;

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

    @Override
    public boolean hasNext() {
        return position < list.size();
    }

    @Override
    public Student next() {
        // 从集合中获取自指定位置数据
        Student student = list.get(position);
        position++;
        return student;
    }
}

// 抽象聚合角色接口
public interface StudentAggregate {
    // 添加学生
    void addStudent(Student student);

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

    // 获取迭代对象功能
    StudentIterator getStudentIterator();
}

public class StudentAggregateImpl implements StudentAggregate{

    private List<Student> list = new ArrayList<>();

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

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

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

public class Client {
    public static void main(String[] args) {
        // 创建聚合对象
        StudentAggregateImpl aggregate = new StudentAggregateImpl();

        // 添加元素
        aggregate.addStudent(new Student("张三","001"));
        aggregate.addStudent(new Student("李四","002"));
        aggregate.addStudent(new Student("王五","003"));
        aggregate.addStudent(new Student("赵六","004"));
        aggregate.addStudent(new Student("武七","005"));

        // 调用迭代器对象
        StudentIterator iterator = aggregate.getStudentIterator();
        while (iterator.hasNext()) {
            Student next = iterator.next();
            System.out.println(next);
        }
    }
}           

3.2、迭代器模式优缺点

优点

  • 访问一个聚合对象的内容而无须暴露它的内部表示。
  • 遍历任务交由迭代器完成,这简化了聚合类。
  • 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  • 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  • 封装性良好,为遍历不同的聚合结构提供一个统一的接口。

缺点

  • 增加了类的个数,这在一定程度上增加了系统的复杂性。

3.3、应用场景

  • 当需要为聚合对象提供多种遍历方式时。
  • 当需要为遍历不同的聚合结构提供一个统一的接口时。
  • 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。

四、访问者模式

在现实生活中,有些集合对象存在多种不同的元素,且每种元素也存在多种不同的访问者和处理方式。例如,公园中存在多个景点,也存在多个游客,不同的游客对同一个景点的评价可能不同;医院医生开的处方单中包含多种药元素,査看它的划价员和药房工作人员对它的处理方式也不同,划价员根据处方单上面的药品名和数量进行划价,药房工作人员根据处方单的内容进行抓药。

这样的例子还有很多,例如,电影或电视剧中的人物角色,不同的观众对他们的评价也不同;还有顾客在商场购物时放在“购物车”中的商品,顾客主要关心所选商品的性价比,而收银员关心的是商品的价格和数量。

这些被处理的数据元素相对稳定而访问方式多种多样的 数据结构 ,如果用“访问者模式”来处理比较方便。访问者模式能把处理方法从数据结构中分离出来,并可以根据需要增加新的处理方法,且不用修改原来的程序代码与数据结构,这提高了程序的扩展性和灵活性。

访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。

4.1、实现方式

4.1.1、访问者模式角色

  • 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
  • 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  • 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
  • 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。

4.1.2、代码实现

案例:通过主人给宠物喂食

public interface Person {

    // 喂猫
    void feed(Cat cat);
    // 喂狗
    void feed(Dog dog);

}

// 抽象元素角色
public interface Animal {

    // 接受访问者访问
    void accept(Person person);
}

public class Cat implements Animal{

    @Override
    public void accept(Person person) {
        person.feed(this);
        System.out.println("好好吃,喵喵喵!");
    }
}

public class Dog implements Animal{
    @Override
    public void accept(Person person) {
        // 喂食
        person.feed(this);
        System.out.println("好好吃,汪汪汪!");
    }
}

public class Owner implements Person{
    @Override
    public void feed(Cat cat) {
        System.out.println("主任喂猫");
    }

    @Override
    public void feed(Dog dog) {
        System.out.println("主任喂狗");
    }
}

public class Someone implements Person{
    @Override
    public void feed(Cat cat) {
        System.out.println("别人喂猫");
    }

    @Override
    public void feed(Dog dog) {
        System.out.println("别人喂狗");
    }
}

public class Home {

    private List<Animal> nodeList = new ArrayList<>();

    // 添加元素功能
    public void add(Animal animal) {
        nodeList.add(animal);
    }

    public void action(Person person) {
        for (Animal animal : nodeList) {
            animal.accept(person);
        }
    }
}

public class Client {
    public static void main(String[] args) {
        // 创建Home对象
        Home home = new Home();

        // 创建被访问者
        Dog dog = new Dog();
        Cat cat = new Cat();

        home.add(dog);
        home.add(cat);

        // 创建主任喂食
        Owner owner = new Owner();
        home.action(owner);
    }
}           

4.2、访问者模式优缺点

优点

  • 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  • 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。灵活性好
  • 访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  • 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

缺点

  • 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  • 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  • 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

4.3、应用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序。
  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
  • 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。

五、备忘录模式

每个人都有犯错误的时候,都希望有种“后悔药”能弥补自己的过失,让自己重新开始,但现实是残酷的。在计算机应用中,客户同样会常常犯错误,能否提供“后悔药”给他们呢?当然是可以的,而且是有必要的。这个功能由“备忘录模式”来实现。

其实很多应用软件都提供了这项功能,如 Word、记事本、Photoshop、Eclipse 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 IE 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。

备忘录(Memento)模式的定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。

5.1、实现方式

5.1.1、备忘录模式角色

  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

5.1.2、白箱备忘录

public class GameRole {

    // 生命力
    private int vit;
    // 攻击力
    private int atk;
    // 防御力
    private int def;

    // 初始化内部状态
    public void initState() {
        this.vit = 100;
        this.atk = 100;
        this.def = 100;
    }

    // 战斗
    public void fight() {
        this.vit = 0;
        this.atk = 0;
        this.def = 0;
    }

    // 保存角色状态
    public RoleStateMemento saveState() {
        return new RoleStateMemento(vit,atk,def);
    }

    // 恢复角色信息
    public void recoverState(RoleStateMemento roleStateMemento) {
        // 将备忘录中的数据赋值给当前对象
        this.vit = roleStateMemento.getVit();
        this.atk = roleStateMemento.getAtk();
        this.def = roleStateMemento.getDef();
    }

    // 展示状态功能
    public void stateDisplay() {
        System.out.println("角色生命力:" + vit);
        System.out.println("角色攻击力:" + atk);
        System.out.println("角色防御力:" + def);
    }

    public int getVit() {
        return vit;
    }

    public void setVit(int vit) {
        this.vit = vit;
    }

    public int getAtk() {
        return atk;
    }

    public void setAtk(int atk) {
        this.atk = atk;
    }

    public int getDef() {
        return def;
    }

    public void setDef(int def) {
        this.def = def;
    }
}

// 备忘录角色类
public class RoleStateMemento {

    // 生命力
    private int vit;
    // 攻击力
    private int atk;
    // 防御力
    private int def;

    public RoleStateMemento() {
    }

    public RoleStateMemento(int vit, int atk, int def) {
        this.vit = vit;
        this.atk = atk;
        this.def = def;
    }



    public int getVit() {
        return vit;
    }

    public void setVit(int vit) {
        this.vit = vit;
    }

    public int getAtk() {
        return atk;
    }

    public void setAtk(int atk) {
        this.atk = atk;
    }

    public int getDef() {
        return def;
    }

    public void setDef(int def) {
        this.def = def;
    }
}

// 备忘录管理对象
public class RoleStateCaretaker {

    private RoleStateMemento roleStateMemento;

    public RoleStateMemento getRoleStateMemento() {
        return roleStateMemento;
    }

    public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
        this.roleStateMemento = roleStateMemento;
    }
}

public class Client {
    public static void main(String[] args) {
        System.out.println("-------------------大战BOSS之前-------------------");
        // 创建
        GameRole gameRole = new GameRole();
        gameRole.initState();
        gameRole.stateDisplay();
        // 将游戏角色进行备份
        RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
        roleStateCaretaker.setRoleStateMemento(gameRole.saveState());

        System.out.println("-------------------大战BOSS之后-------------------");
        // 损耗严重
        gameRole.fight();
        gameRole.stateDisplay();

        System.out.println("-------------------恢复之前的状态-------------------");
        gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
        gameRole.stateDisplay();
    }
}           

5.1.3、黑箱备忘录

将备忘录类当做成员内部类定义在角色类中

public class GameRole {

    // 生命力
    private int vit;
    // 攻击力
    private int atk;
    // 防御力
    private int def;

    // 初始化内部状态
    public void initState() {
        this.vit = 100;
        this.atk = 100;
        this.def = 100;
    }

    // 战斗
    public void fight() {
        this.vit = 0;
        this.atk = 0;
        this.def = 0;
    }

    // 保存角色状态
    public Memento saveState() {
        return new RoleStateMemento(vit,atk,def);
    }

    // 恢复角色信息
    public void recoverState(Memento memento) {
        RoleStateMemento roleStateMemento = (RoleStateMemento)memento;
        // 将备忘录中的数据赋值给当前对象
        this.vit = roleStateMemento.getVit();
        this.atk = roleStateMemento.getAtk();
        this.def = roleStateMemento.getDef();
    }

    // 展示状态功能
    public void stateDisplay() {
        System.out.println("角色生命力:" + vit);
        System.out.println("角色攻击力:" + atk);
        System.out.println("角色防御力:" + def);
    }

    public int getVit() {
        return vit;
    }

    public void setVit(int vit) {
        this.vit = vit;
    }

    public int getAtk() {
        return atk;
    }

    public void setAtk(int atk) {
        this.atk = atk;
    }

    public int getDef() {
        return def;
    }

    public void setDef(int def) {
        this.def = def;
    }
	
    // 私有成员内部类
    private class RoleStateMemento implements Memento {
        // 生命力
        private int vit;
        // 攻击力
        private int atk;
        // 防御力
        private int def;

        public RoleStateMemento(int vit, int atk, int def) {
            this.vit = vit;
            this.atk = atk;
            this.def = def;
        }

        public RoleStateMemento() {
        }

        public int getVit() {
            return vit;
        }

        public void setVit(int vit) {
            this.vit = vit;
        }

        public int getAtk() {
            return atk;
        }

        public void setAtk(int atk) {
            this.atk = atk;
        }

        public int getDef() {
            return def;
        }

        public void setDef(int def) {
            this.def = def;
        }
    }
}

// 备忘录接口
public interface Memento {
}


// 备忘录管理对象
public class RoleStateCaretaker {

    private Memento memento;

    public Memento getMemento() {
        return memento;
    }

    public void setMemento(Memento memento) {
        this.memento = memento;
    }
}

public class Client {
    public static void main(String[] args) {
        System.out.println("-------------------大战BOSS之前-------------------");
        // 创建
        GameRole gameRole = new GameRole();
        gameRole.initState();
        gameRole.stateDisplay();
        // 将游戏角色进行备份
        RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
        roleStateCaretaker.setMemento(gameRole.saveState());

        System.out.println("-------------------大战BOSS之后-------------------");
        // 损耗严重
        gameRole.fight();
        gameRole.stateDisplay();

        System.out.println("-------------------恢复之前的状态-------------------");
        gameRole.recoverState(roleStateCaretaker.getMemento());
        gameRole.stateDisplay();
    }
}           

5.2、备忘录模式优缺点

优点

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

缺点

  • 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

5.3、应用场景

  • 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
  • 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,Eclipse 等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。

六、解释器模式

在软件开发中,会遇到有些问题多次重复出现,而且有一定的相似性和规律性。如果将它们归纳成一种简单的语言,那么这些问题实例将是该语言的一些句子,这样就可以用“编译原理”中的解释器模式来实现了。

虽然使用解释器模式的实例不是很多,但对于满足以上特点,且对运行效率要求不是很高的应用实例,如果用解释器模式来实现,其效果是非常好的,本文将介绍其工作原理与使用方法。

解释器(Interpreter)模式的定义:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。

这里提到的文法和句子的概念同编译原理中的描述相同,“文法”指语言的语法规则,而“句子”是语言集中的元素。例如,汉语中的句子有很多,“我是中国人”是其中的一个句子,可以用一棵语法树来直观地描述语言中的句子。

6.1、实现方式

6.1.1、解释器模式角色

  • 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
  • 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
  • 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
  • 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
  • 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。

6.1.2、代码实现

案例:通过解释器模式实现一个计算器

// 抽象类表达式,通过HashMap键值对,可以获取到变量的值
public abstract class Expression {

    /**
     * 表达式:a+b-c
     * 解释公式和数值,key 就是公式(表达式),参数[a,b,c]
     * value:就是具体的值
     * HashMap:{a:10,b:20,c:30}
     * @param var
     * @return
     */
    public abstract int interpreter(HashMap<String,Integer> var);
}

/**
 * 抽象运算符号解释器,每个运算符号都只和自己左右两个数字有关系
 * 但左右两个数字有可能也是一个解析的结果,无论哪种类型,都是Expression类的实现类
 * @Date 2022/1/25 20:27
 * @Author 不夜
 */
public class SymbolExpression extends Expression{

    protected Expression left;
    protected Expression right;

    public SymbolExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    // 因为 SymbolExpression 是让其子类来实现,因此 interpreter 是一个默认实现
    @Override
    public int interpreter(HashMap<String, Integer> var) {
        return 0;
    }
}

// 变量解析器
public class VarExpression extends Expression{

    // key:a,b,c
    private String key;

    public VarExpression(String key) {
        this.key = key;
    }

    /**
     * var:就是HashMap
     * 根据变量名称,返回对应的值
     * @param var
     * @return
     */
    @Override
    public int interpreter(HashMap<String, Integer> var) {
        return var.get(this.key);
    }
}

public class SubExpression extends SymbolExpression{

    public SubExpression(Expression left, Expression right) {
        super(left, right);
    }

    @Override
    public int interpreter(HashMap<String, Integer> var) {
        return super.left.interpreter(var) - super.right.interpreter(var);
    }
}

// 加法解释器
public class AddExpression extends SymbolExpression{

    public AddExpression(Expression left, Expression right) {
        super(left, right);
    }

    /**
     * 处理相加
     * @param var
     * @return
     */
    @Override
    public int interpreter(HashMap<String, Integer> var) {

        return super.left.interpreter(var) + super.right.interpreter(var);
    }
}

public class Calculator {

    // 定义表达式
    private Expression expression;

    /**
     *
     * @param expStr: a+b
     */
    public Calculator(String expStr) {
        // 安排运算先后顺序
        Stack<Expression> stack = new Stack<>();
        // 表达式拆分成字符数组 [a,+,b]
        char[] charArray = expStr.toCharArray();

        Expression left = null;
        Expression right = null;
        // 遍历我们的字符数组,即遍历[a,+,b]
        // 针对不同情况,做处理
        for (int i = 0; i < charArray.length; i++) {
            switch (charArray[i]) {
                // 如果是 + 号,就从栈中取出left  a
                // 再将,right取出  b
                case '+':
                    left = stack.pop();
                    right = new VarExpression(String.valueOf(charArray[++i]));
                    stack.push(new AddExpression(left,right));
                    break;
                case '-':
                    left = stack.pop();
                    right = new VarExpression(String.valueOf(charArray[++i]));
                    stack.push(new SubExpression(left,right));
                    break;
                default:
                    // 如果是一个Var 就创建一个 VarExpression 对象,并push到stack中
                    stack.push(new VarExpression(String.valueOf(charArray[i])));
                    break;
            }
        }
        // 遍历完整个 charArray后,stack 就得到最后Expression
        this.expression = stack.pop();
    }

    public int run(HashMap<String,Integer> var) {
        // 最后将不表达式 a+b和var 合并
        // 然后传递给expression的interpreter解释执行
        return this.expression.interpreter(var);
    }
}

public class Client {
    public static void main(String[] args) throws IOException {
        String expStr = getExpStr();
        HashMap<String,Integer> var = getValue(expStr);
        Calculator calculator = new Calculator(expStr);
        System.out.println("运算结果:" + expStr + "=" + calculator.run(var));
    }

    private static HashMap<String, Integer> getValue(String expStr) throws IOException {

        HashMap<String, Integer> map = new HashMap<>();
        for (char ch : expStr.toCharArray()) {
            if(ch != '+' && ch != '-') {
                if(!map.containsKey(String.valueOf(ch))) {
                    System.out.println("请输入" + String.valueOf(ch) + "的值:");
                    String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
                    map.put(String.valueOf(ch),Integer.valueOf(in));
                }
            }
        }
        return map;

    }

    private static String getExpStr() throws IOException {
        System.out.println("请输入表达式");
        return (new BufferedReader(new InputStreamReader(System.in))).readLine();
    }

}           

6.2、解释器模式优缺点

优缺点

优点

  • 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
  • 容易实现。在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。

缺点

  • 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
  • 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
  • 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。

6.3、应用场景

  • 当语言的文法较为简单,且执行效率不是关键问题时。
  • 当问题重复出现,且可以用一种简单的语言来进行表达时。
  • 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候,如 XML 文档解释。

七、设计模式总结

分类 设计模式 简述 一句话归纳 目的 生活案例
创建型设计模式 (简单来说就是用来创建对象的) 工厂模式(Factory Pattern) 不同条件下创建不同实例 产品标准化,生产更高效 封装创建细节 实体工厂
单例模式(Singleton Pattern) 保证一个类仅有一个实例,并且提供一个全局访问点 世上只有一个我 保证独一无二 CEO
原型模式(Prototype Pattern) 通过拷贝原型创建新的对象 拔一根猴毛,吹出千万个 高效创建对象 克隆
建造者模式(Builder Pattern) 用来创建复杂的复合对象 高配中配和低配,想选哪配就哪配 开放个性配置步骤 选配
结构型设计模式 (关注类和对象的组合) 代理模式(Proxy Pattern) 为其他对象提供一种代理以控制对这个对象的访问 没有资源没时间,得找别人来帮忙 增强职责 媒婆
外观模式(Facade Pattern) 对外提供一个统一的接口用来访问子系统 打开一扇门,通向全世界 统一访问入口 前台
装饰器模式(Decorator Pattern) 为对象添加新功能 他大舅他二舅都是他舅 灵活扩展、同宗同源 煎饼
享元模式(Flyweight Pattern) 使用对象池来减少重复对象的创建 优化资源配置,减少重复浪费 共享资源池 全国社保联网
组合模式(Composite Pattern) 将整体与局部(树形结构)进行递归组合,让客户端能够以一种的方式对其进行处理 人在一起叫团伙,心在一起叫团队 统一整体和个体 组织架构树
适配器模式(Adapter Pattern) 将原来不兼容的两个类融合在一起 万能充电器 兼容转换 电源适配
桥接模式(Bridge Pattern) 将两个能够独立变化的部分分离开来 约定优于配置 不允许用继承
行为型设计模式 (关注对象之间的通信) 模板模式(Template Pattern) 定义一套流程模板,根据需要实现模板中的操作 流程全部标准化,需要微调请覆盖 逻辑复用 把大象装进冰箱
策略模式(Strategy Pattern) 封装不同的算法,算法之间能互相替换 条条大道通罗马,具体哪条你来定 把选择权交给用户 选择支付方式
责任链模式(Chain of Responsibility Pattern) 拦截的类都实现统一接口,每个接收者都包含对下一个接收者的引用。将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。 各人自扫门前雪,莫管他们瓦上霜 解耦处理逻辑 踢皮球
迭代器模式(Iterator Pattern) 提供一种方法顺序访问一个聚合对象中的各个元素 流水线上坐一天,每个包裹扫一遍 统一对集合的访问方式 逐个检票进站
命令模式(Command Pattern) 将请求封装成命令,并记录下来,能够撤销与重做 运筹帷幄之中,决胜千里之外 解耦请求和处理 遥控器
状态模式(State Pattern) 根据不同的状态做出不同的行为 状态驱动行为,行为决定状态 绑定状态和行为 订单状态跟踪
备忘录模式(Memento Pattern) 保存对象的状态,在需要时进行恢复 失足不成千古恨,想重来时就重来 备份、后悔机制 草稿箱
中介者模式(Mediator Pattern) 将对象之间的通信关联关系封装到一个中介类中单独处理,从而使其耦合松散 联系方式我给你,怎么搞定我不管 统一管理网状资源 朋友圈
解释器模式(Interpreter Pattern) 给定一个语言,定义它的语法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子 我想说”方言“,一切解释权都归我 实现特定语法解析 摩斯密码
观察者模式(Observer Pattern) 状态发生改变时通知观察者,一对多的关系 到点就通知我 解耦观察者与被观察者 闹钟
访问者模式(Visitor Pattern) 稳定数据结构,定义新的操作行为 横看成岭侧成峰,远近高低各不同 解耦数据结构和数据操作 KPI考核
委派模式(Delegate Pattern) 允许对象组合实现与继承相同的代码重用,负责任务的调用和分配 这个需求很简单,怎么实现我不管 只对结果负责 授权委托书

总结

通过7篇文章,通过理论加代码介绍了7种设计原则 和 23种设计模式,希望可以对读者朋友有所帮助,那么设计模式这块就暂时结束,但我们的编码和学习之路还远没有结束。您如果有任何问题可以评论区留言沟通。

继续阅读