scala与函数式编程——面向对象模式在函数式编程下的实现

用函数组合实现设计模式

  设计模式是面向对象下的产物,但其中蕴藏的编程理念仍然是通用的。对于面向对象的编程熟手而言,在编程时几乎离不开常用的设计模式。在刚开始使用函数式编程的时候,还会不自觉地想使用策略、装饰器等模式,但却不知在函数式编程的世界里,有些模式早已被函数的组合替代了。
  设计模式中的核心思想就是遵循封装原则将一段可变的行为提取出来成为另一个对象,并基于多态特性、使用组合优于继承的原则将不同的实现插入。这样的例子有策略模式、状态模式、装饰器模式、命令模式等。而函数式编程的关键在于函数的组合,而策略、命令等接口大多情况下都是只有一个方法的函数式接口,是可以直接用一个函数对象来替代的。

实现策略模式

  策略模式是将一段算法或逻辑提取到一为接口,使用时动态拼装不同的接口实现。而所谓的策略接口其实就是一个函数式接口:只定义了一个方法的接口。函数式接口可以直接用一个具体的函数实例来替代。如下面的java代码基于策略模式实现了一个可以去除不同类型数字的收集器:

class IntCollector {
    public List<Integer> filter(List<Integer> list, IntFilter filter) {
        list.stream().filter(i -> filter.shouldFilter(i)).collect(...);
    }
}
interface IntFilter {
    boolean shouldFilter(Integer i);
}
class EvenFilter implements IntFilter {
    public boolean shouldFilter(Integer i){
        return i % 2 != 0;
    }
}
class ModByNFilter implements IntFilter {
    private int i;
    ModByNFilter(int i) { 
        this.i = i;
    }
    public boolean shouldFilter(Integer i){
        return i % n == 0;
    }
}
IntCollector c = new IntCollector();
c.filter(Arrays.asList(1,2,3,4,5,6), new EvenFilter()) //return 1,3,5
c.filter(Arrays.asList(1,2,3,4,5,6), new ModByNFilter(3)) //return 1,2,4,5

  而通过函数组合,则可以用更少的代码简洁地实现“提取算法并替换”的这个目标。首先把collect这个方法中的IntFilter接口类型替换为函数类型:

object IntCollector {
    def filter(list: List[Int], f:Int=>Boolean): List[Int] = 
        list.filter(i => f(i)) //也可以简写为list.filter(f)
}

  然后,再将策略的实例,即EvenFilter的实现,直接使用Lambda表达式产生一个匿名函数,并传入collect方法中:

//EvenFilter的实现:i => i%2 == 0的类型是函数Int=>Boolean
IntCollector.filter(List(1,2,3,4,5,6), i => i%2 == 0) //return 1,3,5

  对于像ModByNFilter这样,构建时需要额外入参作为算法运行状态的策略,可以通过一个高阶函数接收入参,并返回一个函数作为ModByNFilter的具体实现:

//用高阶函数替代ModByNFilter的构造函数,产生一个函数Int=>Boolean
def modByNFilter(n: Int): Int=>Boolean = i => i % n == 0
IntCollector.filter(List(1,2,3,4,5,6), modByNFilter(3)) //return 1,2,4,5

  可见,用函数组合的方法实现策略模式可以在实现完整功能的前提下减少大量代码,主要包括函数式接口的申明(interface IntFilter),以及简化了含状态的策略构造过程,并且可以通过匿名函数来进一步减少代码量。

实现命令模式

  命令模式的主要表现形式是将一段待执行的行为构造出来但暂不执行,将其传递给调用者在需要的时候调用,在必要的情况下调用者还能缓存这些命令对象,以便重放甚至撤消。如下面一段java代码实现了一个收银的例子,由Client创建购买/退货的命令Purchase/Cancel,并传递给PurchaseInvoker,执行后每个Purchase会依次修改Client中Cash的状态:

