具有可扩展性的公式计算器

近日,学习了一下解释器模式(地址:http://www.cnblogs.com/cbf4life/archive/2009/12/17/1626125.html),作者用一个公式计算器的例子来阐述

解释器模式,该计算器能完成加减法的计算:

  1. 给定任意加减法公式,eg:a+b-c

  2. 分别给定a b c的值

  3. 计算公式的值

 

本文通过改写这个例子,使这个公式计算器更加强大,增加了以下功能:

A. 支持括号符

B. 支持乘除法等优先级不同的运算符

C. 可扩展其他运算符,真正做到开闭原则

 

1. 表达式

  首先,要说的是表达式。表达式可以是一个变量,也可以是一个符号(比如加减乘除)。它们有一个相同点,就是都可以通过解析得到一个值、一个结果。不同点是解析的方式不同:对于变量的解析直接取它的值即可;而符号的解析则依赖其附近的表达式的结果。这里有必要解释一下,“附近”是个较含糊的词,符号的左边还是右边,还是左右两边,这里就涉及到运算符本身的性质,即单目运算符还是双目运算符。”表达式的结果“是指其周围表达式通过解析得到的结果,这里很明显是一种递归的概念。

  根据以上的讨论,我们可以这样设计表达式的继承关系:

 

  

 

  a. Expression

    首先是表达式的抽象类,它只提供一个解析的抽象方法。

    public abstract class Expression {

      public abstract int intepreter(Context context);
    }

    Context是指运算需要的上下文。

 

  b. VarExpression

    表示变量表达式,它包含一个key的域,其intepreter方法是根据key从Context中映射出相应的值,并返回。

 

  c. SignExpression及其子孙类。

    所有运算符必须继承SignExpression类,它提供了一个PRIORITY域,即运算符的优先级。

    UnarySignExpression扩展自SignExpression,表示单目运算符的父类。因为是单目运算符,它只关心前一个表达式解析的结果,而这个表达式可以是一个变量,也可以是一个运算符。所以它包含了一个类型为Expression的实例。

    BinarySignExpression扩展子SignExpression,表示双目运算符的父类。同样它只关心左右表达式解析的结果,故其包含两个类型为Expression的域。

 

  

2. 公式算法

  算法分为两步:1. 中缀表达式转后缀表达式  2. 生成expression并递归解析。

 

  a 中缀表达式转后缀表达式

   解析一个表达式其实并不难,已经有很多现成的算法。本文用的是中缀表达式转后缀表达式的算法。我们输入的表达式(如a+b-c)其实是中缀表达式,但是计算机很难处理这种表达式;相对而言,计算机容易处理的是后缀表达式(如ab+c-)。这里,我不会详细介绍这个算法,因为实现并不难,网上资料也相对较多。

  

  b 生成expression

   得到后缀表达式之后,需要做的工作就是生成一个最终的expression,当我们调用这个expression的intepreter方法后,它会层层递归,最终返回公式的计算结果。

  算法如下:

    1. 取出后缀表达式中下一个字符。

    2. 如果是变量,则生成一个VarExpression的实例,并将其压入堆栈;

      如果是单目运算符,则生成一个UnarySignExpression的实例,并从堆栈中弹出一个Expression作为其expression的域,再将其压入堆栈;

      如果是双目运算符,则生成一个BinarySignExpression的实例,并从堆栈中弹出两个Expression作为其left和right的域,再将其压入堆栈。

    3. 如果是最后一个字符,则从堆栈中弹出expression并返回;否则转向第一步。

 

 

3. 可扩展性的考虑

  一个好的程序不仅需要完成功能需求外,对于一些可预见的业务变化,能够在可扩展性上加以考虑也是必要的。比如说当前程序完成了加减乘除功能,而如果要再多加一个功能(例如阶乘符号),最好的做法是继承相应的抽象类并实现,并且可以通过配置文件的形式,让程序再次运行时意识到已经有新的功能了。

  我的做法还是主要思想还是反射+工厂。

  首先,我规定了一个xml文件用于配置,其格式如下:

<?xml version="1.0" encoding="GBK"?>
<operator>
    <binaryOperator name="+" priority="4">
        <class>com.fc.expression.binary.AddExpression</class>
    </binaryOperator>
    <binaryOperator name="-" priority="4">
        <class>com.fc.expression.binary.SubExpression</class>
    </binaryOperator>
    <binaryOperator name="*" priority="3">
        <class>com.fc.expression.binary.MulExpression</class>
    </binaryOperator>
    <binaryOperator name="/" priority="3">
        <class>com.fc.expression.binary.DivExpression</class>
    </binaryOperator>
    <unaryOperator name="!" priority="2">
        <class>com.fc.expression.unary.FacExpression</class>
    </unaryOperator>
</operator>

  在operaor节点下,是各个类型的运算符(单目,双目等):name属性表示元素运算符的写法,priority表示优先级,class节点表示类的文件。于是通过name属性我们可以直接根据class节点指向的类,通过反射得到一个实例。

 

  此外,通过建立一个SignFactory,可以提供一下功能:

    a. 提供一个各运算符的实例

    b. 提供一些辅助方法(某运算符是单目还是双目、比较两个运算符之间的优先级高低等)

 

  这样的一个好处是,上层的程序只关注这个bean工厂就可以了,完全无需关心epression复杂的继承关系。

 

以上只是一个大致思路,源码:https://github.com/heynoodles/Formular-Calculator

  

posted @ 2013-04-04 21:15  Alex_Monkey  阅读(942)  评论(0编辑  收藏  举报