【学习笔记】设计模式之行为型模式

181 阅读10分钟

本文内容包含:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式

1 模板方法模式

  一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

1.1 例子

  定义一个抽象的游戏模板接口,其实现类具体实现各个方法。该例子的UML类图应如下所示: 021-01.png

1、Game抽象接口

public interface Game {
    void init();

    void run();

    void end();

    // 模板方法
    default void play() {
        init();
        run();
        end();
    }
}

2、实现类

public class GTA5 implements Game {
    @Override
    public void init() {
        System.out.println("GTA5 init..");
    }

    @Override
    public void run() {
        System.out.println("GTA5 run..");
    }

    @Override
    public void end() {
        System.out.println("GTA5 end..");
    }
}

public class Persona5 implements Game {
    @Override
    public void init() {
        System.out.println("Persona5 init...");
    }

    @Override
    public void run() {
        System.out.println("Persona5 run...");
    }

    @Override
    public void end() {
        System.out.println("Persona5 end...");
    }
}

3、测试类

public class Player {
    public static void main(String[] args) {
        Game game1 = new GTA5();
        game1.play();

        Game game2 = new Persona5();
        game2.play();
    }
}

1.2 总结

  该模式的使用场景如下:

  • 有多个子类共有的方法,且逻辑相同
  • 重要的、复杂的方法,可以考虑作为模板方法

2 命令模式

  在软件系统中,"行为请求者"与"行为实现者"通常呈现一种紧耦合关系。但在某些场合,比如要对行为进行记录、撤销/重做、事务等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将行为请求者与行为实现者解耦呢?可以将一组行为抽象为对象,实现二者之间的松耦合,这就是命令模式。

  该模式一般有以下角色:

  • Command:定义命令的接口,声明执行的方法
  • ConcreteCommand:具体的命令接口实现对象;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作
  • Receiver:接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能
  • Invoker:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口

  UML类图如下所示: 021-02.png

2.1 例子

  使用命令模式实现遥控器开关电灯。

1、编写Command接口

public interface Command {
    void execute();

    void cancel();
}

2、编写电灯开关命令类,即ConcreteCommand

public class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }

    @Override
    public void cancel() {
        light.off();
    }
}

public class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.off();
    }

    @Override
    public void cancel() {
        light.on();
    }
}

3、编写电灯类,即Receiver

public class Light {
    public void on() {
        System.out.println("电灯打开...");
    }

    public void off() {
        System.out.println("电灯关闭...");
    }
}

4、编写遥控器类,即Invoker

public class RemoteController {
    private List<Command> commands = new LinkedList<>();

    public void addCommand(Command command) {
        commands.add(command);
    }

    public void runCommand() {
        for (Command c : commands) {
            c.execute();
        }
        commands.clear();
    }
}

5、测试类

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

        RemoteController controller = new RemoteController();
        controller.addCommand(new LightOnCommand(light));
        controller.addCommand(new LightOffCommand(light));

        controller.runCommand();
    }
}

2.2 总结

  该模式的使用场景如下:

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
  • 系统需要在不同的时间指定请求、将请求排队(如:线程池、工作队列)和执行请求
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作
  • 系统需要将一组操作组合在一起,即支持宏命令

3 访问者模式

  访问者模式是一种将数据操作数据结构分离的设计模式。

  在访问者模式(Visitor Pattern)中,使用一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象接受访问者对象,这样访问者对象就可以处理元素对象上的操作。

  该模式一般有以下角色:

  • Visitor:接口或者抽象类,定义了对每个 Element 访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式
  • ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为
  • Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问
  • ConcreteElement:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法
  • ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问

  UML类图如下所示: 021-03.png

3.1 例子

  学校每学期期末都要评测学生学习情况。假如有两类学生(普通学生、艺术生),普通学生有成绩,艺术生有成绩和获奖情况。班主任关注普通学生的成绩和艺术生的成绩;校长则关注普通学生的成绩和艺术生的成绩及获奖情况。由于班主任和校长对学生的关注点不一样,就要做不同处理,因此可以使用访问者模式实现。

