原创
最近更新: 2022/10/17 17:03

设计模式 (三) 行为型模式

设计模式 | 菜鸟教程

黑马程序员Java设计模式详解

目录

设计模式 (一) 创建型模式

设计模式 (二) 结构型模式

设计模式 (三) 行为型模式


模板模式

设计程序过程中可能遇到如下情况:所有关键步骤已知,执行顺序已知,但有部分步骤的具体实现未知。此时可以运用模板模式:定义一个算法骨架,将具体实现延迟到子类中。

其实本质上是面向对象继承多态思想的一个具体实现方案。

包含以下组件:

  • 抽象类:给出一个算法的骨架。

    • 模板方法:按照某种顺序调用其他的基本方法,定义为final防止子类重写。

    • 基本方法:实现具体步骤的方法,又可以分为:

      • 抽象方法:暂时无法实现,由子类实现。
      • 具体方法:已知具体实现的方法,可在父类中实现,也可由子类重写。
      • 钩子方法(可选):包括用于判断的逻辑方法和需要子类重写的空方法,用来决定父类中某个步骤是否执行。
  • 具体子类:实现抽象类中的抽象方法和钩子方法。

优点:提高代码复用性,符合开闭原则。

缺点:对于不同的实现都需要定义一个子类,系统复杂度增加,也提高代码阅读难度。


策略模式

定义了一系列功能相近的算法,并将每种算法封装起来,使他们可以相互替换,且算法的变化不会影响使用算法的客户。

使用场景:有多种功能相似且相互独立的算法需要动态地选择,且用户无须知道算法的实现细节。如排序算法中可以使用不同的比较器。

包含以下组件:

  • 抽象策略:给出具体策略所需要的共用接口。
  • 具体策略:根据不同算法给出实现。
  • 环境类:持有一个策略类的引用,执行具体策略,提供对外接口,是用户和策略的中介。
//抽象策略
public interface Strategy {
    void do();
}

//具体策略
public class StrategyA implements Strategy {
    public void do {
        ...
    }
}
public class StrategyB implements Strategy {
    public void do {
        ...
    }
}

//环境类
public class Context {
    private Strategy strategy;

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

	public void setStrategy(Strategy strategy) {
		this.strategy = strategy;
	}

    public void userDo() {
        strategy.do();
    }

}

可以通过享元模式减少策略的构造。

优点:不同策略之间易于切换,避免多重逻辑语句,方便扩展,符合开闭原则。


命令模式

将一个请求封装为一个对象,使发出请求和执行请求的责任分隔开,降低程序耦合度。

包含以下组件:

  • 抽象命令类:定义命令接口和执行方法。

  • 具体命令类:实现命令接口,通常会持有接收者,并调用接收者的方法来执行操作。

  • 接收者/执行者类:真正执行命令的对象,具有执行对应命令的相关功能。

  • 请求者/调用者类:是调用命令的外部接口,持有一系列命令对象,负责要求命令执行。

Java中Runable接口就是典型的命令模式:

  • Runable是抽象命令类,run()方法是执行方法;
  • Thread是调用者,start()是Thread类中调用命令的接口;
  • 接收者由程序员自己定义。
//抽象命令类
public interface Command {
    void execute();
}

//具体命令类
public class CreateCommand {

    private String info;//命令包含的具体信息
    private Executor executor;//命令执行者

    public CreateCommand(Executor executor, String info) {
        this.info = info;
        this.executor = executor;
    }

    void execute() {
        this.executor.executeCommand(String Info);
    };
}

//======================

//执行者类
public class Executor {
	//仅负责命令的执行
    void executeCommand(String Info) {...}
}

//调用者类
public class Invoker {

    private List<Command> commands;//持有一系列命令

    public void setCommand(Command cmd) {
        commands.add(cmd);
    }

    public void createCommand() {
        if (commands.length == 0) return;
        for (Command cmd: commands) {
            cmd.execute();
        }
    }
}

优点:

  • 降低系统耦合度
  • 可以方便地添加或者删除命令,扩展性好
  • 可以与备忘录模式相结合,实现撤销和重做操作
  • 可以实现批处理命令,方便命令的调度

缺点:可能导致系统中有太多命令类。


责任链模式

如果一个请求的处理按先后顺序涉及一系列的系统,为了避免请求发送者与多个处理对象耦合在一起,可以将请求处理视作一道链条,链条上的每一个节点记录下一个节点对象的引用,请求发送者可以让请求沿着链条传递,最终解决问题。

