值对象(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)
val point = (1, 2) // new Tuple2(1, 2)
元组是一个预先定义的、不可变的“集合”,这个集合由固定数目的类型组成,类型可以相同或不同。元组提供了构造器、getter方法和其它的辅助方法。我们还可以给它创建一个类型的别名,以便我们在声明的时候使用。
type Point = (Int, Int) // Tuple2[Int, Int]
val point: Point = (1, 2)
case class Point(x: Int, y: Int)
val point = Point(1, 2)
样例类将构造器的参数作为属性暴露出来。缺省时,样例类是不可变的。和元组一样,它也自动提供了需要的方法;与元组不同的是,它是有效的类,所以可以使用继承并且定义方法。
值对象是函数式编程中(作为一个代数数据类型(ADT)的概念)广泛使用的工具,Scala语言为值对象提供了完全的支持。
空对象(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()
}
策略模式(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);
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)
当策略中包含多个方法时,我们可以使用一个样例类或者元组来将它们组合在一起。
命令模式(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"));
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参数,但是这样会使代码有一点啰嗦。
职责链模式( 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)
依赖注入(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,但有所缩减和改动。