把书读薄 | 《设计模式之美》设计模式与范式(行为型-解释器模式)

818 阅读3分钟

这是我参与8月更文挑战的第9天,活动详情查看: 8月更文挑战

0x0、引言

😭 周末沉迷农药,喜提五连跪,继续啃设计模式,本文对应设计模式与范式:行为型(72),解释器模式 (Interpreter Pattern),用来描述如何构建一个简单的 "语言"解释器

比命令模式更小众,只有在特定领域才会用到,如编译器、规则引擎、正则表达式、SQL等,而这类语言又称 领域特定语言 (Domain Specific Language, DSL)。

工作中很少会让我们去编写一个解释器,了解下即可,主要是借鉴思想 如何通过更简洁的规则来表示复杂逻辑

Tips:二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。


0x1、定义

原始定义

为某个语言定义它的语法(文法)表示,并定义一个解释器用来处理这个语法。

有点懵逼,没事,先来看下它的四个组成角色:

  • AbstractExpression (抽象表达式) → 定义一个解释器有哪些 解释操作,具体类实现还分 终结符解释器非终结符解释器
  • TerminalExpression (终结符表达式) → 实现 文法中元素相关联的解释操作,通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应不同的终结符。终结符一般是文法中的运算单元,如简单公式R=R1+R2,其中的R1和R2就是终结符,对应R1和R2的解释器就是终结符表达式;
  • NonterminalExpression (非终结符表达式) → 文法中的每条规则 对应于一个非终结符表达式,一般是文法中的运算符或其他关键字。如R=R1+R2里的+就是非终结符,解释+的解释器就是非终结符表达式。它会根据逻辑的复杂度增加而增加,原则上每个文法规则都对应一个非终结符表达式。
  • Context (上下文环境) → 存放各个终结符对应的具体值,还是R=R1+R2,给R1赋值100,给R2赋值200,这些信息需存放到环境角色中,很多情况下使用Map充当环境角色就够了~

直接画出UML类图

0x2、写个简单例子

定义一个能解释加减法的解释器为例子~

// 抽象表达式
public abstract class AbstractExpression {
    public abstract int interpreter(Context context);
    @Override abstract public String toString();
}

// 上下文环境
public class Context {
    private Map<AbstractExpression, Integer> map = new HashMap<>();

    public void addExpression(AbstractExpression expression, int value) {
        map.put(expression, value);
    }

    public int lookup(AbstractExpression expression) { 
        return map.get(expression);
    }
}

// 终结符表达式(常量与变量)
public class ConstantExpression extends AbstractExpression {
    private int value;

    public ConstantExpression(int value) {
        this.value = value;
    }

    @Override
    public int interpreter(Context context) {
        return value;
    }

    @Override
    public String toString() {
        return Integer.toString(value);
    }
}

public class VariableExpression extends AbstractExpression {
    private String name;

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

    @Override
    public int interpreter(Context context) {
        return context.lookup(this);
    }

    @Override
    public String toString() {
        return name;
    }
}

// 非终结符表达式(加法和减法)
public class PlusExpression extends AbstractExpression {
    private final AbstractExpression leftExpression;
    private final AbstractExpression rightExpression;

    public PlusExpression(AbstractExpression leftExpression, AbstractExpression rightExpression) {
        this.leftExpression = leftExpression;
        this.rightExpression = rightExpression;
    }

    @Override
    public int interpreter(Context context) {
        return leftExpression.interpreter(context) + rightExpression.interpreter(context);
    }

    @Override
    public String toString() {
        return leftExpression.toString() + " + " + rightExpression.toString();
    }
}

public class MinusExpression extends AbstractExpression {
    private final AbstractExpression leftExpression;
    private final AbstractExpression rightExpression;

    public MinusExpression(AbstractExpression leftExpression, AbstractExpression rightExpression) {
        this.leftExpression = leftExpression;
        this.rightExpression = rightExpression;
    }

    @Override
    public int interpreter(Context context) {
        return leftExpression.interpreter(context) - rightExpression.interpreter(context);
    }

    @Override
    public String toString() {
        return leftExpression.toString() + " - " + rightExpression.toString();
    }
}

