【设计模式】- 行为型模式2

发布于:2025-05-19 ⋅ 阅读:(18) ⋅ 点赞:(0)

观察者模式

定义了一对多的依赖关系,让多个观察者对象同时监听某一个对象主题。这个主题对象在状态变化时,会通知所有的观察者对象,让他们能够自动更新自己。

主要角色

  • 抽象主题角色:把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以添加和删除观察者对象
  • 具体主题:该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知
  • 抽象观察者:是观察者的抽象类,定义了一个更新接口,在主题更改时通知更新自己
  • 具体观察者:实现抽象观察者定义的更新接口,以便在得到主题更改时通知更新自身的状态

案例:微信公众号

需求】:当这个微信公众号的专栏发生变更后,所有订阅了这个微信公众的用户都会收到通知。

抽象的观察者:

public interface Observer {
    void update(String message);
}

订阅公众号的人(具体的观察者):

@AllArgsConstructor
public class WeiXinUser implements Observer {
    private String name;
    @Override
    public void update(String message) {
        System.out.println(name + "-" + message);
    }
}

抽象主题角色:

public interface Subject {
    // 添加订阅者(观察者对象)
    void attach(Observer observer);
    // 删除订阅者
    void detach(Observer observer);
    // 通知订阅者更新消息
    void notify(String message);
}

公众号类(具体主题类):

public class SubscriptionSubject implements Subject {
    // 定义一个集合,存储多个观察者对象
    private List<Observer> weixinUserList = new ArrayList<>();
    @Override
    public void attach(Observer observer) {
        weixinUserList.add(observer);
    }

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

    @Override
    public void notify(String message) {
        // 通知所有的观察者对象去调用观察者对象的update()方法
        weixinUserList.forEach(observer -> observer.update(message));
    }
}

测试类:

public class Client {
    public static void main(String[] args) {
        // 1. 创建公众号对象
        SubscriptionSubject subject = new SubscriptionSubject();
        // 2. 订阅公众号
        subject.attach(new WeiXinUser("孙悟空"));
        subject.attach(new WeiXinUser("猪八戒"));
        subject.attach(new WeiXinUser("沙僧"));
        // 3. 公众号更新,发消息给订阅者(观察者对象)
        subject.notify("专栏更新了");
    }
}

适用场景

  1. 对象间存在一对多的状态,一个对象的状态改变会影响其他对象
  2. 一个抽象模型有两个方面,其中一个方面依赖于另一个方面

JDK中提供的实现

通过Observable类和Observer接口定义了观察者模式,只要实现他们的子类就可以编写观察者模式实例。

Observable类:抽象主题类

  1. void addObserver(Observer var1)方法:将新的观察者对象添加到集合中
  2. void notifyObservers(Object var1)方法:调用集合中所有观察者对象的update()方法,通知他们数据发生变化。(越晚加入集合的越先通知,因为遍历时从后往前的)
  3. void setChanged()方法:用来设置一个boolean的内部标志,表明目标对象发生变化,当他为true时,notifyObservers()才会通知观察者

Observer接口:抽象观察者

监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用update()方法,进行相应的工作。

案例:警察抓小偷

警察是观察者对象,小偷是被观察者对象

小偷(被观察者,需要继承Observable类):

@Data
@AllArgsConstructor
public class Thief extends Observable {
    private String name;
    public void steal() {
        System.out.println("小偷:我偷东西了");
        super.setChanged(); // changed = true
        super.notifyObservers(); // 只有把changed改成true,这个方法才会被执行
    }
}

警察(观察者类,需要继承Observer接口):

@Data
@AllArgsConstructor
public class Policeman implements Observer {
    private String name;

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("警察:" + ((Thief)o).getName() + "抓到你了");
    }
}

测试类:

