Scala 的面向对象与函数编程

2023-11-09 11:12:19 字數 4589 閱讀 1976

很难说 fp 和 oo 孰优孰劣,应该依场景合理选择使用。倘若从这个角度出发,scala 就体现出好处了,毕竟它同时支持了 oo 和 fp 两种设计范式。

从设计角度看,我认为 oo 更强调对象的自治,即每个对象承担自己应该履行的职责。倘若在编码实现时能遵循“自治”原则,就不容易设计出贫血对象出来。fp 则更强调函数的分治,即努力保证函数的纯粹性和原子性,对一个大问题进行充分地分解,分别治理,然后再利用函数的组合性完成职责的履行,即所谓“通过增量组合建立抽象”。

我最近正在编写的一个需求场景,正好完美地展现了这两种不同范式的设计威力。我要实现的是一个条件表达式树的验证和解析,这棵树的节点分为两种类型:

condition groupconditioncondition group 作为根节点,可以递归嵌套 condition group 和 condition,如下图所示:

对条件表达式树的验证主要是避免出现非法节点,例如不支持的操作符,不符合要求的条件值,不合理的递归嵌套,空节点等。若验证不通过则需要提供错误信息,并返回给前端 400 的 badrequest。解析时,必须保证节点是合规的,解析后的结果为满足 sql 语法中 where 条件子句的字符串。

针对表达式数的合规性验证,我选择了 fp 的实现方式。为何做出这样的选择?试剖析整个验证行为,可以分解为如下的验证逻辑:

对表达式树的验证对当前 condition 节点的验证对当前 condition group 节点的验证可以看到,分解出来的处于同一层次的验证逻辑,彼此之间是完全正交的,获得的结果互相不受影响。同时,这些“原子”的验证逻辑又可以组合起来,形成更高粒度的正交的验证,例如对 condition 和 condition group 的验证,彼此独立,组合起来却又可以形成对整个表达式树的验证。

考虑函数的 side effect,应尽量做到无***这更选择选择 fp 的方式,且 scala 自身提供了try[t]类型,可以避免在函数中抛出具有***的异常。try[t]是一个 monad,可以支持 for comprehension 对函数进行组合。

由于验证逻辑彼此正交,对函数的实现就变得非常纯粹而简单,不用考虑太多外在的因素。只要设计好函数的接口,函数可以专心做自己的事情。

对 condition 当前节点的验证对 condition 的验证相对简单,只需要分别针对操作符和条件值进行验证即可。如下是**实现:

trait conditionvalidator yield result

def validateoperator(condition: condition): try[boolean] =

def validatevalues(condition: condition): try[boolean] =

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)

对 conditiongroup 当前节点的验证这里对 conditiongroup 的验证仅仅针对当前节点,不用去考虑 conditiongroup 的嵌套,那是对表达式树的验证,属于另一个层次。把这一职责的边界明确界定,**实现就变得非常的简单:

trait conditiongroupvalidator yield result

def validateconditionsize(group: conditiongroup): try[boolean] =

group.logicoperator.tolowercase match

def validatelogicoperator(group: conditiongroup): try[boolean] =for conditiongroup"))

对表达式树的验证对表达式树的验证相对复杂,因为牵涉到递归,尤其是从性能考虑,需要使用尾递归(tail recursion)。关于尾递归的知识,在我之前的博客《艾舍尔的画手与尾递归》中已有详细介绍,这里不再赘述。阅读下面的**实现时,注意尾递归方法recursevalidate()的第二个参数,其实就是关键的 accumulator。
trait criteri**alidator extends conditionvalidator with conditiongroupvalidator ")

expr match

validateconditiongroup(group).flatmap(_ recursevalidate(group.conditions, success(true)))

def validateexpression(expr: conditionexpression): try[boolean] =expr match

注意,在函数validate()中,实际上是验证 conditiongroup 当前节点的函数validateconditiongroup()与尾递归方法recursevalidate()的组合。至于validateexpression()函数的引入,不过是为了避免不必要的类型判断和强制类型转换罢了。