// 测试用例
public class Client {
    public static void main(String[] args) {
        Context context = new Context();
        AbstractExpression a = new VariableExpression("a");
        AbstractExpression b = new VariableExpression("b");
        AbstractExpression c = new VariableExpression("c");

        context.addExpression(a, 6);
        context.addExpression(b, 8);
        context.addExpression(c, 16);

        AbstractExpression e1 = new PlusExpression(a, b);
        System.out.println(e1 + " = " + e1.interpreter(context));
        AbstractExpression e2 = new PlusExpression(e1, c);
        System.out.println(e2 + " = " + e2.interpreter(context));
        AbstractExpression e3 = new MinusExpression(e2, new ConstantExpression(7));
        System.out.println(e3 + " = " + e3.interpreter(context));
        AbstractExpression e4 = new MinusExpression(e3, b);
        System.out.println(e4 + " = " + e4.interpreter(context));
    }
}

代码运行输出结果如下

使用场景

  • 语言语法较为简单,且对执行效率要求不高时,如正则判断IP是否合法;
  • 问题重复出现,且可用简单语法来进行表达时,如if-else统一解释为条件语句;
  • 当一个语言需要解释执行时,如XML中<>括号标识不同的结点含义;

优点

  • 易于实现语法,一条语法用一个解释器对象解释执行;
  • 易于扩展新语法,只需创建对应解释器,抽象语法树时使用即可;

缺点

  • 可使用场景少,复用性不高,除了发明新的编程语言或对某些新硬件进行解释外,很少用,特定数据结构,扩展性也低;
  • 维护成本高,每种规则至少要定义一个解释类,语法规则越多,类越难管理和维护;
  • 执行效率低,递归调用方法,解释句子语法复杂时,会执行大量循环语句;

0x3、加餐:Kotlin中创建一个自己的DSL示例

顺带提下Kotlin中的DSL,感兴趣的可以看看,用到了下面这些黑魔法:

扩展函数(属性) + lambda表达式 + 中缀表达式 + 运算符重载(invoke)

代码示例如下:

// 饭店
data class Restaurant(var name: String, var score: Int, var price: Int, var address: String)

// 上下文类
data class Eating(
        var kind: String? = "",
        var area: String? = "",
        var restaurantList: List<Restaurant>? = mutableListOf()
) {
    override fun toString() = StringBuilder().apply {
        append("【${this@Eating.area}】的【${this@Eating.kind}】有下述门店:\n\n")
        this@Eating.restaurantList?.forEach { append("${it.name} - ${it.score}分 - ${it.price}元/人 - ${it.address}\n") }
        append("\n")
    }.toString()
}

class RestaurantBuilder {
    var name: String = ""
    var score: Int = 0
    var price: Int = 0
    var address: String = ""
    fun build(): Restaurant = Restaurant(name, score, price, address)
}

// 中间类,用于实现直接DSL方式添加Restaurant的效果
class EatingBuilder {
    var kind: String? = ""
    var area: String? = ""
    var restaurantList = mutableListOf<Restaurant>()

    fun restaurant(block: RestaurantBuilder.() -> Unit) {
        // 此处演示下invoke实现,可以直接restaurantList.add(RestaurantBuilder().apply(block).build())
        val restaurantBuilder = RestaurantBuilder()
        block.invoke(restaurantBuilder)
        restaurantList.add(restaurantBuilder.build())
    }

    fun build(): Eating = Eating(kind, area, restaurantList)
}

// 创建Eating的DSL写法(顶层函数)
fun eating(block: EatingBuilder.() -> Unit): Eating = EatingBuilder().apply(block).build()

// 此处为了演示使用中缀表达式,实际上可以直接赋值
infix fun Eating.area(area: String) {
    this.area = area
}

fun main() {
    val eating = eating {
        kind = "西式快餐"
        restaurant {
            name = "华莱士·全鸡汉堡"
            score = 5
            price = 19
            address = "观澜牛湖店"
        }
        restaurant {
            name = "汉堡王"
            score = 5
            price = 33
            address = "东海店"
        }
        restaurant {
            name = "麦当劳"
            score = 4
            price = 34
            address = "绒花路店"
        }
    }
    eating.area("平湖区")
    println(eating.toString())
}

代码运行输出结果如下


以上就是本节课程的全部内容,谢谢~