用偏函数实现责任链模式

责任链模式的目的就是避免请求的发送者和接收者之间的耦合关系,将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

假如你遇到这样的业务需求场景:希望使得多个对象都有机会处理某种类型的请求,那么可能就需要考虑是否可以采用责任链模式。

典型的例子就是Servlet中的Filter和FilterChain接口,它们就采用了责任链模式。利用责任链模式我们可以在接收到一个Web请求时,先进行各种filter逻辑的操作,filter都处理完之后才执行servlet。在这个例子中,不同的filter代表了不同的职责,最终它们形成了一个责任链。

简单来说,责任链模式的目的就是避免请求的发送者和接收者之间的耦合关系,将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止

现在举一个更具体的🌰。计算机学院的学生会管理了一个学生会基金,用于各种活动和组织人员工作的开支。当要发生一笔支出时,如果金额在100元之内,可由各个分部长审批;如果金额超过了100元,那么就需要会长同意;但假使金额较大,达到了500元以上,那么就需要学院的辅导员陈老师批准。此外,学院里还有一个不宣的规定,经费的上限为1000元,如果超出则默认打回申请。

当然我们可以用最简单的if-else来实现经费审批的需求。然而根据开闭原则,我们需要将其中的逻辑进行解耦。下面我们就用面向对象的思路结合责任链模式,来设计一个程序。

data class ApplyEvent(val monkey: Int, val title: String)

interface ApplyHandler {
val successor: ApplyHandler?
fun handleEvent(event: ApplyEvent)
}

class GroupLeader(override val successor: ApplyHandler?): ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.monkey <= 100 -> print("Group Leader handled application: ${event.title}")
else -> when(successor) {
is ApplyHandler -> successor.handleEvent(event)
else -> print("Group Leader: This application cannot be handdle")
}
}
}
}

class President(override val successor: ApplyHandler?) :ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.monkey <= 500 -> print("President handled application: ${event.title}")
else -> when(successor) {
is ApplyHandler -> successor.handleEvent(event)
else -> print("President: This application cannot be handdle.")
}
}
}
}

class College(override val successor: ApplyHandler?) :ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.monkey > 1000 -> print("College: This application is refused.")
else -> print("College handled application: ${event.title}.")
}
}
}

我们声明了GroupLeader,President,College三个类来代表学生会部长,分会长,会长及学院,它们都实现了ApplyHandler接口。接口包含了一个可空的后继者对象successor,以及对申请事件的处理方法handleEvent。

当我们把一个申请经费的事件传递给GroupLeader对象进行处理时,它会根据具体的经费金额来判断金额来判断是否将申请转交给successor对象,也就是President类来处理。以此类推,最终形成了一个责任链机制:

st=>start: 经费申请

op1=>operation: 学生部长
op2=>operation: 分会长
op3=>operation: 会长
op4=>operation: 学院
cond1=>condition: 是否小于100元
cond2=>condition: 是否小于500元
cond3=>condition: 是否小于1000元
e1=>end: 申请成功
e2=>end: 申请失败
st->cond1
cond1(yes)->op1->e1
cond1(no)->cond2
cond2(yes)->op2->e1
cond2(no)->cond3
cond3(yes)->op3->e1
cond3(no)->e2
fun main(args: Array<String>) {

val College = College(null)
val president = President(college)
val groupLeader = GroupLeader(president)

groupLeader.handleEvent(ApplyEvent(10,"buy a pen"))
groupLeader.handleEvent(ApplyEvent(200,"team building"))
groupLeader.handleEvent(ApplyEvent(600,"hold a debate match"))
groupLeader.handleEvent(ApplyEvent(1200,"annual meeting of the college"))
}

//result
Group Leader handled application: buy a pen.
President handled application: team building.
College handled applicatioon: hold a debate match.
College: This application is refused.

梳理一下责任链的机制,整个链条的每个处理环节都有对其输入参数的效验标准,当输入参数处于某个责任链环节的有效接收范围之内,该环节才能对其作出正常的处理操作。在编程语言中,我们有一个专门的术语来描述这种情况,这就是“偏函数”。

实现偏函数类型:PartialFunction

那,什么是偏函数呢?

偏函数是数学中的概念,指的是定义域X中可能存在某些值在值域Y中没有对应的值。

为了方便理解,我们可以把偏函数与普通函数进行比较。在一个普通函数中,我们可以给指定类型的参数传入任意该类型的值,比如(Int)->Unit,可以接收任何Int值。而在一个偏函数中,指定类型的参数并不接收任意该类型的值,比如:

