责任链模式是面向对象编程中的一种经典设计模式,但它却在函数式编程中消失了,为什么?要回答这个问题,最简单的方法是看函数式编程如何解决同样的问题。模式也许会消失,但其要解决的问题不会消失。
以日志打印器为例,我们希望不同级别的日志由不同的打印器进行打印,且打印器的具体细节对调用方是不可见的。
面向对象编程的解决方案
enum LogLevel {
DEBUG, INFO, ERROR
}
abstract class Logger {
protected Logger nextLogger;
public void setNextLogger(Logger nextLogger) {
this.nextLogger = nextLogger;
}
public void logMessage(LogLevel level, String message) {
if (isDefinedAt(level)) {
writeMessage(message);
}
if (nextLogger != null) {
nextLogger.logMessage(level, message);
}
}
protected abstract boolean isDefinedAt(LogLevel level);
protected abstract void writeMessage(String message);
}
class DebugLogger extends Logger {
public boolean isDefinedAt(LogLevel level) {
return LogLevel.DEBUG == level;
}
public void writeMessage(String message) {
System.out.println("[DEBUG] - " + message);
}
}
class InfoLogger extends Logger {
public boolean isDefinedAt(LogLevel level) {
return LogLevel.INFO == level;
}
public void writeMessage(String message) {
System.out.println("[INFO] - " + message);
}
}
class ErrorLogger extends Logger {
public boolean isDefinedAt(LogLevel level) {
return LogLevel.ERROR == level;
}
public void writeMessage(String message) {
System.out.println("[ERROR] - " + message);
}
}
class ChainOfResponsibility {
public static void main(String[] args) {
Logger debugLogger = new DebugLogger();
Logger infoLogger = new InfoLogger();
Logger errorLogger = new ErrorLogger();
infoLogger.setNextLogger(errorLogger);
debugLogger.setNextLogger(infoLogger);
Logger logger = debugLogger;
logger.logMessage(LogLevel.DEBUG, "This is a debug message.");
logger.logMessage(LogLevel.INFO, "This is an information.");
logger.logMessage(LogLevel.ERROR, "This is a error message.");
}
}
输出结果为
[DEBUG] - This is a debug message.
[INFO] - This is an information.
[ERROR] - This is a error message.
函数式编程的解决方案
enum LogLevel:
case DEBUG, INFO, ERROR
case class Message(level: LogLevel, content: String)
object ChainOfResponsibility extends App {
val debugLogger: PartialFunction[Message, Unit] = {
case Message(LogLevel.DEBUG, content) =>
println(s"[DEBUG] - ${content}")
}
val infoLogger: PartialFunction[Message, Unit] = {
case Message(LogLevel.INFO, content) =>
println(s"[INFO] - ${content}")
}
val errorLogger: PartialFunction[Message, Unit] = {
case Message(LogLevel.ERROR, content) =>
println(s"[ERROR] - ${content}")
}
val logger = debugLogger orElse infoLogger orElse errorLogger
logger(Message(LogLevel.DEBUG, "This is a debug message"))
logger(Message(LogLevel.INFO, "This is an information"))
logger(Message(LogLevel.ERROR, "This is a error message"))
}
输出结果为
[DEBUG] - This is a debug message.
[INFO] - This is an information.
[ERROR] - This is a error message.
谜底
函数式编程使用了PartialFunction
来解决该问题,它只对部分输入有响应。其定义为
trait PartialFunction[-A, +B] extends (A => B) {
def isDefinedAt(x: A): Boolean
def apply(x: A): B
def applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1 =
if (isDefinedAt(x)) apply(x) else default(x)
}
而 { case _ => ???}
是创建PartialFunction
的一个语法糖。
我们看到isDefinedAt
和apply
可以与Logger
中的isDefinedAt
和writeMessage
一一对应。其不同点是PartialFunction
只能接收一个参数,所以我们使用Message
对LogLevel
和content
进行了封装。
Logger
使用setNextLogger
来组装下一个Logger
,而PartialFunction
使用orElse
来组装下一个PartialFunction
。
def orElse[A1 <: A, B1 >: B](that: PartialFunction[A1, B1]): PartialFunction[A1, B1] =
new OrElse[A1, B1] (this, that)
其中OrElse
是PartialFunction
的子类,它存储了两个PartialFunction
并定义了其调用顺序。
Logger
通过logMessage
来控制责任的传递,而OrElse
通过apply
来控制责任传递
override def apply(x: A): B = f1.applyOrElse(x, f2)
现在我们可以揭开谜底了,PartialFunction
是对责任链模式的进一步抽象,是函数式编程语言的基本特性,不再需要我们自己搭建模式框架。
但PartialFunction
对责任链模式添加了一些限制,例如
- 每个输入只能由一个
PartialFunction
来处理,且越靠前优先级越高,而在Logger
中我们可以自由控制是否将责任传递给下一个Logger
。不过我们也可以实现自己的OrElse
子类来达到相同的目的。这里我们更应该思考的是,如果一个输入有多个处理者,它适合使用责任链模式来解决么?是否应该合并处理逻辑或使用订阅者模式呢? -
PartialFunction
只能有一个入参,而我们自己搭建的模式框架可以支持多个入参。不过我们可以将所有参数封装到一起来达到相同的目的。 - 如果不能处理输入,
PartialFunction
会直接报错,而在Logger
中我们可以控制是报错还是直接返回。不过我们可以通过PartialFunction.lift
将PartialFunction
转换为一个类型为A => Option[B]
的Function
,这样返回None
就意味着输入无法处理,调用者可以自己决定是报错还是忽略。
总结
责任链模式在函数式编程中并没有消失,而是变成了一个原生特性PartialFunction
,我们不需要自己搭建模式框架,直接使用就好。文中的代码都已上传到design-pattern-in-fp,欢迎大家批评指正。