想一下 算数中还允许将一元运算符用作求负运算符(减号) 将值从正数转换为负数 因此我们可以引入下列基本 AST
清单 计算器 AST(src/calc scala)
package tedneward calcdsl{private[calcdsl] abstract class Exprprivate[calcdsl]case class Number(value : Double) extends Exprprivate[calcdsl]case class UnaryOp(operator : String arg : Expr) extends Exprprivate[calcdsl]case class BinaryOp(operator : String left : Expr right : Expr)extends Expr}
注意包声明将所有这些内容放在一个包( tedneward calcdsl)中 以及每一个类前面的访问修饰符声明表明该包可以由该包中的其他成员或子包访问 之所以要注意这个是因为需要拥有一系列可以测试这个代码的 JUnit 测试 计算器的实际客户机并不一定非要看到 AST 因此 要将单元测试编写成 tedneward calcdsl 的一个子包
清单 计算器测试(testsrc/calctest scala)
package tedneward calcdsl test{class CalcTest{import junit _ Assert _@Test def ASTTest ={val n = Number( )assertEquals( n value)}@Test def equalityTest ={val binop = BinaryOp( + Number( ) Number( ))assertEquals(Number( ) binop left)assertEquals(Number( ) binop right)assertEquals( + binop operator)}}}
到目前为止还不错 我们已经有了 AST
再想一想 我们用了四行 Scala 代码构建了一个类型分层结构 表示一个具有任意深度的数学表达式集合(当然这些数学表达式很简单 但仍然很有用) 与 Scala 能够使对象编程更简单 更具表达力相比 这不算什么(不用担心 真正强大的功能还在后面)
接下来 我们需要一个求值函数 它将会获取 AST 并求出它的数字值 有了模式匹配的强大功能 编写这样的函数简直轻而易举
清单 计算器(src/calc scala)
package tedneward calcdsl{//object Calc{def evaluate(e : Expr) : Double ={e match {case Number(x) = xcase UnaryOp( x) = (evaluate(x))case BinaryOp( + x x ) = (evaluate(x ) + evaluate(x ))case BinaryOp( x x ) = (evaluate(x ) evaluate(x ))case BinaryOp( * x x ) = (evaluate(x ) * evaluate(x ))case BinaryOp( / x x ) = (evaluate(x ) / evaluate(x ))}}}}
注意 evaluate() 返回了一个 Double 它意味着模式匹配中的每一个 case 都必须被求值成一个 Double 值 这个并不难 数字仅仅返回它们的包含的值 但对于剩余的 case(有两种运算符) 我们还必须在执行必要运算(求负 加法 减法等)前计算运算数 正如常在函数性语言中所看到的 会使用到递归 所以我们只需要在执行整体运算前对每一个运算数调用 evaluate() 就可以了
大多数忠实于面向对象的编程人员会认为在各种运算符本身以外 执行运算的想法根本就是错误的 — 这个想法显然大大违背了封装和多态性的原则 坦白说 这个甚至不值得讨论 这很显然违背 了封装原则 至少在传统意义上是这样的
在这里我们需要考虑的一个更大的问题是 我们到底从哪里封装代码?要记住 AST 类在包外是不可见的 还有就是客户机(最终)只会传入它们想求值的表达式的一个字符串表示 只有单元测试在直接与 AST case 类合作
但这并不是说所有的封装都没有用了或过时了 事实上恰好相反 它试图说服我们在对象领域所熟悉的方法之外 还有很多其他的设计方法也很奏效 不要忘了 Scala 兼具对象和函数性 有时候 Expr 需要在自身及其子类上附加其他行为(例如 实现良好输出的 toString 方法) 在这种情况下可以很轻松地将这些方法添加到 Expr 函数性和面向对象的结合提供了另一种选择 无论是函数性编程人员还是对象编程人员 都不会忽略到另一半的设计方法 并且会考虑如何结合两者来达到一些有趣的效果
从设计的角度看 有些其他的选择是有问题的 例如 使用字符串来承载运算符就有可能出现小的输入错误 最终会导致结果不正确 在生产代码中 可能会使用(也许必须使用)枚举而非字符串 使用字符串的话就意味着我们可能潜在地 开放 了运算符 允许调用出更复杂的函数(诸如 abs sin cos tan 等)乃至用户定义的函数 这些函数是基于枚举的方法很难支持的
- mysql游标和存储过程是什么 mysql游标表名为变量
- mysql子查询和连接查询 mysql子查询插入
- 纯phpmysql
- mongodb存储图片和文件实践 mongodb存文件和表
- java查询数组中是否包含某一个值 javamongodb数组查询
- 数据库和redis数据不一致 h2数据库和redis
- mongodb 权威指南 mongodb权威指南和实战
- mongo 新建数据库 mongodb创建用户和数据库
- redis怎么和数据库交互 redis数据结合
- redis实战电子书 redisjava书籍