public class Client {
    public static void main(String[] args) {
        // 创建小偷对象
        Thief t = new Thief("老王");
        // 创建警察对象
        Policeman p = new Policeman("小林");
        // 让警察盯着小偷
        t.addObserver(p); // 订阅主题(把警察添加到小偷的集合中,小偷偷东西就会通知警察)
        // 小偷偷东西
        t.steal();
    }
}

中介者模式

同事之间的关系是错综复杂的(网状结构)
但是如果引入中介者,就是呈现一种星形结构,所有的对象只需要和中介者有关联即可。
在这里插入图片描述

中介者模式:定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合冲散,且可以独立改变他们之间的交互

主要角色

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

案例:租房

房屋中介(抽象中介者类):

public abstract class Mediator {
    public abstract void constact(String message, Person person);
}

抽象 租客 or 房主类:

@AllArgsConstructor
public abstract class Person {
    // 租房者 or 房主的姓名
    protected String name;
    // 中介
    protected Mediator mediator;
    // 和中介联系
    abstract void constact(String message);
    // 获取信息
    abstract void getMessage(String message);
}

具体的租客类:

public class Tenant extends Person {
    public Tenant(String name, Mediator mediator) {
        super(name, mediator);
    }

    @Override
    public void constact(String message) {
        mediator.constact(message, this);
    }
    @Override
    public void getMessage(String message) {
        System.out.println("租房者" + name + "获取到的信息:" + message);
    }
}

具体的房主类:

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

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

    @Override
    void getMessage(String message) {
        System.out.println("房主" + name + "获取到的信息:" + message);
    }
}

具体的中介者角色:

@Data
public class MediatorStructure extends Mediator {
    // 房主
    private HouseOwner houseOwner;
    // 租房者对象
    private Tenant tenant;

    @Override
    public void constact(String message, Person person) {
        if(person == houseOwner) { // 房主
            tenant.getMessage(message);
        }else{ // 租房者
            houseOwner.getMessage(message);
        }
    }
}

测试类:

public class Client {
    public static void main(String[] args) {
        // 1. 创建中介者对象
        MediatorStructure mediator = new MediatorStructure();
        // 2. 创建租房者对象
        Tenant tenant = new Tenant("李四", mediator);
        // 3. 创建房主对象
        HouseOwner houseOwner = new HouseOwner("张三", mediator);
        // 4.中介者知道房主和租房者对象
        mediator.setTenant(tenant);
        mediator.setHouseOwner(houseOwner);
        // 5. 租房者和中介沟通
        tenant.constact("我要租房"); // 中介者会把这个信息发给房主
        // 6. 房主回应消息
        houseOwner.constact("我有房子");
    }
}

适用场景

  1. 系统中对象之间存在复杂的引用关系
  2. 想创建一个运行于多个类之间的对象(中介者角色),又不想生成新的子类

迭代器模式

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。

主要角色

  • 抽象聚合角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口
  • 具体聚合角色:实现抽象聚合角色,返回具体迭代器实例
  • 抽象迭代器角色:定义访问和遍历聚合元素的接口
  • 具体迭代器角色:定义抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置

案例:学生容器

学生类:

@Data
@AllArgsConstructor
public class Student {
    private String name; // 姓名
    private String number; // 学号
}

抽象迭代器角色接口:

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() {
        return list.get(position++);
    }
}

抽象聚合角色类:

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) {
        // 1. 创建聚合对象
        StudentAggregateImpl aggregate = new StudentAggregateImpl();
        // 2.添加元素
        aggregate.addStudent(new Student("小林", "001"));
        aggregate.addStudent(new Student("xiaolin", "002"));
        aggregate.addStudent(new Student("03", "003"));
        // 3. 遍历聚合对象
        StudentIterator iterator = aggregate.getStudentIterator(); // 获取迭代器对象
        while(iterator.hasNext()) { // 判断是否有元素
            System.out.println(iterator.next()); // 输出
        }
    }
}

适用场景

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

JDK源码解析:Iterator类