1、Visitor接口及其具体实现类

public interface Visitor {
    void visit(GeneralStudent student);

    void visit(ArtStudent student);
}

// 班主任
public class Headmaster implements Visitor {
    @Override
    public void visit(GeneralStudent student) {
        System.out.println("普通学生" + student.getName() + "的成绩是:" + student.getMark());
    }

    @Override
    public void visit(ArtStudent student) {
        System.out.println("艺术生" + student.getName() + "的成绩是:" + student.getMark());
    }
}

// 校长
public class President implements Visitor {
    @Override
    public void visit(GeneralStudent student) {
        System.out.println("普通学生" + student.getName() + "的成绩是:" + student.getMark());
    }

    @Override
    public void visit(ArtStudent student) {
        System.out.println("艺术生" + student.getName() + "的成绩是:" + student.getMark() + ",获奖情况是:" + student.getAward());
    }
}

2、Element接口及其具体实现类

public interface Student {
    void accept(Visitor visitor);
}

// 普通学生
public class GeneralStudent implements Student {
    private String name;

    private int mark;

    public GeneralStudent(String name) {
        this.name = name;
        this.mark = new Random().nextInt(100);
    }

    public String getName() {
        return name;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public int getMark() {
        return this.mark;
    }
}

// 艺术生
public class ArtStudent implements Student {
    private String name;

    private int mark;

    private int award;

    public ArtStudent(String name) {
        this.name = name;
        this.mark = new Random().nextInt(100);
        this.award = new Random().nextInt(5);
    }

    public String getName() {
        return name;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public int getMark() {
        return this.mark;
    }

    public int getAward() {
        return this.award;
    }
}

3、ObjectStructure,对应例子的评测单

public class Evaluation {
    private List<Student> students = new ArrayList<>();

    public Evaluation() {
        students.add(new GeneralStudent("张三"));
        students.add(new ArtStudent("李四"));
        students.add(new GeneralStudent("王五"));
    }

    public void view(Visitor visitor) {
        for (Student s : students) {
            s.accept(visitor);
        }
    }
}

4、测试类

public class Test {
    public static void main(String[] args) {
        Evaluation evaluation = new Evaluation();
        Headmaster headmaster = new Headmaster();
        President president = new President();

        System.out.println("==========班主任评测学生==========");
        evaluation.view(headmaster);

        System.out.println("==========校长评测学生==========");
        evaluation.view(president);
    }
}

3.2 总结

  该模式有以下缺点:

  • 具体元素对访问者公开了细节,违反了迪米特原则。比如,班主任和校长需要调用具体学生的方法

  • 具体元素变更时导致修改成本大。比如,当具体学生发生变化时,每个访问者都可能要修改

  • 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。 比如,访问者的visit()方法中依赖了具体学生的方法

4 迭代器模式

  提供一种方法遍历一个聚合对象中的各个元素, 而又无须暴露该对象的内部表示。一般有以下角色:

  • Iterator:迭代器接口,一般定义hasNext()方法和next()方法
  • ConcreteIterator:具体的迭代器
  • Container:聚合对象,即容器接口,一般定义iterator()方法来获取迭代器
  • ConcreteContainer:具体的容器,一般聚合具体的迭代器

  UML类图如下所示: 021-04.png

4.1 例子

  自定义一个容器,然后遍历它。

1、编写迭代器接口

public interface MyIterator<T> {
    boolean hasNext();

    T next();
}

2、编写容器接口

public interface MyContainer<T> {
    MyIterator<T> iterator();
}

3、编写具体的容器类,并聚合一个具体迭代器

public class ConcreteContainer<T> implements MyContainer<T> {
    private Object[] data;

    private int length;

    private class ConcreteIterator implements MyIterator<T> {
        int index;

        @Override
        public boolean hasNext() {
            return index < data.length;
        }

        @Override
        public T next() {
            if (hasNext()) {
                return (T) data[index++];
            }
            return null;
        }
    }

    public ConcreteContainer(int size) {
        data = new Object[size];
    }

    @Override
    public MyIterator<T> iterator() {
        return new ConcreteIterator();
    }