fun mustGreaterThan5(x: Int): Boolean {

if (x > 5) {
return true
}
throw Exception("x must be greator than 5")
}

>>> mustGreatorThan5(6)
true

>>> mustGreatorThan5(1)
java.lang.Exception: x must be greatoor than 5 at Line17.mustGreatorThan5(Unknow Source)

之所以提高偏函数是因为在一些函数式编程语言中,如Scala,有一种PartialFunction类型,我们可以用它来简化责任链模式的实现。由于Kotlin的语言特性足够灵活强大,虽然它的标准库没有支持PartialFunction,然而一些开源库(如Arrow)已经实现了这个功能。我们来定义一个PartialFunction类型:

class PartialFunction<in P1, out R> (

private val definetAt: (P1) -> Boolean,
private val f: (P1) -> R
) : (P1) -> R {
override fun invoke(p1: P1): R {
if (definetAt(p1)) {
return f(p1)
}
throw IllegalArgumentException("Value: ($p1) isn't supported by this function")
}

fun isDefinetAt(p1: P1) = definetAt(p1)
}

现在来分析下PartialFunction类的具体作用:

  • 声明类对象时需要接收两个构造参数,definetAt为效验函数,f为处理函数
  • 当PartialFunction类对象执行invoke方法时,definetAt会对输出参数p1进行有效性效验
  • 如果效验结果通过,则执行f函数,同时将p1作为参数传递给它,反之抛出异常

如上PartialFunction类已经可以处理责任链模式中各个环节对于输入的效验及处理逻辑的问题,但是依旧有一个问题,就是如何将请求在整个链条中进行传递。

接下来我们利用Kotlin的扩展函数给PartialFunction类增加一个orElse方法。在此之前,我们先注意下这个类中的isDefinedAt方法,它其实并没有什么特殊之处,仅仅只是作为拷贝definetAt的一个内部方法,为了在orElse方法中能够被调用。

infix fun <P1, R> PartialFunction<P1, R>.orElse(that: PartialFunction<P1, R>): PartialFunction<P1, R> {

return PartialFunction({ this.isDefinedAt(it) || that.isDefinedAt(it) }) {
when {
this.isDefinedAt(it) -> this(it)
else -> that(it)
}
}
}

在orElse方法中可以传入另一个PartialFunction类对象that,它也就是责任链模式中的后继者。当isDefinedAt方法执行结果为false的时候,那么就调用that对象来处理申请。

这里用infix关键字来让orElse成为一个中辍函数,从而让链式调用变得更加直观。

用orElse构建责任链

接下来我们就用设计好的PartialFunction类及扩展的orElse方法,来重新实现以下最开始的例子。首先来看看如何用PartialFunction定义groupLeader对象:

data class ApplyEvent(val money: Int, val title: String)

val groupLeader = {
val definetAt: (ApplyEvent) -> Boolean = { it.money <= 200 }
val handler: (ApplyEvent) -> Unit = { println("Group Leader handled application: ${it.title}.") }
PartialFunction(definetAt, handler)
}()

这里我们借助了自运行Lambda的语法来构建一个PartialFunction的对象groupLeader。definetAt用于效验申请的经费金额是否在学生会部长可审批的范围之内,handler函数用来处理童年各国金额效验后的审批操作。

同理,我们用类似的方法再定义剩下的president和college对象:

val president = {

val definetAt: (ApplyEvent) -> Boolean = { it.money <= 500 }
val handler: (ApplyEvent) -> Unit = { println("President handled application: ${it.title}.") }
PartialFunction(definetAt, handler)
}()

val college = {
val definetAt: (ApplyEvent) -> Boolean = { true }
val handler: (ApplyEvent) -> Unit = {
when {
it.money > 1000 -> println("College: This application is refused.")
else -> println("College handled application: ${it.title}.")
}
}
PartialFunction(definetAt,handler)
}

最后我们用orElse来构建一个基于责任链模式和PartialFunction类型的中辍表达式applyChain:

val applyChain = groupLeader orElse president orElse college

>>> applyChain(ApplyEvent(600,"hold a debate match"))
College handled application: hold a debate match

借助PartialFunction类的封装,我们不仅大幅度减少了程序的代码量,而且在构建责任链时,可以用orElse获得更好的语法表达。

作者

8MilesRD

发布于

2020-01-19

更新于

2020-01-21

许可协议

评论