应用实例:

  • iOS中的点击事件的处理就是责任链模式,当用户点击手机上的一个控件时,系统会先检查相应控件能否相应点击,如不能,则上溯到父控件,直到底层窗口。

  • DNS域名解析服务(旧版):访问网站时,输入域名后,本地DNS服务器将请求发给根域名服务器,并在各级DNS服务器之间转发,最终返回IP地址。(新版中为了降低DNS服务器的负载,将转发的指责放在了本地DNS服务器中,但本质相似)

包含以下组件:

  • 抽象处理者:定义一个处理请求的接口,包括处理方法以及一个后继处理者

  • 具体处理者:实现处理请求的方法,判断能否处理请求,能则处理,否则交给后继处理者。

  • 客户类:创建处理链,只需要向链条的头部发送请求,不关心请求的传递过程。

代码略。

优点:降低耦合度,提高可扩展性,简化系统复杂度。

缺点:

  • 不能保证每一个请求都能得到处理
  • 当职责链太长时系统性能受到影响
  • 如果错误设置职责链会导致循环调用

状态模式

对于有状态的对象,将不同的状态独立地封装到对象中,并设置对应动作。

是有限状态机的改进版本,将复杂的“状态判断”逻辑封装到一个个单独的对象中,允许不同状态对不同操作独立地改变行为,降低了程序的耦合度。

Flutter中有一种Stateful构件,允许在页面运行过程中动态改变页面内容,就是由状态模式实现的。

包含以下组件:

  • 上下文类:定义客户接口,存储和维护状态信息,并将具体操作委托给状态对象来处理。

  • 抽象状态:规定不同状态所需要执行的操作。

  • 具体状态:实现特定状态下的操作细节。或许可以用享元模式或单例模式减少额外的状态对象的开销?

结构上类似代理模式,可以将状态看作是上下文对象的代理。

//案例:电梯运行,上下文即为电梯
public class Context {

    //定义对应状态的常量,有三个状态:开门、关门/停止、运行
    public final static OpenState OPEN_STATE = new OpenState();
    public final static RunState RUN_STATE = new RunState();
    public final static StopState STOP_STATE = new StopState();

    //当前状态对象变量
    private LiftState state;

    public LiftState getState() {
        return state;
    }

    public void setState(LiftState state) {
        this.state = state;
        this.state.setContext(this);//状态对象会反过来操作上下文
    }

    //外部接口,调用状态对象进行实际操作
    public void open() {
        this.state.open();
    }
    public void run() {
        this.state.run();
    }
    public void stop() {
        this.state.stop();
    }
}

//============定义状态===========
//抽象状态类
public abstract class LiftState {

    //需要持有一个上下文变量,类似代理模式
    protected Context context;

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

    //有几个状态就要定义几个改变状态的方法
    public abstract void open();
    public abstract void run();
    public abstract void stop();
}

//---------定义对应状态的类---------

//电梯门开启状态
public class OpenState extends LiftState{

	//状态内动作
    @Override
    public void open() {
        //做开门动作
        System.out.println("电梯门开启完毕。");
    }

    @Override
    public void run() {}//假设电梯门开启时不能运行

    @Override
    public void stop() {
        context.setState(Context.STOP_STATE);
        System.out.println("电梯把门关上。");
        context.stop();//状态改变后再进行一次状态内动作
    }
}

//电梯运行状态
public class RunState extends LiftState{

    @Override
    public void open() {}//假设电梯运行时不能开门

    @Override
    public void run() {
        //做运行动作
        System.out.println("电梯在升降。");
    }

    @Override
    public void stop() {
        context.setState(Context.STOP_STATE);
        System.out.println("电梯逐渐停下。");
        context.stop();
    }
}

//电梯停止/关门状态,假设电梯停止状态是运行和开门两个状态的中介
public class StopState extends LiftState{

    @Override
    public void open() {
        context.setState(Context.OPEN_STATE);
        System.out.println("电梯开门。");
        context.open();
    }

    @Override
    public void run() {
        context.setState(Context.RUN_STATE);
        System.out.println("电梯从静止开始运行。");
        context.run();
    }

    @Override
    public void stop() {
        //做静止动作
        System.out.println("电梯静止。");
    }
}

//============调用方法=============
Context lift = new Context();
lift.setState(Context.STOP_STATE);