    public void add(T t) {
        if (length < data.length) {
            data[length++] = t;
        } else {
            System.out.println("添加失败,数据已存满");
        }
    }
}

4、编写测试类

public class Test {
    public static void main(String[] args) {
        ConcreteContainer<String> container = new ConcreteContainer<>(4);
        container.add("1");
        container.add("2");
        container.add("3");
        container.add("4");
        container.add("5");
        MyIterator<String> iterator = container.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

4.2 总结

  该模式有以下缺点:

  • 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性

5 观察者模式

  当对象间存在一对多关系时,可以使用观察者模式。比如,当被观察者修改后,则会自动通知它的观察者。该模式一般有以下角色:

  • Subject:主题,即被观察者。定义被观察者的一些方法,常见方法有:添加观察者,移除观察者,通知观察者
  • ConcreteSubject:具体被观察者,会聚合多个观察者
  • Observer:观察者。定义被观察者的一些方法,如更新响应,当Subject发出通知后,要及时做出响应。
  • ConcreteObserver:具体观察者

  UML类图如下所示: 021-05.png

5.1 例子

  气象局的天气数据会经常更新,而各个网站的天气数据都是来自于气象局的,使用观察者模式完成这种设计。

1、Subject接口

public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

2、Observer接口

public interface Observer {
    void update(String weather, double centigrade);
}

3、编写气象局类,即ConcreteSubject

public class WeatherBureau implements Subject {

    private String weather;

    private double centigrade;

    private List<Observer> observerList = new ArrayList<>();

    public WeatherBureau(String weather, double centigrade) {
        this.weather = weather;
        this.centigrade = centigrade;
    }

    public void update(String weather, double centigrade) {
        this.weather = weather;
        this.centigrade = centigrade;
        notifyObservers();
    }

    @Override
    public void registerObserver(Observer o) {
        observerList.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        if (observerList.contains(o)) {
            observerList.remove(o);
        }
    }

    @Override
    public void notifyObservers() {
        for (Observer o : observerList) {
            o.update(weather, centigrade);
        }
    }
}

4、编写具体观察者

public class WebsiteA implements Observer {
    private String weather;

    private double centigrade;

    @Override
    public void update(String weather, double centigrade) {
        this.weather = weather;
        this.centigrade = centigrade;
    }

    public void show() {
        System.out.println("网站A的天气情况");
        System.out.println("天气:" + weather + ", 温度:" + centigrade);
    }
}

public class WebsiteB implements Observer {
    private String weather;

    private double centigrade;

    @Override
    public void update(String weather, double centigrade) {
        this.weather = weather;
        this.centigrade = centigrade;
    }

    public void show() {
        System.out.println("网站B的天气情况");
        System.out.println("天气:" + weather + ", 温度:" + centigrade);
    }
}

5、测试类

public class Test {
    public static void main(String[] args) {
        WeatherBureau weatherBureau = new WeatherBureau("晴", 29.2);
        WebsiteA a = new WebsiteA();
        WebsiteB b = new WebsiteB();

        weatherBureau.registerObserver(a);
        weatherBureau.registerObserver(b);
        weatherBureau.notifyObservers();

        a.show();
        b.show();

        weatherBureau.removeObserver(a);
        weatherBureau.update("雨", 18);
        
        // 由于网站a被移除了,所以其数据仍是旧的
        a.show();
        b.show();
    }
}

5.2 总结

  JDK自带该模式的两个角色,Observable类(被观察者)和Observer接口(观察者)。该模式的使用场景如下:

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用
  • 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度
  • 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象......,可以使用观察者模式创建一种链式触发机制

  但有如下缺点:

  • 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间
  • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃
  • 该模式没有相应的机制让观察者知道被观察者是怎么发生变化的,而仅仅只是知道观察目标发生了变化

6 中介者模式

  中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。

  该模式一般有以下角色:

  • Mediator:抽象中介者,定义一个接口用于与各同事(Colleague)对象通信
  • ConcreteMediator:具体中介者,通过协调各同事对象实现协作行为。了解并维护它的各个同事
  • Colleague:抽象同事,定义各同事的公有方法
  • ConcreteColleague:具体同事,每一个同事类仅需要知道中介者对象;每个具体同事类只需要了解自己的行为,而不需要了解其他同事类的情况。在需要与其他的同事通信的时候,与中介者通信即可

6.1 例子

  使用中介者模式模拟聊天室。该例子的UML类图应如下所示: 021-06.png

1、编写Mediator接口

public interface Mediator {
    void registerColleague(Colleague c);

    void handleMessage(Colleague src, String message);
}

2、编写具体Mediator

public class ChatRoom implements Mediator {
    private Map<String, Colleague> userMap = new HashMap<>();

    @Override
    public void registerColleague(Colleague c) {
        if (null == userMap.get(c.getName())) {
            userMap.put(c.getName(), c);
        }
    }

    @Override
    public void handleMessage(Colleague src, String message) {
        for (Map.Entry<String, Colleague> entry : userMap.entrySet()) {
            if (entry.getKey().equals(src.getName())) {
                continue;
            }
            entry.getValue().getMessage(src, message);
        }
    }
}

3、编写抽象Colleague类

public abstract class Colleague {
    private String name;

    private Mediator mediator;

    public String getName() {
        return name;
    }

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

    public Mediator getMediator() {
        return mediator;
    }

    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    public abstract void getMessage(Colleague src, String message);

    public abstract void sendMessage(String message);
}

4、编写具体Colleague类

public class Student extends Colleague {

    public Student(String name, Mediator mediator) {
        super.setName(name);
        super.setMediator(mediator);
        super.getMediator().registerColleague(this);
    }

    @Override
    public void getMessage(Colleague src, String message) {
        System.out.println("【" + this.getName() + "】从" + src.getName() + "收到消息:" + message);
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("【" + this.getName() + "】发送消息:" + message);
        super.getMediator().handleMessage(this, message);
    }
}


public class Teacher extends Colleague {

    public Teacher(String name, Mediator mediator) {
        super.setName(name);
        super.setMediator(mediator);
        super.getMediator().registerColleague(this);
    }

    // Teacher可能还有其他与Student不同的方法

    @Override
    public void getMessage(Colleague src, String message) {
        System.out.println("【" + this.getName() + "】从" + src.getName() + "收到消息:" + message);
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("【" + this.getName() + "】发送消息:" + message);
        super.getMediator().handleMessage(this, message);
    }
}

5、测试类

public class Test {
    public static void main(String[] args) {
        ChatRoom chatRoom = new ChatRoom();

        Student xiaoMing = new Student("小明", chatRoom);
        Student xiaoHong = new Student("小红", chatRoom);
        Student xiaoLi = new Student("小李", chatRoom);

        Teacher teacher = new Teacher("李老师", chatRoom);

        xiaoHong.sendMessage("你好");
    }
}

6.2 总结

  中介者模式通过引入中介者对象,将系统内部的结构由网状结构转化为星型结构,减少系统中对象引用其它对象的情况,简化了系统结构,降低了系统内部的耦合。由于中介者对象本身需要整理系统内部的对象之间的交互,会导致其本身会变得臃肿和复杂,进而变得难以维护,故在使用中介者模式前需要详细考虑所设计的系统是否适合使用中介者模式,及中介者类本身的设计。

7 备忘录模式

  备忘录模式(Memento Pattern)用于保存一个对象的某个状态,以便在适当的时候恢复对象。常见的场景有:游戏的存档、各个软件的撤销操作等。在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

  该模式一般有以下角色:

  • Originator:发起者,它是一个普通类,可以创建一个备忘录来存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为发起者
  • Memento:备忘录,存储发起者的内部状态,根据实际情况来决定保存哪些内部状态,备忘录的设计一般可以参考发起者的设计。需要注意的是,除了发起者本身与管理者之外,备忘录对象不能直接供给其他类使用
  • Caretaker:管理者,对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改

  UML类图如下所示: 021-07.png

7.1 例子

  使用备忘录模式来保存某对象的状态。

1、编写Originator类

public class Originator {
    private int status;

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Memento saveStatus() {
        return new Memento(status);
    }

    public void recoverStatus(Memento memento) {
        this.status = memento.getStatus();
    }
}

2、编写Memento类

public class Memento {
    private int status;

    public int getStatus() {
        return status;
    }

    public Memento(int status) {
        this.status = status;
    }
}

3、编写Caretaker类

public class Caretaker {
    private List<Memento> mementoList = new ArrayList<>();

    public void addMemento(Memento memento) {
        mementoList.add(memento);
    }

    public Memento getMemento(int index) {
        return mementoList.get(index - 1);
    }
}

4、测试类

public class Client {
    public static void main(String[] args) {
        Originator o = new Originator();
        o.setStatus(5);

        Caretaker caretaker = new Caretaker();
        caretaker.addMemento(o.saveStatus());

        o.setStatus(3);
        // 输出3
        System.out.println(o.getStatus());

        o.recoverStatus(caretaker.getMemento(1));
        // 恢复后,输出5
        System.out.println(o.getStatus());
    }
}

7.2 总结

  该模式有以下优点:

  • 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态
  • 实现了信息的封装,使得用户不需要关心状态的保存细节

  但有以下缺点:

  • 资源消耗过大,如果需要保存的Originator类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源

8 解释器模式

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

  该模式一般有以下角色:

  • AbstractExpression:抽象表达式,定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()
  • TerminalExpression:终结符表达式,是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符一般都有一个具体终结表达式与之相对应
  • NonterminalExpression:非终结符表达式,也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。一般会聚合抽象表达式,并递归解释句子
  • Context:上下文,通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值

  UML类图如下所示: 021-08.png

8.1 例子

  假设某公园的门票有两种优惠方式,如生肖属"牛"可打八折,若是女性客户还能再打九折。而且优惠方式会发生变化,将来可能是属"虎"以及男性客户打折。使用解释器模式来实现:客户只需出示身份证明,如"1997的男性",就能自动算出最终价格。

1、编写抽象表达式

public interface Expression {
    double interpret(String info);
}

2、编写终结符表达式,在本例中应该有两个,一个用于解释生肖,另一个用于解释性别

public class ZodiacExpression implements Expression {
    private String[] zodiacs = {"猴", "鸡", "狗", "猪", "鼠", "牛",
            "虎", "兔", "龙", "蛇", "马", "羊"};

    private String rightZodiac;

    public ZodiacExpression(String rightZodiac) {
        this.rightZodiac = rightZodiac;
    }

    @Override
    public double interpret(String info) {
        int year = Integer.valueOf(info);
        int index = year % 12;
        if (zodiacs[index].equals(rightZodiac)) {
            return 0.8;
        }
        return 1;
    }
}

public class SexExpression implements Expression {
    private String rightSex;

    public SexExpression(String rightSex) {
        this.rightSex = rightSex;
    }

    @Override
    public double interpret(String info) {
        if (rightSex.equals(info)) {
            return 0.9;
        }
        return 1;
    }
}

3、编写非终结符表达式

public class OrExpression implements Expression {
    private Expression zodiac = null;

    private Expression sex = null;

    public OrExpression(Expression zodiac, Expression sex) {
        this.zodiac = zodiac;
        this.sex = sex;
    }

    private double originalPrice;

    public void setOriginalPrice(double originalPrice) {
        this.originalPrice = originalPrice;
    }

    @Override
    public double interpret(String info) {
        String[] newData = info.split("的");
        return originalPrice * zodiac.interpret(newData[0]) * sex.interpret(newData[1]);
    }
}

4、编写上下文类

public class Park {
    private OrExpression notTerminal;

    public Park(double price, String zodiac, String sex) {
        Expression zodiacExpression = new ZodiacExpression(zodiac);
        Expression sexExpression = new SexExpression(sex);
        notTerminal = new OrExpression(zodiacExpression, sexExpression);
        notTerminal.setOriginalPrice(price);
    }

    public double pay(String info) {
        return notTerminal.interpret(info);
    }
}

5、客户端

public class Client {
    public static void main(String[] args) {
        // 相当于定义了规则,原价400,属牛可打八折,若为女性还能再打九折
        Park park = new Park(400, "牛", "女性");

        System.out.println(park.pay("1997的女性")); // 输出288.0
        System.out.println(park.pay("1997的男性")); // 输出320.0
        System.out.println(park.pay("1998的女性")); // 输出360.0
        System.out.println(park.pay("1998的男性")); // 输出400.0
    }
}

8.2 总结

  如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。

  但该模式有以下缺点:

  • 对于复杂的文法比较难维护
  • 解释器模式会引起类膨胀
  • 解释器模式采用的是递归调用的方法

9 状态模式

  在状态模式(State Pattern)中,类的行为是基于它的状态改变的。允许对象在内部状态发生改变时改变它的行为,使得对象看起来好像修改了它的类。

  该模式一般有以下角色:

  • Context:环境/上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中一般要聚合State抽象状态类
  • State:抽象状态类,定义接口以封装"与环境类某状态相关的行为",在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类的方法实现可能有所不同,相同的方法可以写在抽象状态类中
  • ConcreteState:具体状态类,抽象状态类的子类,每一个子类实现"与环境类某状态相关的行为",每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同

  UML类图如下所示: 021-09-01.png

9.1 例子

  某抽奖活动的流程大致如下图所示,使用状态模式实现。 021-09-02.png

1、编写抽象状态类

public interface State {
    void consume(Activity activity);

    void raffle(Activity activity);
}

2、编写具体状态类,包括不可抽奖状态、可抽奖状态、活动结束状态

// 不可抽奖状态
public class CannotRaffleState implements State {
    @Override
    public void consume(Activity activity) {
        System.out.println("支付2元成功,可以抽奖了");
        activity.toCanRaffleState();
    }

    @Override
    public void raffle(Activity activity) {
        System.out.println("您未付钱,不能抽奖");
    }
}

// 可抽奖状态
public class CanRaffleState implements State {
    @Override
    public void consume(Activity activity) {
        System.out.println("您已支付了2元");
    }

    @Override
    public void raffle(Activity activity) {
        System.out.println("抽奖中...");
        if (0 == new Random().nextInt(10)) {
            System.out.println("恭喜您中奖了");
            activity.reducePrize();
        } else {
            System.out.println("很遗憾,您没有中奖");
            activity.toCannotRaffleState();
        }
    }
}

// 活动结束状态
public class EndState implements State {
    @Override
    public void consume(Activity activity) {
        System.out.println("活动已结束,请下次再来");
    }
    @Override
    public void raffle(Activity activity) {
        System.out.println("活动已结束,请下次再来");
    }
}

3、编写活动类,即环境/上下文

public class Activity {
    private int num;

    private State currentState;

    private State cannotRaffleState = new CannotRaffleState();

    private State canRaffleState = new CanRaffleState();

    private State endState = new EndState();

    public Activity(int num) {
        this.num = num;
        toCannotRaffleState();
    }

    // 支付
    public void consume() {
        currentState.consume(this);
    }

    // 抽奖
    public void raffle() {
        currentState.raffle(this);
    }
    
    public void reducePrize() {
        num--;
        System.out.println("奖品数量剩余 " + num + " 个");
        if (0 == num) {
            toEndState();
        }
    }

    public void toCannotRaffleState() {
        currentState = cannotRaffleState;
    }

    public void toCanRaffleState() {
        currentState = canRaffleState;
    }

    public void toEndState() {
        currentState = endState;
    }
}

4、客户端

public class Client {
    public static void main(String[] args) {
        Activity activity = new Activity(2);
        for (int i = 0; i < 10; i++) {
            activity.consume();
            activity.raffle();
            System.out.println("---------------------------------");
        }
    }
}

9.2 总结

  在代码中包含大量与对象状态有关的条件语句时可以使用该模式,但该模式有以下缺点:

  • 状态模式的使用必然会增加系统类和对象的个数
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱
  • 状态模式对"开闭原则"的支持并不太好,增加新的状态类需要修改那些"负责状态转换"的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码

10 策略模式

  类的行为或其算法可以在运行时更改,在有多种算法相似的情况下,使用 if/else 难以维护,这时可以使用策略模式。该模式有以下角色:

  • Strategy:策略接口,定义行为或算法
  • 各个具体Strategy实现类
  • Context:使用了各个策略的类

  UML类图如下所示: 021-10.png

10.1 例子

  使用策略模式实现简单数学运算。

1、Strategy接口

public interface Strategy {
    int operation(int num1, int num2);
}

2、Strategy具体实现类

public class AddOperation implements Strategy {
    @Override
    public int operation(int num1, int num2) {
        return num1 + num2;
    }
}

public class ReduceOperation implements Strategy {
    @Override
    public int operation(int num1, int num2) {
        return num1 - num2;
    }
}

3、Context类

public class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public int operation(int num1, int num2) {
        return strategy.operation(num1, num2);
    }
}

4、测试类

public class Test {
    public static void main(String[] args) {
        Context context1 = new Context(new AddOperation());
        System.out.println(context1.operation(10, 5));

        Context context2 = new Context(new ReduceOperation());
        System.out.println(context2.operation(10, 5));
    }
}

10.2 总结