class Client {
    private CommandInvoker invoker;
    private Cash cash;
    ...
    public void purchase(int amount) {
        invoker.add(new Purchase(amount, cash));
    }
    public void cancel(int amount) {
        invoker.add(new Cancel(amount, cash));
    }
    public void refresh() {
        invoker.invokeAll();
    }
}
interface Command {
    void execute();
}
class Purchase implements Command {
    private Cash cash;
    private int amount;
    public void execute() {cash.minus(amount);}
}
class Cancel implements Command {
    private Cash cash;
    private int amount;
    public void execute() {cash.plus(amount);}
}
class CommandInvoker {
    private List<Command> commands;
    ...
    public void invokeAll() {
        for (Command c: commands)
            c.execute();
        commands.clear();
    }
}
Client c = create a client with cash 100
c.purchase(30);
c.cancel(10);
c.purchase(20); //cash = 100
c.refresh(); //cash = 60

  有了上面策略模式的经验,很容易产生将Command作为函数的直觉。再结合之前ModByN的例子,就可以通过高阶函数将Purchase和Cancel这两个带有状态的命令对象构造出来。整体的感觉和策略模式非常接近:

class Client {
    ...
    //Unit相当于java中的void,表示忽略返回值,返回Unit常常表示含有副作用
    def purchase(amount:Int):Unit = invoker.add(makePruchase(amount))
    def cancel(amount:Int):Unit = invoker.add(makeCancel(amount))

    //通过高阶函数返回带有副作用的函数:()=>Unit
    def makePruchase(amount: Int):()=>Unit = () => cash.minus(amount)
    def makeCancel(amount: Int):()=>Unit = () => cash.plus(amount)
}
class CommandInvoker {
    //Command已经改为()=>Unit,因此commands也要改为List[()=>Unit],是一个函数的列表
    var commands: List[() => Unit]
    //refreshAll含有副作用
    def refreshAll():Unit = commands.foreach(c => c()) //执行每个c,c是一个函数
}

  命令模式和策略一样,也从一个函数式接口改为一个普通函数,而原先用到Command类型的地方都替换为函数类型。唯一不同的是,IntFilter接口的函数类型是Int=>Boolean,而命令接口则变为了()=>Unit。使用函数组合的方法也节约了大量代码,从原来一页纸变为短短几行。编程效率的提升十分明显。

实现装饰器模式

  装饰器模式的作用是提供多种附加功能并按需组合,从而应对多变的使用场景。具体的操作对象以及装饰对象都共享同一个接口,装饰对象会调用被装饰的对象,并在处理过程中增加自身的额外逻辑。如下面一段java代码展示了一个奶茶制作流程:

class TeaDrink {
    private List<String> liquids; //可供选择:Water,Milk,GreenTea, RedTea
    private List<String> additives; //可供选择:珍珠,波霸,椰果,仙草
    private int sugar; //从0-10表示甜度
}
interface TeaDrinkMaker {
    TeaDrink make();
}
class BasicMaker implements TeaDrinkMaker {
    public TeaDrink make() {
        TeaDrink drink = new TeaDrink();
        drink.liquids.add("water");
        drink.sugar = 8;
    }
}
class LiquidAdder implements TeaDrinkMaker {
    private TeaDrinkMaker maker;
    private String liquid;

    LiquidAdder(TeaDrinkMaker maker, String liquid){...}

    public TeaDrink make() {
        TeaDrink drink = maker.make();
        drink.liquids.add(liquid);
        drink.sugar += 2;
    }
}
class AdditiveAdder implements TeaDrinkMaker {
    private TeaDrinkMaker maker;
    private String additive;

    LiquidAdder(TeaDrinkMaker maker, List<String> additives){...}

    public TeaDrink make() {
        TeaDrink drink = maker.make();
        drink.additives.addAll(additives);
    }
}
class NoneSugarMaker implements TeaDrinkMaker {
    private TeaDrinkMaker maker;

    LiquidAdder(TeaDrinkMaker maker){...}

    public TeaDrink make() {
        TeaDrink drink = maker.make();
        drink.sugar = 0;
    }
}
class HalfSugarMaker implements TeaDrinkMaker {
    private TeaDrinkMaker maker;

    LiquidAdder(TeaDrinkMaker maker){...}

    public TeaDrink make() {
        TeaDrink drink = maker.make();
        drink.sugar /= 2;
    }
}
//含Water,RedTea;波霸,仙草;糖度=(8+2)/2
TeaDrink drink = new HalfSugarMaker(new AdditiveAdder(new LiquidAdder(new BasicMaker(), "RedTea"), Arrays.asList("波霸","仙草"))).make();
//含Water,GreenTea;none;糖度=0+2
TeaDrink drink = new LiquidAdder(new NoneSugarMaker(new BasicMaker()), "GreenTea")).make();

  如果用函数组合来实现奶茶的制作过程,则首先也需要将TeaDrinkMaker这个函数式接口转为函数类型,并将各类装饰类的构造函数替换为函数生成器(指产生函数的高阶函数):