lift.run();
lift.stop();
lift.open();

假设有n种状态,那么在抽象状态中就应当对应有改变状态的n条方法,那么总共就需要实现n*n个具体的方法。

优点:

  • 可以方便地增加新的状态。
  • 状态转换时避免了巨大的条件语句,而是拆分细化到了不同对象的不同方法中。

缺点:

  • 程序耦合度依旧较高,对“开闭原则”支持不太好。
  • 实现较为复杂,容易造成代码结构的混乱。

观察者模式

又称作发布-订阅模式,定义了一种一对多的依赖关系,让多个观察者对象监听某一个主题对象。这个主题对象状态变化时,会通知所有的观察者,使他们做出相应动作。

iOS开发中的通知Notification组件就是观察者模式。

包含以下组件:

  • 抽象被观察者:将任意多观察者保存在一个集合中;提供一个接口,可以增加和删除观察者

  • 具体被观察者:当自己的内部状态发生改变时,给所有注册过的观察者发通知。

  • 抽象观察者:是观察者的抽象类,定义一个更新接口,收到通知时调用。

  • 具体观察者:实现上述更新接口。

//抽象被观察者
public interface Observed {

    public void addObserver(Observer observer);
    public void removeObserver(Observer observer);
    public void notify(String message);
}

//具体被观察者
public class NotificationCenter implements Observed {

    private List<Observer> observers;

    public NotificationCenter() {
        observers = new LinkedList<>();
    }

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

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

//======================
//抽象观察者
public interface Observer {
    public void update(String message);
}

//具体观察者
public class Listener implements Observer {
    public void update(String message) {
        //...
    }
}

优点:降低了观察者和被观察者的耦合;可以实现广播机制。 缺点:当观察者很多,会很耗系统资源。


中介者模式

又叫调停模式,如果一组对象之间存在两两交互,而对象数量一多,这个交互关系就很复杂;此时可以定义一个中介介入,对象只和中介交互,可以简化管理,降低耦合。

MVC模式中,controller就可以看作是model与view的中介。

包含以下组件:

  • 抽象中介:定义同辈对象的注册和消息转发接口。

  • 具体中介:维护一个列表来管理同辈对象。

  • 抽象同辈:保存中介者的一个引用,实现消息发送和处理的接口。

  • 具体同辈:实现上述接口。

代码略,简单来说就是同辈对象和中介互相持有,同辈发送信息的方法会调用中介的转发方法,中介的转发方法又调用同辈的接收方法。

//以信息聊天室为例,聊天室是中介者,用户是同辈

//抽象中介者类
public interface MessageMediator {
    void transform(String message, Peer sender);
    void add(Peer peer);
}

//具体中介者类
public class ChattingRoom implements MessageMediator{

    private List<Peer> users;

    public ChattingRoom() {
        this.users = new ArrayList<>();
    }

    @Override
    public void add(Peer peer) {
        users.add(peer);
        peer.setMessageMediator(this);
    }

    @Override
    public void transform(String message, Peer sender) {
        //此处稍微破坏了依赖原则
        System.out.println(((User)sender).getName() + ": " + message);
        for (Peer user: users) {
            user.receive(message);
        }
    }
}

//=====================
//抽象同辈类
public interface Peer {
    void send(String message);
    void receive(String message);
    void setMessageMediator(MessageMediator chattingRoom);
}

public class User implements Peer{

    private String name;
    private MessageMediator chattingRoom;

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

    @Override
    public void setMessageMediator(MessageMediator chattingRoom) {
        this.chattingRoom = chattingRoom;
    }

    public String getName() {
        return name;
    }

	//处理消息的方法
    @Override
    public void send(String message) {
        this.chattingRoom.transform(message, this);
    }
    @Override
    public void receive(String message) {
        System.out.println(name + "收到消息:" + message);
    }
}

//调用方法
ChattingRoom chattingRoom = new ChattingRoom();
User zhangsan = new User("张三");
User lisi = new User("李四");

chattingRoom.add(zhangsan);
chattingRoom.add(lisi);

zhangsan.send("大家好我是张三");
lisi.send("大家好我是李四");

优点:降低耦合度,将多对多的关系变为一对一的关系,多个同辈对象的交互被集中到中介中管理,当交互行为变化时只需要修改中介对象即可。

缺点:中介类可能会变得庞大且难以维护。


迭代器模式

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

包含以下组件:

  • 抽象容器:定义添加、删除、创建迭代器等接口

  • 具体容器

  • 抽象迭代器:定义访问元素的接口,通常包含hasNext()、next()等

  • 具体迭代器

//抽象迭代器
public interface Iterator<T> {
    boolean hasNext();
    T next();
}

//具体迭代器
public class ListIterator<T> implements Iterator<T>{

    private List<T> list;//需要持有对应容器或其底层数据结构的引用,这里仅为示意
    private int index = -1;

    public ListIterator(List<T> list) {
        this.list = list;
    }

    @Override
    public boolean hasNext() {
        return index < list.size() - 1;
    }

    @Override
    public T next() {
        if (hasNext()) {
            return list.get(++index);
        }
        return null;
    }
}

迭代器是在各个语言的各种容器中都有广泛应用。

优点:支持以不同方式遍历一个容器,只需要定义不同的迭代器即可


访问者模式

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

包含以下组件:

  • 抽象访问者:定义了对元素的访问行为,参数就是元素,方法数量理论上等于元素的种类数。

  • 具体访问者:实现了对每一种元素的具体行为。

  • 抽象元素:定义一个接受访问者的方法,即,每一个元素都必须能被访问者访问。

  • 具体元素:实现被访问者访问时的方法。

  • 数据结构类:是存储元素的容器。

代码略,使用场景不明。


备忘录模式

又叫快照模式,在不破坏封装性的情况下,捕获一个对象的内部状态,并在对象之外保存这个状态,以便以后需要时恢复到原先保存的状态。主要用于实现撤销操作。

包含以下组件:

  • 发起人:记录当前时刻的内部信息,提供创建备忘录和恢复数据的功能,可以访问备忘录中的所有信息。

  • 备忘录:负责存储发起人的内部状态,在需要时提供这些状态。

  • 管理者:对备忘录进行管理,提供保存和获取备忘录信息的功能,但不能修改。

备忘录通过两种接口实现上面的结果:窄接口和宽接口。

  • 窄接口只允许传输备忘录的数据,但不允许修改。
  • 宽接口允许读取和修改所有数据。
//以记事本为例

//抽象备忘录,用于定义宽接口
public interface Memento {
    String getState();
}

//发起者类
public class NoteBook {

    //将备忘录设计成私有内部类,防止外部对象随意修改
    private class NoteBookMemento implements Memento {

        private String state;

		//窄接口,只有发起者类能调用
        NoteBookMemento(String state) {
            this.state = state;
        }
		//宽接口,由于在接口中定义了这个方法,所以外部也可调用
        @Override
        public String getState() {
            return state;
        }
    }

    private String content;

    public void setContent(String content) {
        this.content = content;
    }
    public String getContent() {
        return content;
    }

    //保存和恢复备忘录的接口
    public Memento saveState() {
        return new NoteBookMemento(content);
    }
    public void recoverState(Memento memento) {
        content = memento.getState();
    }
}

//备忘录管理类
public class MementoCareTaker {

    private Memento memento;

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

    //外部可以调用宽接口
    public String getState() {
        return memento.getState();
    }
}


解释器模式

定义一种语言的文法表示,并定义解释这种语言的方法。一般使用抽象语法树实现。多少沾点编译原理。

包含以下组件:

  • 抽象表达式:定义解释器接口,约定解释器的解释操作,主要包含interpret()方法。

  • 原子表达式:抽象表达式的一个子类,涉及的表达式往往有独立语义。文法中每一个原子语义都有一个具体的原子表达式对应。

  • 关系表达式:抽象表达式的一个子类,涉及的表达式反映原子表达式的某种关系,不能单独使用。文法中每条规则都对应于一个关系表达式。

  • 上下文:包含解释器需要的数据或公共功能,一般用来传递被所有解释器共享的数据。

  • 客户端:将分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的方法。也可以通过上下文间接访问解释器。

这玩意有点复杂,代码就不写了,到要用的时候再说吧。

优点:

  • 抽象语法树中每一个表达式节点类的实现方法都是相似的,实现方法不会太复杂。
  • 可以通过继承等机制来改变和扩展文法,可以方便地实现一个语言。
  • 可以简单地添加新的表达式,符合“开闭原则”。

缺点:

  • 难以维护复杂文法。
  • 使用了大量循环和递归,执行效率低。

评论区