行为型模式-中介者模式与解释器模式

2020-03-22 1806点热度 0人点赞 0条评论
设计模式系列文章:https://zhum.in/blog/category/notes/design-pattern
Demo code:https://github.com/njZhuMin/BlogSampleCode

中介者模式

中介者模式(Mediator Pattern)又称为调解者模式或调停者模式。中介者模式使用一个中介对象来封装一系列的对象交互过程,使各对象之间不需要显式地直接相互调用,从而松散耦合,而且可以独立地改变它们之间的交互。属于行为型模式。

一个系统中,如果各层次对象之间存在大量的关联关系,或者呈现复杂的网状结构,如果直接让它们紧耦合通信,会造成系统结构变得异常复杂。且其中某个层次对象发生改变时,与其紧耦合的相应层次对象也需进行修改,系统很难进行维护。

而通过为该系统增加一个中介者层次对象,让其他各层次需对外通信的行为统统交由中介者进行转发,使系统呈现以中介者为中心进行通讯的星形结构,系统的复杂性大大降低。
简单的说就是多个类相互耦合,形成了网状结构,则可以考虑使用中介者模式进行优化。

中介者模式主要包含 4 个角色:

  • 抽象中介者(Mediator):定义统一的接口,用于各同事角色之间的通信;
  • 具体中介者(ConcreteMediator):从具体的同事对象接收消息,向具体同事对象发出命令,协调各同事间的协作;
  • 抽象同事类(Colleague):每一个同事对象均需要依赖中介者角色,与其他同事间通信时,交由中介者进行转发协作;
  • 具体同事类(ConcreteColleague):负责实现自发行为(Self-Method),转发依赖方法(Dep-Method)交由中介者进行协调。

中介者模式适用场景:

  1. 系统中对象之间存在复杂的引用关系,相互依赖的关系结构复杂混乱;
  2. 交互的公共行为,如果需要改变行为则可以增加新的中介者类。

案例

这里我们用一个群聊场景来演示中介者模式。假设我们要构建一个聊天室系统,用户可以向聊天室发送消息,聊天室会向所有的用户显示消息。不过用户无法直接将信息发给其他用户,而是需要先将信息发送到服务器上,然后服务器再将该消息发给聊天室进行显示。

public class ChatRoom {
    public void showMsg(User user,String msg) {
        System.out.println("[" + user.getName() + "]: " + msg);
    }
}

public class User {
    private String name;
    private ChatRoom chatRoom;

    public User(String name, ChatRoom chatRoom) {
        this.name = name;
        this.chatRoom = chatRoom;
    }
    public String getName() {
        return name;
    }

    public void sendMessage(String msg) {
        this.chatRoom.showMsg(this,msg);
    }
}

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

        User tom = new User("Tom", chatRoom);
        User jerry = new User("Jerry", chatRoom);

        tom.sendMessage("Hi! I am Tom.");
        jerry.sendMessage("Hello! My name is Jerry.");
    }
}

中介者模式在源码中的应用

JDK 中的 Timer 类就是一个典型的中介者角色。

public class Timer {

    private final TaskQueue queue = new TaskQueue();

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

        // Constrain value of period sufficiently to prevent numeric
        // overflow while still being effectively infinitely large.
        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();
        }
    }
}

其中有很多重载的 shedule 方法,这些方法最终都调用的是 sched 这个私有方法。在这个方法中,不管用 Timer 创建出什么样的任务,都被加入到一个队列中顺序调度执行。这个队列中的所有任务对象就相当于中介者模式的 “同事” 角色。同事之间通信都是通过 Timer 类来协调完成的,Timer 类承担了中介者的角色。

小结

中介者模式与代理模式:

  1. 中介者的角色是牵线搭桥的作用,是 “不负责任” 的代理,牵线之后所有的事情还是由你自己完成;代理者的作用是完成你做不到而又必须要做的事,往往还需要扩展被代理对象的功能。
  2. 代理模式是一对一,一个代理只能代表一个对象。中介者模式则是多对多,中介者的功能多样,客户也可以多个。
  3. 代理模式是单向的,中介者模式是同级相互的。

