当前位置: 首页 > 科技观察

Scala基于Trait的设计模式

时间:2023-03-12 14:08:59 科技观察

在《作为Scala语法糖的设计模式》中,我重点介绍了那些已被纳入Scala语法的设计模式。今天要介绍的两种设计模式,主要是和Scala的特性有关。DecoratorPattern在GoF的23种设计模式中,DecoratorPattern是一种比较特殊的模式。它充分利用了继承和组合(或委托)各自的优点,将它们混合起来,既扩大了优点,又抵消了各自的缺点。Decorator模式的核心思想其实就是“职责分离”,就是把要装饰的职责和被装饰的职责分开,让它们在各自的继承体系下独立进化,然后完成被装饰的职责通过传递对象(组合)重用。从某种角度看,装饰职责和装饰职责的分离和抽象可以看作是Bridge模型的一种变体。但不同的是Decorator模式额外引入了继承,但不是为了复用,而是为了多态,让装饰器因为继承自decorated而具有被装饰的能力。所以继承的引入真的是点睛之笔。要理解装饰器模式,就必须理解继承和组合各自的作用。简而言之就是:继承:装饰器的多态组合:装饰器的复用正因如此,在Java代码中实现装饰器模式时,要注意装饰器类在重写装饰器的业务行为时,必须装饰对象将在传入的行为上被调用。假设传入的装饰对象是decoratee,调用时肯定是decoratee,而不是super(由于继承的关系,装饰类可以访问super)。例如,BufferedOutputStream类用作装饰类。修饰OutputStream的写行为,必须这样实现:publicinterfaceOutputStream{voidwrite(byteb);voidwrite(byte[]b);}publicclassFileOutputStreamimplementsOutputStream{/*...*/}publicclassBufferedOutputStreamextendsOutputStream{//这里是组合装饰器protectedfinalOutputStreamdecoratee;publicBufferedOutputStream(OutputStreamdecoratee){this.decoratee=decoratee;}publicvoidwrite(byteb){//这里应该调用decoratee,而不是super,虽然可以访问superdecoratee.write(buffer)}}但是,在Scala中实现Decorator模式,情况有点不同。Scala的trait不仅体现了JavaInterface的语义,还可以提供实现逻辑(相当于Java8的默认接口),并在编译时使用mixin完成代码复用。也就是说,trait完美的融合了继承和组合各自的优势。因此,如果想在Scala中实现Decorator模式,只需要定义一个trait来实现装饰器的功能:traitOutputStream{defwrite(b:Byte)defwrite(b:Array[Byte])}classFileOutputStream(path:String)extendsOutputStream{/*...*/}traitBufferingextendsOutputStream{abstractoverridefwrite(b:Byte){//...super.write(buffer)}}在Buffering的定义中,完全没有combination的影子,而且write方法重写的时候调用了super,这和前面说的背道而驰!区别在于授权的时间。在Java(原谅我,因为使用了Scala,我没有研究Java8的默认接口,不知道是不是和Scala的trait一模一样)语言中,组合是通过传递完成的责任委托和重用对象,也就是说,组合发生在运行时。Scala的实现不是这样的。在trait中使用abstractoverride关键字来完成一次可堆叠的修改。这种方法称为StackableTraitPattern。该语法只能用于trait,这意味着trait会将特定类为该方法提供的实现混合到trait中。修饰后的客户端代码如下:newFileOutputStream("foo.txt")和Buffering,FileOutputStream的write方法的实现在编译时混入了Buffering。所以这种组合可以称为静态组合。DependencyInjectionDependencyInjection(依赖注入或IoC,即控制反转)其实应该结合依赖倒置的原理来理解。首先要保证不依赖于实现细节,而是依赖于抽象(接口),然后考虑将具体的依赖从类内部移到外部,将依赖注入到类的内部运行时的类。这也是DependencyInjection名称的由来。在Java世界中,大多数情况下我们会引入Spring、Guice等框架来完成依赖注入(这并不是说依赖注入就一定需要框架,严格来说只要把依赖转移到外部,然后通过set或constructor注入,可以认为是实现了依赖注入),无论是基于xml配置,还是注解,还是Groovy,其核心思想都是将对象之间的依赖设置(组装)传递给框架来完成。Scala也有类似的IoC框架。然而,在大多数情况下,Scala程序员会充分利用traits和self类型来实现所谓的依赖注入。这种设计模式在Scala中通常被称为蛋糕模式。一个典型的案例是将Repository实现注入到Service中。在Scala中,应该将Repository抽象为trait,然后在具体的Service实现中,通过SelfType引入Repository:traitRepository{defsave(user:User)}traitDatabaseRepositoryextendsRepository{/*...*/}traitUserService{self:Repository=>defcreate(user:User){//这里调用了Repository的save方法//调用SelfType的方法就像调用了自己的方法save(user)}}//这里的with完成了依赖在DatabaseRepository中,newUserServicewithDatabaseRepositoryCakePattern的注入遵循了DependencyInject的要求,但它并没有像Spring或Guice那样将注入依赖的责任完全转移给外部框架,而是将注入的权利交给了调用者。这会导致调用者代码无法与具体的依赖完全解耦,但在大多数情况下,这种轻量级的依赖注入方式更讨人喜欢。在Scala开发中,我们经常会用到CakePattern。在我的一篇文章《一次设计演进之旅》中,我介绍了CakePattern来完成ReportMetadata依赖的注入。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文