List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
while(itertaor.hasNext()) {
	System.out.println(iterator.next());
}

在开发中,如果想使用迭代器模式,只需要让我们自己实现的类实现java.util.Iterable,并实现其中的iterator()方法,使其返回一个java.util.Iterable的实现类就可以了

访问者模式

封装一些作用于某种数据结构中的个元素的操作,可以在不改变数据结构的前提下,定义作用于这些元素的新操作。

主要角色】:

  • 抽象访问者角色:定义了对每一个元素的访问行为(访问者模式要求元素的个数不能改变)
  • 具体访问者角色:给出对每个元素类访问时所产生的具体行为
  • 抽象元素角色:定义了一个接受访问者的方法(accept),每一个元素都要可以被访问者访问
  • 具体元素角色:提供接收访问角色的具体方法
  • 对象结构角色:定义了对象结构,会包含一组元素,可以迭代这些元素,供访问者使用

案例:给宠物喂食

需求】:要给宠物(狗、猫)喂食的时候,主人可以喂,其他人也可以喂

  • 访问者角色:给宠物喂食的人
  • 具体访问者角色:主人、其他人
  • 抽象元素角色:动物抽象类
  • 具体元素角色:宠物狗、宠物猫
  • 结构对象角色:主人家

人(访问者角色类):

public interface Person {
    // 给宠物猫喂食
    void feed(Cat cat);
    // 给宠物猫喂食
    void feed(Dog dog);
}

主人、其他人(具体访问者角色类):

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 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 Home {
    // 存储元素对象
    private List<Animal> nodeList = new ArrayList<>();
    // 添加元素
    public void add(Animal animal) {
        nodeList.add(animal);
    }
    //
    public void action(Person person) {
        // 遍历集合,获取每一个元素,让访问者访问每一个元素
        nodeList.forEach(animal -> animal.accept(person));
    }
}

测试类:

public class Client {
    public static void main(String[] args) {
        // 创建Home对象
        Home home = new Home();
        home.add(new Dog());
        home.add(new Cat());
        // 创建主人对象
        Owner owner = new Owner();
        // 让主人喂食所有的宠物
        home.action(owner);
    }
}

适用场景

对象结构相对稳定,操作算法经常变化

扩展:双分派技术

分派技术

分派:变量被声明的类型叫变量的静态类型,根据对象的类型对方法进行的选择就是分派。

  1. 动态分派:发生在运行十七,动态分派动态的置换某个方法(方法重写)
class Animal {
    public void eat() {}
}
class Dog extends Animal {
    public void eat() {
        System.out.println("dog");
    }
}
class Cat extends Animal {
    public void eat() {
        System.out.println("cat");
    }
}
public class Demo01 {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.eat();
        a = new Cat();
        a.eat();
    }
}
  1. 静态分派:发生在编译时期,分派根据静态类型信息发生(方法重载)
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class Execute {
    public void execute(Animal a) {
        System.out.println("animal");
    }
    public void execute(Dog d) {
        System.out.println("dog");
    }
    public void execute(Cat c) {
        System.out.println("cat");
    }
}
public class Demo02 {
    public static void main(String[] args) {
        Animal a = new Animal();
        Animal d = new Dog();
        Animal c = new Cat();

        Execute e = new Execute();
        e.execute(a);// animal
        e.execute(d);// animal
        e.execute(c);// animal
    }
}

重载方法的分派是在编译时期进行的,这个分派过程在编译时期就完成了

双分派技术

在选择一个方法时,不仅仅要根据消息接收者的运行时区别,还要根据参数的运行时区别。

class Execute {
    public void execute(Animal a) {
        System.out.println("animal");
    }
    public void execute(Dog d) {
        System.out.println("dog");
    }
    public void execute(Cat c) {
        System.out.println("cat");
    }
}
class Animal {
    public void accept(Execute e) {
        e.execute(this);
    }
}
class Dog extends Animal {
    public void accept(Execute e) {
        e.execute(this);
    }
}
class Cat extends Animal {
    public void accept(Execute e) {
        e.execute(this);
    }
}