中介者模式与门面模式:

  1. 门面模式是对子系统提供统一的接口,中介者模式是用一个中介对象来封装一系列同事对象(同级)的交互行为。
  2. 门面模式协议是单向,中介者模式协议是双向。
  3. 门面模式所有的请求处理都委托给子系统完成,而中介者模式则由中心协调同事类和中心本身共同完成业务。

中介者模式与桥接模式:

  • 桥接强调不是对象之间的通信,而是通过非继承的方式耦合不同维度的属性。

中介者模式与命令模式:

  1. 中介者模式一般接口是固定的(即请求比较固定);
  2. 命令模式请求一般不固定,可能会改变,所以将命令进行对象化和独立化使得代码更加灵活。

中介者模式的优缺点
优点:

  1. 减少类间依赖,将多对多的依赖关系转化成了一对多,降低了类间耦合;
  2. 类间各司其职,符合迪米特法则。

缺点:

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

解释器模式

解释器模式(Interpreter Pattern)是指给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表达来解释语言中的句子。解释器模式是一种按照规定的语法(文法)进行解析的模式,属于行为型模式。

例如编译器可以将源码编译解释为机器码,让 CPU 能进行识别并运行。解释器模式的作用其实与编译器的思想一样,都是对某些固定的文法进行解释,从而构建出一个解释表达式的解释器。简单理解,解释器是一个简单语法分析工具,它可以识别句子语义,分离终结符号和非终结符号,提取出需要的信息,让我们能针对不同的信息做出相应的处理。

解释器模式在我们日常生活中也很常见,例如我们平时所听到的音乐都是通过乐谱进行编曲而来的。还有战争年代发明的摩斯密码,其实也是一种解释器。

解释器模式主要包含四种角色:

  • 抽象表达式(Expression):负责定义一个解释方法 interpret,交由具体子类进行具体解释;
  • 终结符表达式(TerminalExpression):实现文法中与终结符有关的解释操作。文法中的每一个终结符都有一个具体终结表达式与之相对应。通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应不同的终结符。
  • 非终结符表达式(NonterminalExpression):实现文法中与非终结符有关的解释操作。文法中的每条规则都对应于一个非终结符表达式。非终结符表达式一般是文法中的运算符或者其他关键字。非终结符表达式根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式。
  • 上下文环境类(Context):包含解释器之外的全局信息。它的任务一般是用来存放文法中各个终结符所对应的具体值。

结合一个具体的表达式 R = R1 + R2 来简单解释一下:

  • R1 和 R2 就是终结符,对应的解析 R1 和 R2 的解释器就是终结符表达式。
  • "+" 是非终结符,解析"+" 的解释器就是一个非终结符表达式。
  • 给 R1 赋值 100,给 R2 赋值 200,这些具体值需要存放到环境中。

应用案例

解释器模式适用于以下应用场景:

  1. 一些重复出现的问题可以用一种简单的语言来进行表达;
  2. 一个简单语法需要解释的场景。

这里来使用解释器模式来实现一个简易的四则运算计算表达式的解析:

public interface IArithmeticInterpreter {
    int interpret();
}

public abstract class Interpreter implements IArithmeticInterpreter {
    protected IArithmeticInterpreter left;
    protected IArithmeticInterpreter right;

    public Interpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
        this.left = left;
        this.right = right;
    }
}

public class AddInterpreter extends Interpreter {
    public AddInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
        super(left, right);
    }
    public int interpret() {
        return this.left.interpret() + this.right.interpret();
    }
}

public class SubInterpreter extends Interpreter {
    public SubInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
        super(left, right);
    }
    public int interpret() {
        return this.left.interpret() - this.right.interpret();
    }
}

public class MultiInterpreter extends Interpreter {
    public MultiInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
        super(left, right);
    }
    public int interpret() {
        return this.left.interpret() * this.right.interpret();
    }
}

