Scala 设计模式:行为型模式

值对象(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 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,但有所缩减和改动。


你可能感兴趣的:(scala,design,patterns,设计模式)