  该模式与状态模式相似,但状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么Context对象就会改变它的行为;而策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换Context使用的算法。策略模式的使用场景如下:

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为
  • 一个系统需要动态地在几种算法中选择一种

11 职责链模式

  责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果当前对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

  该模式一般有以下角色:

  • Request:请求,用于被接收者处理
  • Handler:处理者/接收者,是一个抽象类,定义处理请求的方法接口,一般会聚合另一个Handler
  • ConcreteHandler:具体处理者/接收者,是Handler的子类,具体实现处理请求的方法。倘若该处理者不能处理请求,则把请求传给它所聚合的Handler并让其处理,以此形成一个处理请求的链

  UML类图如下所示: 021-11.png

11.1 例子

  假设某学校要买进一批教学器材,若金额小于3000,则交给教导主任审批;若金额大于等于3000并小于10000,则交给副校长审批;若金额大于10000,则交给校长审批。使用职责链模式实现。

1、编写Request

public class BuyRequest {
    private double money;

    public BuyRequest(double money) {
        this.money = money;
    }

    public double getMoney() {
        return money;
    }
}

2、编写抽象Handler

public abstract class Handler {
    protected Handler nextHandler;

    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public abstract void examine(BuyRequest request);
}

3、编写具体Handler

// 教导主任
public class DirectorTeacher extends Handler {
    @Override
    public void examine(BuyRequest request) {
        if (request.getMoney() < 3000) {
            System.out.println("本次购买器材已被教导主任审核通过");
        } else {
            nextHandler.examine(request);
        }
    }
}

// 副校长
public class VicePrincipal extends Handler {
    @Override
    public void examine(BuyRequest request) {
        if (request.getMoney() >= 3000 && request.getMoney() < 10000) {
            System.out.println("本次购买器材已被副校长审核通过");
        } else {
            nextHandler.examine(request);
        }
    }
}

// 校长
public class Principal extends Handler {
    @Override
    public void examine(BuyRequest request) {
        if (request.getMoney() >= 10000) {
            System.out.println("本次购买器材已被校长审核通过");
        } else {
            nextHandler.examine(request);
        }
    }
}

4、测试类

public class Client {
    public static void main(String[] args) {
        BuyRequest request = new BuyRequest(20000);

        Handler directorTeacher = new DirectorTeacher();
        Handler vicePrincipal = new VicePrincipal();
        Handler principal = new Principal();

        directorTeacher.setNextHandler(vicePrincipal);
        vicePrincipal.setNextHandler(principal);
        // 这样写可以形成一个环
        principal.setNextHandler(directorTeacher);

        directorTeacher.examine(request);
    }
}

11.2 总结

  使用该模式,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链降低了请求发送者与请求处理者之间的耦合度。但也有一些缺点:

  • 不能保证请求一定被接收
  • 系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用
  • 可能不容易观察运行时的特征,有碍于除错

12 相关链接