public class DivInterpreter extends Interpreter {
    public DivInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
        super(left, right);
    }
    public int interpret() {
        return this.left.interpret() / this.right.interpret();
    }
}

public class NumInterpreter implements IArithmeticInterpreter {
    private int value;
    public NumInterpreter(int value) {
        this.value = value;
    }
    public int interpret() {
        return this.value;
    }
}

public enum OperatorEnum {
    LEFT_BRACKET("("),
    RIGHT_BRACKET(")"),
    SUB("-"),
    ADD("+"),
    MUL("*"),
    DIV("/");

    private String operator;
    public String getOperator() {
        return operator;
    }
    OperatorEnum(String operator) {
        this.operator = operator;
    }
}

实现表达式解析:

public class OperatorUtil {

    public static boolean isOperator(String symbol) {
        return Arrays.stream(OperatorEnum.values()).anyMatch(operator -> symbol.equals(operator.getOperator()));
    }

    public static Interpreter getInterpreter(Stack<IArithmeticInterpreter> numStack, Stack<String> operatorStack) {
        IArithmeticInterpreter right = numStack.pop();
        IArithmeticInterpreter left = numStack.pop();
        String operator = operatorStack.pop();

        System.out.println("Pop left, right number: " + left.interpret() + ", "
                + right.interpret());
        System.out.println("Pop operator: " + operator);

        if (operator.equals(OperatorEnum.ADD.getOperator()))
            return new AddInterpreter(left, right);
        if (operator.equals(OperatorEnum.SUB.getOperator()))
            return new SubInterpreter(left, right);
        if (operator.equals(OperatorEnum.MUL.getOperator()))
            return new MultiInterpreter(left, right);
        if (operator.equals(OperatorEnum.DIV.getOperator()))
            return new DivInterpreter(left, right);
        return null;
    }
}

public class MyCalculator {
    private Stack<IArithmeticInterpreter> numStack = new Stack<>();
    private Stack<String> operatorStack = new Stack<>();

    public MyCalculator(String expression) {
        this.parse(expression);
    }

    /**
     * Only support operators separated by space, e.g. "10 + 30"
     * @param expression
     */
    private void parse(String expression) {
        System.out.println("Parsing: " + expression);

        for (String ele : expression.split("\\s+")) {
            // ele is Operator
            if (OperatorUtil.isOperator(ele)) {
                // ele is "+" or "-", calculate all operators in stack
                if (ele.equals(OperatorEnum.ADD.getOperator()) ||
                        ele.equals(OperatorEnum.SUB.getOperator())) {
                    while (!operatorStack.isEmpty() && (
                            operatorStack.peek().equals(OperatorEnum.ADD.getOperator()) ||
                            operatorStack.peek().equals(OperatorEnum.SUB.getOperator()) ||
                            operatorStack.peek().equals(OperatorEnum.MUL.getOperator()) ||
                            operatorStack.peek().equals(OperatorEnum.DIV.getOperator()))) {
                        numStack.push(OperatorUtil.getInterpreter(numStack, operatorStack));
                        System.out.println("Push NumStack: " + numStack.peek());
                    }
                    System.out.println("Push OperatorStack: " + ele);
                    operatorStack.push(ele);
                }
                // ele is "x" or "/", calculate "x", "/" and skip "+", "-"
                if (ele.equals(OperatorEnum.MUL.getOperator()) ||
                        ele.equals(OperatorEnum.DIV.getOperator())) {
                    while (!operatorStack.isEmpty() && (
                            operatorStack.peek().equals(OperatorEnum.MUL.getOperator()) ||
                            operatorStack.peek().equals(OperatorEnum.DIV.getOperator()))) {
                        numStack.push(OperatorUtil.getInterpreter(numStack, operatorStack));
                        System.out.println("Push NumStack: " + numStack.peek());
                    }
                    System.out.println("Push OperatorStack: " + ele);
                    operatorStack.push(ele);
                }
                // ele is "(", push stack and do nothing
                if (ele.equals(OperatorEnum.LEFT_BRACKET.getOperator())) {
                    System.out.println("Push OperatorStack: " + ele);
                    operatorStack.push(ele);
                }
                // ele is ")", calculate till operator stack is "("
                if (ele.equals(OperatorEnum.RIGHT_BRACKET.getOperator())) {
                    System.out.println("Encounter ), calculate...");
                    while (!operatorStack.isEmpty() &&
                            !operatorStack.peek().equals(OperatorEnum.LEFT_BRACKET.getOperator())) {
                        numStack.push(OperatorUtil.getInterpreter(numStack, operatorStack));
                        System.out.println("Push NumStack: " + numStack.peek());
                    }
                    // pop "(" in operator stack
                    String pop = operatorStack.pop();
                    System.out.println("Pop OperatorStack: " + pop);
                }
            }
            // ele is number, just push stack
            else {
                NumInterpreter numInterpreter = new NumInterpreter(Integer.parseInt(ele));
                System.out.println("Push NumStack: " + ele);
                numStack.push(numInterpreter);
            }
        }

        System.out.println("Start final calculation...");
        while (!operatorStack.isEmpty()) {
            numStack.push(OperatorUtil.getInterpreter(numStack, operatorStack));
        }
    }