我最初也曾尝试依旧采用 fp 方式实现解析功能。思索良久,发现要实现起来困难重重。最主要的障碍在于:每个解析行为返回的结果都会影响到别的节点,进而影响整个表达式。例如,为了保证解析后 where 子句的语法合规,需要考虑为每个节点解析的结果添加小括号。

当对整个表达式树进行递归解析时,每次返回的结果无法直接作为 accumulator 的值。如果在当前递归层添加了小括号,由于该层次下的子节点还未得到解析,就会导致小括号范围有误;如果不添加小括号,就无法界定各个层次逻辑子句的优先级,导致筛选结果不符合预期。换言之,其中的关键在于每个解析操作并非正交的,因此无法对函数进行“分治”的拆解。

倘若站在 oo 的角度去思考,则对条件表达式的解析,实际就是对各个节点的解析。由于解析行为需要的数据是各个节点对象已经具备的,遵循信息专家模式,就应该让节点对象自己来履行职责,这就是所谓的“对象的自治”。而从抽象层面进行分析,虽然各个节点拥有的数据不同,解析行为的实现也不尽相同,却都是在完成对自身的解析。于是,我们通过conditionexpression完成对不同节点类型的抽象。此时,condition group 是表达式树的枝节点,而 condition 则是表达式树的叶子节点。如下图所示,不恰好是 composite 模式的体现么?

我们首先需要定义conditionexpression抽象。这里之所以定义为抽象类,而非 trait,是为了支持 json 解析的多态,与本文无关,这里不再解释。

abstract class conditionexpression )"

case _

s"($expr)"

case class condition(fieldname: string, operator: string, values: list[string], datatype: string) extends conditionexpression '"

case "number" =value

case _ value

val correctvalues = values.map(v =>handlevalue(v, datatype))

val expr = operator.tolowercase() match and $"

case "in" =

case _ s"$

s"($

若采用自顶向下的设计方法来看待整个功能,则表达式树的验证与解析属于两个不同的职责,遵循“单一职责原则”,我们应该将其分离。在进行验证时,无需考虑解析的逻辑;在开始解析表达式树时,也无需负担验证合法性的包袱。分则简易,合则纠缠不清。只有进行了合理地“分治”后然后再组合,景色就大不相同了:

trait criteriaparser extends criteri**alidator 

就我个人而言,我认为 oo 与 fp 并不是势如水火的天敌,也无需发出“既生瑜何生亮”的慨叹,非得比出胜负。本文的例子当然仅仅是冰山一角地体现了 oo 与 fp 各自的优势。善于面向对象思维的,不能抱残守缺,闭关自守。函数式思维的大潮挡不住,也不必视其为洪水猛兽,反而应该主动去拥抱。精通函数式编程的,也不必过于炫技,夸大函数式思维的重要性,就好似要“一统江湖”似的。

无论面向对象还是函数思维,用对了才是对的。

面向对象的魅力 为何选择C ?

c 和c语言是两种广泛应用于软件开发领域的编程语言。虽然它们有着共同的起源,但在语法 特性和应用方面存在着显著的区别。如果你正在学习编程或者考虑选择一种适合自己的语言,了解它们之间的不同将对你做出明智的选择非常有帮助。首先,让我们从语言的起源说起。c语言是在世纪年代初由贝尔实验室的dennis ri...

韩信创新“对面笑”规则 象棋的嬗变与楚汉之战的传奇

象棋,这个古老而精妙的中国文化遗产,承载着千年智慧和历史的痕迹。相传,象棋的起源可以追溯到尧舜时期,一个叫象的人,因热爱军事而创造了这一令人着迷的策略游戏。如今,象棋已经发展成为一门极具竞技性和思维深度的竞技运动,但它的规则和玩法并非一成不变。最初的象棋规则相对简单,棋子有限,但随着时间的推移,象棋...

浪漫时尚 陈紫函与向宇的甜蜜大片!

真爱无惧,陈紫函与戴向宇的爱情故事如同一部浪漫冒险 他们选择诚实勇敢地面对自己的心,无视外界的非议,用心经营着自己的完美爱情。最近,他们登上了 时尚新娘cosmobride 杂志,展现了他们甜蜜的爱意。在大片中,他们佩戴了niessing霓星珠宝的作品,展现出了他们的简单而真实的爱情。陈紫函佩戴了n...