很难说 FP 和 OO 孰优孰劣, 应该依场景合理选择使用倘若从这个角度出发, Scala 就体现出好处了, 毕竟它同时支持了 OO 和 FP 两种设计范式
从设计角度看, 我认为 OO 更强调对象的自治, 即每个对象承担自己应该履行的职责倘若在编码实现时能遵循自治原则, 就不容易设计出贫血对象出来 FP 则更强调函数的分治, 即努力保证函数的纯粹性和原子性, 对一个大问题进行充分地分解, 分别治理, 然后再利用函数的组合性完成职责的履行, 即所谓通过增量组合建立抽象
需求
我最近正在编写的一个需求场景, 正好完美地展现了这两种不同范式的设计威力我要实现的是一个条件表达式树的验证和解析, 这棵树的节点分为两种类型:
- Condition Group
- Condition
Condition Group 作为根节点, 可以递归嵌套 Condition Group 和 Condition, 如下图所示:
对条件表达式树的验证主要是避免出现非法节点, 例如不支持的操作符, 不符合要求的条件值, 不合理的递归嵌套, 空节点等若验证不通过则需要提供错误信息, 并返回给前端 400 的 BadRequest 解析时, 必须保证节点是合规的, 解析后的结果为满足 SQL 语法中 where 条件子句的字符串
验证
针对表达式数的合规性验证, 我选择了 FP 的实现方式为何做出这样的选择? 试剖析整个验证行为, 可以分解为如下的验证逻辑:
对表达式树的验证
对当前 Condition 节点的验证
对操作符的验证
对条件值的验证
对当前 Condition Group 节点的验证
对逻辑操作符的验证
对子条件 Size 的验证
可以看到, 分解出来的处于同一层次的验证逻辑, 彼此之间是完全正交的, 获得的结果互相不受影响同时, 这些原子的验证逻辑又可以组合起来, 形成更高粒度的正交的验证, 例如对 Condition 和 Condition Group 的验证, 彼此独立, 组合起来却又可以形成对整个表达式树的验证
考虑函数的 side effect, 应尽量做到无副作用, 这更选择选择 FP 的方式, 且 Scala 自身提供了 Try[T]类型, 可以避免在函数中抛出具有副作用的异常 Try[T]是一个 Monad, 可以支持 for comprehension 对函数进行组合
由于验证逻辑彼此正交, 对函数的实现就变得非常纯粹而简单, 不用考虑太多外在的因素只要设计好函数的接口, 函数可以专心做自己的事情
对 Condition 当前节点的验证
对 Condition 的验证相对简单, 只需要分别针对操作符和条件值进行验证即可如下是代码实现:
- trait ConditionValidator {
- def validateCondition(condition: Condition): Try[Boolean] = {
- for {
- _ <- validateOperator(condition)
- result <- validateValues(condition)
- } yield result
- }
- def validateOperator(condition: Condition): Try[Boolean] = {
- List("between", "in", "<", ">", "=", "<=", ">=", "<>").contains(condition.operator.toLowerCase) match {
- case true => Success(true)
- case false => Failure(new Throwable(s"can't support operator ${condition.operator}"))
- }
- }
- def validateValues(condition: Condition): Try[Boolean] = {
- val error = new Throwable(s"invalid values for condition ${condition}")
- if (condition.values.isEmpty) return Failure(error)
- if (condition.operator.isBetween && condition.values.size != 2) return Failure(error)
- if (condition.operator.isCommon && condition.values.size != 1) return Failure(error)
- Success(true)
- }
- implicit class StringOperator(operator: String) {
- def isBetween: Boolean = operator.toLowerCase == "between"
- def isIn: Boolean = operator.toLowerCase == "in"
- def isCommon: Boolean = List("<", ">", "=", "<=", ">=", "<>").contains(operator.toLowerCase)
- }
- }
对 ConditionGroup 当前节点的验证
这里对 ConditionGroup 的验证仅仅针对当前节点, 不用去考虑 ConditionGroup 的嵌套, 那是对表达式树的验证, 属于另一个层次把这一职责的边界明确界定, 代码实现就变得非常的简单:
- trait ConditionGroupValidator {
- def validateConditionGroup(group: ConditionGroup): Try[Boolean] = {
- for {
- _ <- validateLogicOperator(group)
- result <- validateConditionSize(group)
- } yield result
- }
- def validateConditionSize(group: ConditionGroup): Try[Boolean] = {
- val error = new Throwable(s"invalid condition group for ${group}")
- group.logicOperator.toLowerCase match {
- case "not" => if (group.conditions.size == 1) Success(true) else Failure(error)
- case _ => if (group.conditions.size >= 2) Success(true) else Failure(error)
- }
- }
- def validateLogicOperator(group: ConditionGroup): Try[Boolean] = {
- List("and", "or", "not").contains(group.logicOperator.toLowerCase()) match {
- case true => Success(true)
- case false => Failure(new Throwable(s"invalid logic operator ${group.logicOperator} for ConditionGroup"))
- }
- }
- }
对表达式树的验证
对表达式树的验证相对复杂, 因为牵涉到递归, 尤其是从性能考虑, 需要使用尾递归 (tail recursion) 关于尾递归的知识, 在我之前的博客艾舍尔的画手与尾递归中已有详细介绍, 这里不再赘述阅读下面的代码实现时, 注意尾递归方法 recurseValidate()的第二个参数, 其实就是关键的 accumulator
- trait CriteriaValidator extends ConditionValidator with ConditionGroupValidator {
- def validate(group: ConditionGroup): Try[Boolean] = {
- @tailrec
- def recurseValidate(expr: List[ConditionExpression], result: Try[Boolean]): Try[Boolean] = {
- val ex = new Throwable(s"invalid condition group ${group}")
- expr match {
- case Nil => Failure(ex)
- case head::Nil => result.flatMap(_ => validateExpression(head))
- case head::tail => recurseValidate(tail, validateExpression(head))
- }
- }
- validateConditionGroup(group).flatMap(_ => recurseValidate(group.conditions, Success(true)))
- }
- def validateExpression(expr: ConditionExpression): Try[Boolean] = expr match {
- case expr: ConditionGroup => validateConditionGroup(expr)
- case expr: Condition => validateCondition(expr)
- }
- }
注意, 在函数 validate()中, 实际上是验证 ConditionGroup 当前节点的函数
validateConditionGroup()
与尾递归方法 recurseValidate()的组合至于
validateExpression()
函数的引入, 不过是为了避免不必要的类型判断和强制类型转换罢了
解析
我最初也曾尝试依旧采用 FP 方式实现解析功能思索良久, 发现要实现起来困难重重最主要的障碍在于: 每个解析行为返回的结果都会影响到别的节点, 进而影响整个表达式例如, 为了保证解析后 where 子句的语法合规, 需要考虑为每个节点解析的结果添加小括号当对整个表达式树进行递归解析时, 每次返回的结果无法直接作为 accumulator 的值如果在当前递归层添加了小括号, 由于该层次下的子节点还未得到解析, 就会导致小括号范围有误; 如果不添加小括号, 就无法界定各个层次逻辑子句的优先级, 导致筛选结果不符合预期换言之, 其中的关键在于每个解析操作并非正交的, 因此无法对函数进行分治的拆解
倘若站在 OO 的角度去思考, 则对条件表达式的解析, 实际就是对各个节点的解析由于解析行为需要的数据是各个节点对象已经具备的, 遵循信息专家模式, 就应该让节点对象自己来履行职责, 这就是所谓的对象的自治而从抽象层面进行分析, 虽然各个节点拥有的数据不同, 解析行为的实现也不尽相同, 却都是在完成对自身的解析于是, 我们通过
ConditionExpression
完成对不同节点类型的抽象此时, Condition Group 是表达式树的枝节点, 而 Condition 则是表达式树的叶子节点如下图所示, 不恰好是 Composite 模式的体现么?
我们首先需要定义
ConditionExpression
抽象这里之所以定义为抽象类, 而非 trait, 是为了支持 Json 解析的多态, 与本文无关, 这里不再解释若希望了解, 请阅读我的另一篇博客在 Scala 项目中使用 Spring Cloud:
- abstract class ConditionExpression {
- def evaluate: String
- }
作为枝节点的 ConditionGroup, 不仅要解析自身, 还要负责解析嵌套的子节点但是, 父节点不用考虑解析子节点内部的实现, 它仅仅是在合适的地方发起对子节点的调用就可以了这才是真正的自治, 也就是每个对象在理智上都保持对权力的克制, 仅负责履行属于自己的职责, 绝不越权
- case class ConditionGroup(logicOperator:
- String, conditions: List[ConditionExpression]) extends ConditionExpression {
- def evaluate: String = {
- logicOperator.toLowerCase match {
- case "not" = >s "(NOT ${conditions.head.evaluate})"
- case _ = >{
- val expr = conditions.map(_.evaluate).reduce((l, r) = >s "${l} ${logicOperator.toUpperCase} ${r}") s "($expr)"
- }
- }
- }
- }
- case class Condition(fieldName:
- String, operator: String, values: List[String], dataType: String) extends ConditionExpression {
- def evaluate: String = {
- def handleValue(value: String, dataType: String) : String = {
- dataType.toLowerCase match {
- case "text" = >s "'${value}'"
- case "number" = >value
- case _ = >value
- }
- }
- val correctValues = values.map(v = >handleValue(v, dataType)) val expr = operator.toLowerCase() match {
- case "between" = >s "BETWEEN ${correctValues.head} AND ${correctValues.last}"
- case "in" = >{
- val range = correctValues.map(x = >s "$x").mkString(",") s "IN (${range})"
- }
- case _ = >s "${operator.toUpperCase} ${correctValues.head}"
- }
- s "(${fieldName} ${expr})"
- }
- }
组合验证与解析
若采用自顶向下的设计方法来看待整个功能, 则表达式树的验证与解析属于两个不同的职责, 遵循单一职责原则, 我们应该将其分离在进行验证时, 无需考虑解析的逻辑; 在开始解析表达式树时, 也无需负担验证合法性的包袱分则简易, 合则纠缠不清只有进行了合理地分治后然后再组合, 景色就大不相同了:
- trait CriteriaParser extends CriteriaValidator {
- def parse(group: ConditionGroup) : Try[String] = {
- validate(group).map(_ = >group.evaluate)
- }
- }
结论
就我个人而言, 我认为 OO 与 FP 并不是势如水火的天敌, 也无需发出既生瑜何生亮的慨叹, 非得比出胜负本文的例子当然仅仅是冰山一角地体现了 OO 与 FP 各自的优势善于面向对象思维的, 不能抱残守缺, 闭关自守函数式思维的大潮挡不住, 也不必视其为洪水猛兽, 反而应该主动去拥抱精通函数式编程的, 也不必过于炫技, 夸大函数式思维的重要性, 就好似要一统江湖似的
无论面向对象还是函数思维, 用对了才是对的谁也不是江湖永恒的霸主, 青山依旧在, 几度夕阳红!
来源: https://juejin.im/entry/5a7face16fb9a0636263cf67