Scala 设计模式:行为型模式

来源:互联网 发布:温州软件外派 编辑:程序博客网 时间:2024/04/27 18:28

值对象(Value Object)

值对象(Value object)是一个小的、不可变的(immutable)对象,一般用来代表一个简单的实体。如果对象中的所有字段都相等,那么这个对象就相等。值对象被广泛用于表示数字、日期、颜色等等。在企业应用中他们被用作过程间通讯的数据传输对象(DTOs),由于他们的不可变性,所以在多线程编程中使用起来很方便。

在Java中,没有特殊的语法用来表示值对象,所以我们必须显示的定义构造函数,getter方法和其它的辅助方法,见下面的代码:

public class Point {    private final int x, y;    public Point(int x, int y) { this.x = x; this.y = y; }    public int getX() { return x; }    public int getY() { return y; }    public boolean equals(Object o) {        // ...        return x == that.x && y == that.y;    }    public int hashCode() {        return 31 * x + y;    }    public String toString() {        return String.format("Point(%d, %d)", x, y);    }}Point point = new Point(1, 2)

在scala中,我们可以使用元组(tuple)或者样例类(case class)来声明值对象,如果我们不需要单独的类,那么一般使用元组。

val point = (1, 2) // new Tuple2(1, 2)

元组是一个预先定义的、不可变的“集合”,这个集合由固定数目的类型组成,类型可以相同或不同。元组提供了构造器、getter方法和其它的辅助方法。我们还可以给它创建一个类型的别名,以便我们在声明的时候使用。

type Point = (Int, Int) // Tuple2[Int, Int]val point: Point = (1, 2)

如果我们需要一个特定的类名称,或者我们需要给字段一个有意义的名字,那么可能我们就需要定义一个样例类(case class):

case class Point(x: Int, y: Int)val point = Point(1, 2)

样例类将构造器的参数作为属性暴露出来。缺省时,样例类是不可变的。和元组一样,它也自动提供了需要的方法;与元组不同的是,它是有效的类,所以可以使用继承并且定义方法。

值对象是函数式编程中(作为一个代数数据类型(ADT)的概念)广泛使用的工具,Scala语言为值对象提供了完全的支持。

优势:
  • 语法简洁.
  • 预定义的元组类.
  • 内建的辅助方法.
劣势:
  • 没有.




空对象(Null Object)

空对象(Null Object)是通过定义一个中性的,什么都不做的行为来表示一个对象的不存在。使用这种方式比使用null引用(null references)有利,因为在使用前我们不用检查引用是否有效。在Java中,可以通过定义一个特殊的带有空方法的子类来实现。

public interface Sound {    void play();}public class Music implements Sound {    public void play() { /* ... */ }}public class NullSound implements Sound {    public void play() {}}public class SoundSource {    public static Sound getSound() {    return available ? music : new NullSound();    }}SoundSource.getSound().play();

这样,在调用play的时候我们就不需要检查getSound的引用是否为空。我们还可以将空对象定义为单例(Singleton)从而将“空实例”减少为一个。

Scala 使用了类似的方式,但是它提供了一个预定义的Option类型,作为可选值的占位符来使用。

trait Sound {  def play()}   class Music extends Sound {    def play() { /* ... */ }}object SoundSource {  def getSound: Option[Sound] =     if (available) Some(music) else None}  for (sound <- SoundSource.getSound) {  sound.play()}

在这个例子中,我们使用for推导式(for comprehension)来处理可选值(高阶函数(higher-order functions)和模式匹配(pattern matching)也同样适用)。

优势:
  • 预定义类型.
  • 清晰的可选值.
  • 使用内建的构造器.
劣势:
  • 使用的时候比较啰嗦.




策略(Strategy)

策略模式(strategy pattern),又叫算法簇模式,就是定义了不同的算法族,并且之间可以互相替换,此模式让算法的变化独立于使用算法的客户, 策略模式的好处在于你可以动态的改变对象的行为。

在Java中, strategy模式通常通过继承基类接口的一些类的方式来实现。

public interface Strategy {    int compute(int a, int b);}public class Add implements Strategy {    public int compute(int a, int b) { return a + b; }}public class Multiply implements Strategy {    public int compute(int a, int b) { return a * b; }}public class Context  {    private final Strategy strategy;    public Context(Strategy strategy) { this.strategy = strategy; }    public void use(int a, int b) { strategy.compute(a, b); }}new Context(new Multiply()).use(2, 3);

在scala中,函数是“头等公民”,我们可以直接使用它们来表示同样的概念。

type Strategy = (Int, Int) => Int class Context(computer: Strategy) {  def use(a: Int, b: Int)  { computer(a, b) }}val add: Strategy = _ + _val multiply: Strategy = _ * _new Context(multiply).use(2, 3)

当策略中包含多个方法时,我们可以使用一个样例类或者元组来将它们组合在一起。

优势:
  • 语法简洁.
劣势:
  • General-purpose type.

命令(Command)

命令模式(command pattern)封装了需要调用方法的所有信息,这些方法稍后会被调用。封装的信息包括方法的名字,拥有方法的对象和方法的参数值。命令模式用来延迟、序列化或记录方法的调用。

在Java中,我们可以通过将调用包含在对象中来实现这个目的。

public class PrintCommand implements Runnable {    private final String s;    PrintCommand(String s) { this.s = s; }    public void run() {        System.out.println(s);    }}public class Invoker {    private final List<Runnable> history = new ArrayList<>();    void invoke(Runnable command) {        command.run();        history.add(command);    }}Invoker invoker = new Invoker();invoker.invoke(new PrintCommand("foo"));invoker.invoke(new PrintCommand("bar"));

在scala中,我们可以依赖by-name 参数去延迟对一个表达式的求值。

object Invoker {  private var history: Seq[() => Unit] = Seq.empty  def invoke(command: => Unit) { // by-name parameter    command    history :+= command _  }}Invoker.invoke(println("foo"))  Invoker.invoke {  println("bar 1")  println("bar 2")}

这样我们就可以将任意表达式或者代码块转换成一个函数对象。对println方法的调用在invoke方法内执行,然后代码块被作为函数存放在history序列中,我们也可以直接定义function而不用by-name参数,但是这样会使代码有一点啰嗦。

优势:
  • 语法简洁.
劣势:
  • General-purpose type.


职责链(Chain of responsibility)

职责链模式( chain of responsibility pattern)解耦了请求的发送者和接收者,使多个接收者有机会去处理这个请求,每个接收者对象及其下家形成了一个链,请求在这个链上被传递直到找到合适的接收者来处理它。

在一个典型的实现中,每一个处理对象继承一个基类接口,并且包含一个可选的引用来指向下一个处理对象,每一个对象可以处理请求(或者打断处理过程),或者传递请求到下一个处理对象。对象序列的逻辑可以有其它对象实现,或者封装在基类中。

public abstract class EventHandler {    private EventHandler next;    void setNext(EventHandler handler) { next = handler; }    public void handle(Event event) {        if (canHandle(event)) doHandle(event);        else if (next != null) next.handle(event);    }    abstract protected boolean canHandle(Event event);    abstract protected void doHandle(Event event);}public class KeyboardHandler extends EventHandler { // MouseHandler...    protected boolean canHandle(Event event) {        return "keyboard".equals(event.getSource());    }    protected void doHandle(Event event) { /* ... */ }}KeyboardHandler handler = new KeyboardHandler();handler.setNext(new MouseHandler());

因为这样的一个实现有点类似装饰者模式(Decorator),所以我们也可以使用override的功能来达到这个目的。但是,Scala提供了一个更加直接的方式,这就是用偏函数(partial functions),或者翻译成部分函数。

偏函数是这样的一个函数,它只定义了函数参数的有限的可能值。我们可以在偏函数上直接调用isDefinedAt和Apply方法来实现序列,还有一种更好的方式是使用内建的orElse方法。

case class Event(source: String)type EventHandler = PartialFunction[Event, Unit]val defaultHandler: EventHandler = PartialFunction(_ => ())val keyboardHandler: EventHandler = {  case Event("keyboard") => /* ... */}def mouseHandler(delay: Int): EventHandler = {  case Event("mouse") => /* ... */}keyboardHandler.orElse(mouseHandler(100)).orElse(defaultHandler)

注意,我们使用defaultHandle来避免没有定义的事件所引发的错误。

优势:
  • 语法简洁.
  • 内建逻辑.
劣势:
  • General-purpose type.



依赖注入(Dependency injection)

依赖注入(dependency injection) (DI) 模式可以使我们避免对依赖的硬编码,并且可以在运行时或编译时去替换依赖。这个模式是反转控制(inversion of control)技术的一个特例。

在应用程序中,依赖注入用于从一个组件不同的实现中选择合适的实现,或者提供一个模拟的实现用于单元测试。

如果不考虑Ioc容器( IoC containers)的实现,最简单的实现方式就是用构造参数来传递需要的依赖。 于是我们就可以使用构成(composition)的方式来表示依赖。

public interface Repository {    void save(User user);}public class DatabaseRepository implements Repository { /* ... */ }public class UserService {    private final Repository repository;    UserService(Repository repository) {        this.repository = repository;    }    void create(User user) {        // ...        repository.save(user);    }}new UserService(new DatabaseRepository());

除了构成(“HAS-A”关系)和继承(“IS-A”关系),scala提供了一个特殊的关系 —— 需要关系(“REQUIRES-A”),用自身类型(self-type)的方式的表示。自身类型允许我们定义一个对象需要的特殊的附加类型,而不用显示的在继承层次结构去暴露它。

我们可以利用在特质中使用自身类型来实现依赖注入。

trait Repository {  def save(user: User)}trait DatabaseRepository extends Repository { /* ... */ }trait UserService { self: Repository => // requires Repository  def create(user: User) {    // ...    save(user)  }}new UserService with DatabaseRepository

和构造函数注入不同的是,这种方式需要对每个配置的依赖的一个单一引用。对这个技术的一个完整的实现被称为蛋糕模式( Cake pattern) (在scala中对依赖注入可以有多种实现方式)。

由于特质的混入是静态的,所以这种方式仅限于编译时的依赖注入。实际上,很少在运行期间重新配置依赖关系,而特质混入方式提供的静态类型检查相对于基于xml的方式的配置又具有明显的优势。

优势:
  • 内容清晰.
  • 语法简洁.
  • 静态类型检查.
劣势:
  • 编译期配置.
  • 可能导致代码庸长。.




这篇文章主要是翻译Design Patterns in Scala,但有所缩减和改动。


原创粉丝点击