case class TeaDrink(liquids:List[String], additives:List[String], sugar: Int)
//返回一个产生TeaDrink的函数
def basicMaker:()=>TeaDrink = () => TeaDrink(List("Water"), List.empty, 8)
//接受一个函数,返回一个同样类型的函数
def liquidAdder(maker:()=>TeaDrink, liquid:String):()=>TeaDrink = () => {
    val drink = maker()
    drink.copy(liquids=drink.liquids::liquid, sugar = drink.sugar+2)
}
def additiveAdder(maker:()=>TeaDrink, addtives:List[String]) = () => {
    val drink = maker()
    drink.copy(additives=addtives++drink.liquids)
}
def noneSugar(maker:()=>TeaDrink) =  () => {
    val drink = maker()
    drink.copy(sugar=0)
}
def halfSugar(maker:()=>TeaDrink) =  () => {
    val drink = maker()
    drink.copy(sugar=drink.sugar/2)
}
//先获取产生TeaDrink的函数,再()调用获取结果
val drink = halfSugar(additiveAdder(liquidAdder(basicMaker, "RedTea"),List("波霸","仙草"))()
val drink = nonSugar(liquidAdder(basicMaker, "GreenTea"))()

  在实现装饰器的过程中,所用到的技巧是将接口改为函数,对非装饰功能直接返回此类型的函数,对装饰功能的构造则接受一个函数作为参数,返回一个新的函数作为输出。

用函数组合实现依赖注入

  在任何模式的编程过程中,总是会用到依赖注入的原则将模块之间解耦,从而便于模块之间的组合复用与单独测试。在面向对象的语言中,依赖注入体现为通过构造函数或set方法将依赖模块设置到调用模块中,并通过框架来简化简化模块的组装。如下面的@Autowire会由框架来代为处理,自动将各个组件注入到所需要的模块中:

class TeaDomainService {
    @Autowire private TeaRepository repo;
    public Tea businessOp1(String param) {
        //use repository to complete operation
    }
}
class OtherDomainService {
    @Autowire private TeaRepository repo;
    public Tea businessOp2(Tea tea) {
        //use repository & tea to complete operation
    }
}
class TeaAppService {
    @Autowire private TeaDomainService service1;
    @Autowire private OtherDomainService service2;
    public String perform(String param) {
        Tea t = service1.businessOp1(param);
        String s = service2.businessOp2(t);
        return s;
    }
}

  也可以手工组装:

//In the Context:
TeaRepository repo = new HibernateTeaRepository();
TeaDomainService service1 = new TeaDomainService();
service1.setRepository(repo);
OtherDomainService service2 = new OtherDomainService();
service2.setRepository(repo);
TeaAppService appService = new TeaAppService();
appService.setService1(service1);
appService.setService2(service2);
//Out of the Context
TeaAppService appService = Context.getBean(TeaAppService.class);
String result = appService.perform("param");

  然而在函数式编程中,我们可以通过语言本身的特性,不依赖任何外部的框架,只通过函数组合来做到依赖注入。
  首先,让我们回归函数是一等公民的角度,摆脱在类中定义函数、并且让函数使用类的状态这种思维方式。函数是可以独立存在的!

trait TeaDomainService {
    def businessOp1(String param):Tea = {
        //如何拿到repository?
    }
}

  那么如何获取依赖的repository呢?答案是,返回一个函数。这个函数的参数是依赖的repository,把相应的repository传给这个函数,就能得到相应的结果。

trait TeaDomainService {
    def businessOp1(String param):TeaRepository=>Tea = repository => {
        repository.xxx //与原来的代码一致
    }
}

val repository = new TeaRepository
//businessOp1("param")得到一个函数:TeaRepository=>Tea,而不是一个具体的Tea
val tea = businessOp1("param")(repository)

  至此,我们能处理1个模块需要注入的情况了,那么如何处理TeaAppService中需要2个模块的情况呢?一个简单的想法就是把多个模块打包到一个对象中去:

trait Context {
    val repository: TeaRepository
    val service1: TeaDomainService 
    val service2: OtherDomainService 
}

trait OtherDomainService {
    def businessOp2(Tea tea):Context=>Tea = context => {
        context.repository.xxx //将原来的repository改为context.repository
    }
}

trait TeaAppService {
    def perform(String param):Context=>String = context => {
        Tea t = context.service1.businessOp1(param)(context) //businessOp1(param)返回一个函数
        String s = context.service2.businessOp2(t)(context) //businessOp2(tea)也返回一个函数
        s
    }
}

//与Spring一样,需要一个Context的实例
object AppContext extends Context {
    val repository: TeaRepository = new HibernateTeaRepository
    val service1: TeaDomainService = new xxx
    val service2: OtherDomainService = new xxx
}
val s = perform("param")(AppContext)

  至此,已经相对完整地处理了多个模块的注入问题,程序已经可以正常运行。但TeaAppService.perform里的代码显得很啰嗦,不如DomainService中的简洁。这是因为perform方法本身需要注入,所使用的service对象上的方法也需要注入,因此不得不反复将Context应用到service返回的函数上来获取结果。
  为了处理这一情况,可以使用高级的Reader[Context,X]类型来替代我们现在使用的context=>X函数类型。Reader类型是范畴论中的一种单子类型(Monad),什么是Monad会在后续文章中介绍,目前只要知道它支持一个操作叫flatMap,签名类似这样:

trait Reader[Context,A] {
    def flatMap[B](f: A=>Reader[Context,B]):Reader[Context,B]
    def map[B](f:A=>B):Reader[Context,B]
    def apply(context:Context):A
}

  如果用Reader类型来实现依赖注入,那么在遇到嵌套注入的时候代码就会好看很多:

trait OtherDomainService {
    def businessOp2(tea:Tea):Reader[Context,Tea] = Reader { context => 
        context.repository.xxx //与原来的代码一致,只是要返回Reader对象
    }
}

trait TeaAppService {
    def perform(String param):Reader[Context,String] = {
        for { //下面的语法和原来的基本一致
            t <- context.service1.businessOp1(param)
            s <- context.service2.businessOp2(t)
        } yield s
    }
}

object AppContext extends Context {
    ...
}
val s = perform("param").apply(AppContext) //perform("param")返回一个Reader,再调用apply获得结果

  上面的for{}是什么意思?其实这是scala的语法糖,scala会将for和yield语句替换为flatMap和map操作。如果我们还原for语法糖就能看清Reader其中的原理:

trait TeaAppService {
    def perform(String param):Reader[Context,String] = {
        val tReader:Reader[Context,Tea] = context.service1.businessOp1(param)
        val sReader:Reader[Context,String] = tReader.flatMap(
            //businessOp2(t)返回一个Reader[Context,String],正好满足flatMap的要求
            t => context.service2.businessOp2(t)) 
        sReader
    }
}

  先是通过businessOp1获得了一个Reader[Context,Tea],然后Reader可以支持flatMap这个高阶函数操作。我们可以将t => businessOp2(t)传递给它,是因为businessOp2返回Reader[Context,String],因此t => businessOp2(t)的类型是Tea=>Reader[Context,String],满足flatMap的要求,因而tReader.flatMap返回的结果就是Reader[Context,String]

总结

  个人认为传统面向对象中的一些原则其实是放之四海而皆准的,比如单一职责、开闭原则以及优先使用组合原则(即组合优于继承,但在函数式编程里没有继承)等。在函数式编程中也一样要以这些原则为指导,否则代码依然会陷入混乱。但两者的实现方式有所不同,主要区别在于将面向对象中原本的函数式接口直接替换为函数类型本身,将函数式接口对象的构造方法或工厂方法替换为输出新函数的高阶函数,因而大大减少了代码量,不仅增加了可读性,也提高了编码效率。
  同样,依赖注入也是所有编程语言都要遵循的原则。在面向对象中主要通过对象的set方法和框架来组装对象,而在函数式编程中可以不依赖框架,通过返回一个以依赖组件为形参的函数来实现。获得了这个函数之后再将依赖的组件以实参传入即能得到相应的结果。最后,还可以使用Reader等Monad类型中提供的复杂函数组合方法来简化在嵌套注入情况下的代码。

如何进一步学习?

  若想进一步了解设计模式是如何在函数式编程下实现的,可以参考Scala与Clojure函数式编程模式一书。
  若想进一步了解scala的for语法,可以参考Scala官网文档
  若想进一步了解Monad相关知识,请期待后续文章
  

猜你喜欢

转载自blog.csdn.net/samsai100/article/details/71749775
今日推荐