    public int calculate() {
        return this.numStack.pop().interpret();
    }
}

解释器模式在源码中的应用

JDK 中的 Pattern 类提供了对正则表达式的支持:

public final class Pattern implements Serializable {
    private Pattern(String p, int f) {
        pattern = p;
        flags = f;

        // to use UNICODE_CASE if UNICODE_CHARACTER_CLASS present
        if ((flags & UNICODE_CHARACTER_CLASS) != 0)
            flags |= UNICODE_CASE;

        // Reset group index count
        capturingGroupCount = 1;
        localCount = 0;

        if (pattern.length() > 0) {
            compile();
        } else {
            root = new Start(lastAccept);
            matchRoot = lastAccept;
        }
    }
}

另一个应用案例是 Spring-Expressions 模块,我们先来简单测试一下:

public class SpringTest {
    public static void main(String[] args) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(
                "10 + 30 / ( ( 6 - 4 ) * 2 - 2 )");
        int result = (Integer) expression.getValue();
        System.out.println("Result = " + result);
    }
}

来看 SpelExpressionParser 类的实现:

public class SpelExpressionParser extends TemplateAwareExpressionParser {
    private final SpelParserConfiguration configuration;

    protected SpelExpression doParseExpression(String expressionString, ParserContext context) throws ParseException {
        return (new InternalSpelExpressionParser(this.configuration)).doParseExpression(expressionString, context);
    }
}

class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
    private final Stack<SpelNodeImpl> constructedNodes = new Stack();
    private List<Token> tokenStream;
}

这里的实现逻辑比我们的实现复杂很多,但可以大致猜测到这里的 Stack 和 List 做的是和我们类似的表达式解析和出入栈的操作。

小结

解释器模式的优点:

  1. 扩展性强:在解释器模式中,语法是由很多类表示的,当语法规则更改或扩展时,只需修改相应的非终结符表达式即可;
  2. 增加了新的解释表达式的方式;
  3. 易于实现文法:解释器模式对应的文法应当是比较简单且易于实现的,过于复杂的语法不适合使用解释器模式。

缺点:

  1. 语法规则较复杂时会引起类膨胀:解释器模式中,每个语法都要产生一个非终结符表达式。当语法规则比较复杂时,就会产生大量的解释类,增加系统维护困难;
  2. 执行效率比较低:解释器模式采用递归调用方法,每个非终结符表达式只关心与自己有关的表达式。每个表达式需要知道最终的结果,因此完整表达式的最终结果是通过从后往前递归调用的方式获取得到。当完整表达式层级较深时,解释效率下降,且调试困难。

SilverLining

也可能是只程序猿

文章评论