public class Demo03 {
    public static void main(String[] args) {
        Animal a = new Animal();
        Animal d = new Dog();
        Animal c = new Cat();

        Execute e = new Execute();
        a.accept(e); // animal
        d.accept(e); // dog
        c.accept(e); // cat
    }
}

上边代码经历了两次分派

  1. 第一次分派(方法重写、静态分派):客户端将Execute对象作为参数传递给Animal类型的变量调用方法
  2. 第二次分派(方法重载、动态分派):每个类将自己this作为参数传递,这里的Execute类中有多个重载的方法,而传递的this是具体的实际类型对象

双分派实现动态绑定的本质:在重载方法委派的前面加上了继承体系中的覆盖环节,因为覆盖是动态的,所以重载就是动态的了。

备忘录模式

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

主要角色

  • 发起人角色:记录当前时刻的内部状态信息,提供创建、恢复备忘录的功能
  • 备忘录角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人
  • 管理者角色:对备忘录进行管理,提供保存、获取备忘录的功能,不能对备忘录的内容进行访问和修改。

案例:游戏挑战boss

需求】:游戏角色有生命力、攻击力、防御力等数据,在打boss前后都不一样,我们允许玩家如果感觉和boss决斗的效果不理想可以让游戏恢复到快斗之前的状态

白箱备忘录模式

备忘录角色对任何对象都一个一个接口(宽接口),备忘录角色内部所存储的对象就对所有对象公开。

游戏角色类(发起人角色):

@Data
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 memento) {
        this.vit = memento.getVit();
        this.atk = memento.getAtk();
        this.def = memento.getDef();
    }

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

}

备忘录角色类:

@NoArgsConstructor
@AllArgsConstructor
@Data
public class RoleStateMemento {
    private int vit; // 生命力
    private int atk; // 攻击力
    private int def; // 防御力
}

备忘录管理对象:

@Data
public class RoleStateCaretaker {
    // 备忘录
    private RoleStateMemento roleStateMemento;
}

测试类:

public class Client {
    public static void main(String[] args) {
        // 创建游戏角色对象
        GameRole gameRole = new GameRole();
        gameRole.initState(); // 初始数据
        gameRole.stateDisplay();

        // 备份内部状态
        RoleStateCaretaker caretaker = new RoleStateCaretaker();
        caretaker.setRoleStateMemento(gameRole.saveState());

        gameRole.fight(); // 战斗
        gameRole.stateDisplay();

        // 恢复备忘录
        gameRole.recoverState(caretaker.getRoleStateMemento());
        gameRole.stateDisplay();

    }
}

上边之所以是白箱备忘录模式,是因为管理者、客户端其实都可以拿到备忘录对象,进而对备忘录对象里的内容进行操作。

黑箱备忘录模式

双重接口:备忘录角色对发起人对象提供宽接口,对其他对象提供窄接口(将备忘录类设置成发起人类的成员内部类)

备忘录接口(对外提供窄接口):

public interface Memento {
}

备忘录管理对象:

@Data
public class RoleStateCaretaker {
    // 备忘录
    private Memento memento; // 面向接口(返回的是接口,所以在外界是访问不到里边的成员的)
}

游戏角色类(发起人角色):

@Data
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(Memento memento) {
        RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
        this.vit = roleStateMemento.vit;
        this.atk = roleStateMemento.atk;
        this.def = roleStateMemento.def;
    }

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

    // 成员内部类,必须实现Memento接口
    // 把备忘录角色类定义在了发起者角色类中,并将它私有
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    class RoleStateMemento implements Memento {
        private int vit; // 生命力
        private int atk; // 攻击力
        private int def; // 防御力
    }
}

测试类:

public class Client {
    public static void main(String[] args) {
        // 创建游戏角色对象
        GameRole gameRole = new GameRole();
        gameRole.initState(); // 初始数据
        gameRole.stateDisplay();

        // 备份内部状态
        RoleStateCaretaker caretaker = new RoleStateCaretaker();
        caretaker.setMemento(gameRole.saveState());

        gameRole.fight(); // 战斗
        gameRole.stateDisplay();

        // 恢复备忘录
        gameRole.recoverState(caretaker.getMemento());
        gameRole.stateDisplay();
    }
}

把备忘录角色类定义在了发起者角色类中,并将它私有,这样外界就不能访问了

适用场景

  1. 需要保存和恢复数据的场景(玩游戏时的中间结果存档功能)
  2. 需要提供一个可回滚操作的场景
    • word、记事本按ctrl + z
    • 数据库中事务操作

解释器模式

定义一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。

主要角色

  • 抽象表达式角色:约定解释器的解释操作(主要包含解释方法interpret())
  • 终结符表达式角色:抽象表达式的子类,用来标识文法中与终结符相关的操作
  • 非终结符表达式角色:抽象表达式的子类,用来表示文法中与非终结符相关的操作
  • 环境角色:包含各个解释器需要的数据或公共的功能
  • 客户端:将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树

案例:实现加减法的软件

抽象表达式类:

public abstract class AbstractExpression {
    public abstract int interpret(Context context);
}

环境角色类:

public class Context {
    // 定义map集合,存储变量及对应的值
    private Map<Variable, Integer> map = new HashMap<>();
    // 添加变量
    public void assign(Variable var, Integer value) {
        map.put(var, value);
    }
    // 根据变量获取对应值
    public int getValue(Variable var) {
        return map.get(var);
    }
}

封装变量的类:

@Data
@AllArgsConstructor
public class Variable extends AbstractExpression {
    // 声明存储【变量名】的成员变量
    private String name;
    @Override
    public int interpret(Context context) {
        // 返回变量的值
        return context.getValue(this);
    }
    public String toString() {
        return name;
    }
}

减法、加法表达式类

@AllArgsConstructor
@Data
public class Minus extends AbstractExpression {
    // 加号左边的表达式
    private AbstractExpression left;
    // 加号右边的表达式
    private AbstractExpression right;

    @Override
    public int interpret(Context context) {
        // 将左边表达式的结果和右边表达式的结果相减
        return left.interpret(context) - right.interpret(context);
    }

    public String toString() {
        return "(" + left.toString() + " - " + right.toString() + ")";
    }
}

@AllArgsConstructor
@Data
public class Plus extends AbstractExpression {
    // 加号左边的表达式
    private AbstractExpression left;
    // 加号右边的表达式
    private AbstractExpression right;

    @Override
    public int interpret(Context context) {
        // 将左边表达式的结果和右边表达式的结果相加
        return left.interpret(context) + right.interpret(context);
    }

    public String toString() {
        return "(" + left.toString() + " + " + right.toString() + ")";
    }
}

测试类:

public class Client {
    public static void main(String[] args) {
        // 创建环境对象
        Context context = new Context();
        // 创建多个变量对象
        Variable a = new Variable("a");
        Variable b = new Variable("b");
        Variable c = new Variable("c");
        Variable d = new Variable("d");
        // 将变量存储到环境中
        context.assign(a, 1);
        context.assign(b, 2);
        context.assign(c, 3);
        context.assign(d, 4);
        // 构建抽象语法树
        AbstractExpression expression = new Minus(a, new Plus(new Minus(b, c), d));
        // 解释(计算)
        int result = expression.interpret(context);
        System.out.println(expression + " = " + result);
    }
}

每条规则都需要定义一个类

适用场景

  1. 问题重复出现,可以用一种简单的语言来进行表达
  2. 一个语言需要解释执行,并且语言中的句子可以表示为一个抽象的语法树

网站公告

今日签到

点亮在社区的每一天
去签到