本文展示了一些經(jīng)典的軟件設(shè)計(jì)模式在Scala中的實(shí)現(xiàn)。
所謂設(shè)計(jì)模式强霎,就是針對(duì)在軟件設(shè)計(jì)過程中出現(xiàn)的一些共性問題夏跷,從而產(chǎn)生的一種可重用的解決方案。設(shè)計(jì)模式不是已完成的代碼窃这,而更像是一個(gè)可以在不同場(chǎng)景下解決問題的通用模板瞳别。
模式是由一些設(shè)計(jì)的最佳實(shí)踐組成的,可以幫助我們避免一些問題杭攻,并且能增加代碼的可讀性祟敛,及加快開發(fā)進(jìn)度。
經(jīng)典的設(shè)計(jì)模式(一般指GoF)都是基于面向?qū)ο蟮恼捉狻K麄冋故玖祟惻c對(duì)象間的關(guān)系和行為馆铁。這些模式并不能很好的應(yīng)用到純函數(shù)式編程語言上,但是既然Scala是一種結(jié)合了面向?qū)ο缶幊毯秃瘮?shù)式編程的語言锅睛,那Scala還是能夠采用這些模式的埠巨,甚至是在函數(shù)式風(fēng)格的Scala代碼中。
很多時(shí)候設(shè)計(jì)模式被認(rèn)為是某種語言缺乏一些特性的信號(hào)衣撬。在此種情況下乖订,當(dāng)一種語言提供了相關(guān)特性以后,這些模式可以被簡(jiǎn)化或者索性消除具练。得益于Scala富有表現(xiàn)力的語法乍构,很多經(jīng)典設(shè)計(jì)模式都可以直接實(shí)現(xiàn)。
盡管Scala還有一些基于語言特性的設(shè)計(jì)模式扛点,單本文還是著重于介紹大家所周知的經(jīng)典設(shè)計(jì)模式哥遮,因?yàn)檫@些設(shè)計(jì)模式被認(rèn)為是開發(fā)者之間交流的工具。
創(chuàng)建型設(shè)計(jì)模式
1陵究、工廠方法模式
2眠饮、延遲加載模式
3、單例模式
結(jié)構(gòu)型模式
1铜邮、適配器模式
2仪召、裝飾模式
行為型
1寨蹋、值對(duì)象模式
2、空值模式
3扔茅、策略模式
4已旧、命令模式
5、責(zé)任鏈模
6召娜、依賴注入模式
一运褪、工廠方法模式
工廠方法模式將對(duì)實(shí)際類的初始化封裝在一個(gè)方法中,讓子類來決定初始化哪個(gè)類玖瘸。
工廠方法允許:
1秸讹、組合復(fù)雜的對(duì)象創(chuàng)建代碼
2、選擇需要初始化的類
3雅倒、緩存對(duì)象
4璃诀、協(xié)調(diào)對(duì)共享資源的訪問
我們考慮靜態(tài)工廠模式,這和經(jīng)典的工廠模式略有不同屯断,靜態(tài)工廠方法避免了子類來覆蓋此方法文虏。
在Java中,我們使用new關(guān)鍵字殖演,通過調(diào)用類的構(gòu)造器來初始化對(duì)象氧秘。為了實(shí)現(xiàn)這個(gè)模式,我們需要依靠普通方法趴久,此外我們無法在接口中定義靜態(tài)方法丸相,所以我們只能使用一個(gè)額外的工廠類。
public interface Animal {}
private class Dog implements Animal {}
private class Cat implements Animal {}
public class AnimalFactory {
public static Animal createAnimal(String kind) {
if ("cat".equals(kind)) return new Cat();
if ("dog".equals(kind)) return new Dog();
throw new IllegalArgumentException();
}
}
AnimalFactory.createAnimal("dog");
除了構(gòu)造器之外彼棍,Scala提供了一種類似于構(gòu)造器調(diào)用的特殊的語法灭忠,其實(shí)這就是一種簡(jiǎn)便的工廠模式。
trait Animal
private class Dog extends Animal
private class Cat extends Animal
object Animal {
def apply(kind: String) = kind match {
case "dog" => new Dog()
case "cat" => new Cat()
}
}
Animal("dog")
以上代碼中座硕,工廠方法被定義為伴生對(duì)象弛作,它是一種特殊的單例對(duì)象,和之前定義的類或特質(zhì)具有相同的名字华匾,并且需要定義在同一個(gè)原文件中映琳。這種語法僅限于工廠模式中的靜態(tài)工廠模式,因?yàn)槲覀儾荒軐?chuàng)建對(duì)象的動(dòng)作代理給子類來完成蜘拉。
優(yōu)勢(shì):
- 重用基類名字
- 標(biāo)準(zhǔn)并且簡(jiǎn)潔
- 類似于構(gòu)造器調(diào)用
劣勢(shì):
- 僅限于靜態(tài)工廠方法
二 萨西、延遲初始化模式
延遲初始化是延遲加載的一個(gè)特例。它指僅當(dāng)?shù)谝淮卧L問一個(gè)值或者對(duì)象的時(shí)候旭旭,才去初始化他們谎脯。
延遲初始化可以延遲或者避免一些比較復(fù)雜的運(yùn)算。
在Java中持寄,一般用null來代表未初始化狀態(tài)源梭,但假如null是一個(gè)合法的final值的時(shí)候娱俺,我們就需要一個(gè)獨(dú)立的標(biāo)記來指示初始化過程已經(jīng)進(jìn)行。
在多線程環(huán)境下废麻,對(duì)以上提到的標(biāo)記的訪問必須要進(jìn)行同步矢否,并且會(huì)采用雙重檢測(cè)技術(shù)(double-check)來保證正確性,當(dāng)然這也進(jìn)一步增加了代碼的復(fù)雜性脑溢。
private volatile Component component;
public Component getComponent() {
Component result = component;
if (result == null) {
synchronized(this) {
result = component;
if (result == null) {
component = result = new Component();
}
}
}
return result;
}
Scala提供了一個(gè)內(nèi)置的語法來定義延遲變量.
lazy val x = {
print("(computing x) ")
42
}
print("x = ")
println(x)
// x = (computing x) 42
在Scala中,延遲變量能夠持有null值赖欣,并且是線程安全的屑彻。
優(yōu)勢(shì)
- 語法簡(jiǎn)潔
- 延遲變量能夠持有null值
- 延遲變量的訪問是線程安全的
劣勢(shì)
- 對(duì)初始化行為缺乏控制
三、單例模式
單例模式限制了一個(gè)類只能初始化一個(gè)對(duì)象顶吮,并且會(huì)提供一個(gè)全局引用指向它社牲。
在Java中,單例模式或許是最為被人熟知的一個(gè)模式了悴了。這是java缺少某種語言特性的明顯信號(hào)搏恤。
在java中有static關(guān)鍵字,靜態(tài)方法不能被任何對(duì)象訪問湃交,并且靜態(tài)成員類不能實(shí)現(xiàn)任何接口熟空。所以靜態(tài)方法和Java提出的一切皆對(duì)象背離了。靜態(tài)成員也只是個(gè)花哨的名字搞莺,本質(zhì)上只不過是傳統(tǒng)意義上的子程序息罗。
public class Cat implements Runnable {
private static final Cat instance = new Cat();
private Cat() {}
public void run() {
// do nothing
}
public static Cat getInstance() {
return instance;
}
}
Cat.getInstance().run()
在Scala中完成單例簡(jiǎn)直巨簡(jiǎn)單無比
object Cat extends Runnable {
def run() {
// do nothing
}
}
Cat.run()
優(yōu)勢(shì):
- 含義明確
- 語法簡(jiǎn)潔
- 按需初始化
- 線程安全
劣勢(shì):
- 對(duì)初始化行為缺乏控制
四、適配器模式
適配器模式能將不兼容的接口放在一起協(xié)同工作才沧,適配器對(duì)集成已經(jīng)存在的各個(gè)組件很有用迈喉。
在Java實(shí)現(xiàn)中,需要?jiǎng)?chuàng)建一個(gè)封裝類温圆,如下所示:
public interface Log {
void warning(String message);
void error(String message);
}
public final class Logger {
void log(Level level, String message) { /* ... */ }
}
public class LoggerToLogAdapter implements Log {
private final Logger logger;
public LoggerToLogAdapter(Logger logger) { this.logger = logger; }
public void warning(String message) {
logger.log(WARNING, message);
}
public void error(String message) {
logger.log(ERROR, message);
}
}
Log log = new LoggerToLogAdapter(new Logger());
在Scala中挨摸,我們可以用隱式類輕松搞定。(注意:2.10后加的特性)
trait Log {
def warning(message: String)
def error(message: String)
}
final class Logger {
def log(level: Level, message: String) { /* ... */ }
}
implicit class LoggerToLogAdapter(logger: Logger) extends Log {
def warning(message: String) { logger.log(WARNING, message) }
def error(message: String) { logger.log(ERROR, message) }
}
val log: Log = new Logger()
最后的表達(dá)式期望的得到一個(gè)Log實(shí)例岁歉,而卻使用了Logger得运,這個(gè)時(shí)候Scala編譯器會(huì)自動(dòng)把log實(shí)例封裝到適配器類中。
優(yōu)勢(shì):
- 含義清晰
- 語法簡(jiǎn)潔
劣勢(shì):
- 在沒有IDE的支持下會(huì)顯得晦澀
五刨裆、裝飾模式
裝飾模式被用來在不影響一個(gè)類其它實(shí)例的基礎(chǔ)上擴(kuò)展一些對(duì)象的功能澈圈。裝飾者是對(duì)繼承的一個(gè)靈活替代。
當(dāng)需要有很多獨(dú)立的方式來擴(kuò)展功能時(shí)帆啃,裝飾者模式是很有用的瞬女,這些擴(kuò)展可以隨意組合。
在Java中努潘,需要新建一個(gè)裝飾類诽偷,實(shí)現(xiàn)原來的接口坤学,封裝原來實(shí)現(xiàn)接口的類,不同的裝飾者可以組合起來使用报慕。一個(gè)處于中間層的裝飾者一般會(huì)用來代理原接口中很多的方法深浮。
public interface OutputStream {
void write(byte b);
void write(byte[] b);
}
public class FileOutputStream implements OutputStream { /* ... */ }
public abstract class OutputStreamDecorator implements OutputStream {
protected final OutputStream delegate;
protected OutputStreamDecorator(OutputStream delegate) {
this.delegate = delegate;
}
public void write(byte b) { delegate.write(b); }
public void write(byte[] b) { delegate.write(b); }
}
public class BufferedOutputStream extends OutputStreamDecorator {
public BufferedOutputStream(OutputStream delegate) {
super(delegate);
}
public void write(byte b) {
// ...
delegate.write(buffer)
}
}
new BufferedOutputStream(new FileOutputStream("foo.txt"))
Scala提供了一種更直接的方式來重寫接口中的方法,并且不用綁定到具體實(shí)現(xiàn)眠冈。下面看下如何來使用abstract override標(biāo)識(shí)符飞苇。
trait OutputStream {
def write(b: Byte)
def write(b: Array[Byte])
}
class FileOutputStream(path: String) extends OutputStream { /* ... */ }
trait Buffering extends OutputStream {
abstract override def write(b: Byte) {
// ...
super.write(buffer)
}
}
new FileOutputStream("foo.txt") with Buffering // with Filtering, ...
這種代理是在編譯時(shí)期靜態(tài)建立的,不過通常來說只要我們能在創(chuàng)建對(duì)象時(shí)任何組合裝飾器蜗顽,就已經(jīng)夠用了布卡。
與基于組合(指需要特定的裝飾類來把原類封裝進(jìn)去)的實(shí)現(xiàn)方式不一樣,Scala保持了對(duì)象的一致性雇盖,所以可以在裝飾對(duì)象上放心使用equals忿等。
優(yōu)勢(shì):
- 含義清晰
- 語法簡(jiǎn)潔
- 保持了對(duì)象一致性
- 無需顯式的代理
- 無需中間層的裝飾類
劣勢(shì):
- 靜態(tài)綁定
- 沒有構(gòu)造器參數(shù)
六、值對(duì)象模式
值對(duì)象是一個(gè)很小的不可變對(duì)象崔挖,他們的相等性不基于identity贸街,而是基于不同對(duì)象包含的字段是否相等。
值對(duì)象被廣泛應(yīng)用于表示數(shù)字狸相、時(shí)間薛匪、顏色等等。在企業(yè)級(jí)應(yīng)用中脓鹃,它們經(jīng)常被用作DTO(可以用來做進(jìn)程間通信)蛋辈,由于不變性,值對(duì)象在多線程環(huán)境下使用起來非常方便将谊。
在Java中冷溶,并沒有特殊語法來支持值對(duì)象。所以我們必須顯式定義一個(gè)構(gòu)造器尊浓,getter方法及相關(guān)輔助方法逞频。
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中,我們使用元組或者樣例類來申明值對(duì)象栋齿。當(dāng)不需要使用特定的類的時(shí)候苗胀,元組就足夠了.
val point = (1, 2) // new Tuple2(1, 2)
元組是一個(gè)預(yù)先定義好的不變集合,它能夠持有若干個(gè)不同類型的元素瓦堵。元組提供構(gòu)造器基协,getter方法以及所有輔助方法。
我們也可以為Point類定義一個(gè)類型別名
type Point = (Int, Int) // Tuple2[Int, Int]
val point: Point = (1, 2)
當(dāng)需要一個(gè)特定的類或者需要對(duì)數(shù)據(jù)元素名稱有更明確的描述的時(shí)候菇用,可以使用樣例類;
case class Point(x: Int, y: Int)
val point = Point(1, 2)
樣例類將構(gòu)造器參數(shù)默認(rèn)為屬性澜驮。樣例類是不可變的,與元組一樣惋鸥,它提供了所有所需的方法杂穷。因?yàn)闃永愂呛戏ǖ念惡凡运部梢允褂美^承及定義成員。
值對(duì)象模式是函數(shù)式編程中一個(gè)非常常用的工具耐量,Scala在語言級(jí)別對(duì)其提供了直接支持飞蚓。
優(yōu)勢(shì):
- 語法簡(jiǎn)潔
- 預(yù)定義元組類
- 內(nèi)置輔助方法
劣勢(shì):
- 無
七、空值模式
空值模式定義了一個(gè)“啥都不干”的行為廊蜒,這個(gè)模式比起空引用有一個(gè)優(yōu)勢(shì)趴拧,它不需要在使用前檢查引用的合法性。
在java中山叮,我們需要定義一個(gè)帶空方法的子類來實(shí)現(xiàn)此模式八堡。
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();
所以,由getSound獲得Sound實(shí)例再調(diào)用play方法聘芜,不需要檢查Sound實(shí)例是否為空。更進(jìn)一步缝龄,我們可以使用單例模式來限制只生成唯一的空對(duì)象汰现。Scala也采用了類似的方法,但是它提供了一個(gè)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()
}
在此場(chǎng)景下,我們使用for推導(dǎo)來處理Option類型(高階函數(shù)和模式匹配也能輕松搞定此事)炼绘。
優(yōu)勢(shì):
- 預(yù)定義類型
- 明確的可選擇性
- 內(nèi)置結(jié)構(gòu)支持
劣勢(shì):
- 比較冗長(zhǎng)的用法
八嗅战、策略模式
策略模式定義了一組封裝好的算法,讓算法變化獨(dú)立于用戶調(diào)用俺亮。需要在運(yùn)行時(shí)選擇算法時(shí)驮捍,策略模式非常有用。
在java中脚曾,一般先要定義一個(gè)接口东且,然后新建幾個(gè)類分別去實(shí)現(xiàn)這個(gè)接口。
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中本讥,函數(shù)是頭等公民珊泳,可以直接實(shí)現(xiàn)如下(不得不說實(shí)現(xiàn)起來很爽)。
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)
假如策略包含很多方法的話拷沸,我們可以使用元組或者樣例類把所有方法封裝在一起色查。
優(yōu)勢(shì):
- 語法簡(jiǎn)潔
劣勢(shì):
- 通用類型
九、命令模式
命令模式封裝了需要在稍后調(diào)用方法的所有信息撞芍,這些信息包括擁有這些方法的對(duì)象和這些方法的參數(shù)值秧了。
命令模式適用于延時(shí)方法調(diào)用,順序化方法調(diào)用及方法調(diào)用時(shí)記錄日志序无。(當(dāng)然還有其它很多場(chǎng)景)
在Java中示惊,需要把方法調(diào)用封裝在對(duì)象中好港。
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中,我們使用換名調(diào)用來實(shí)現(xiàn)延遲調(diào)用
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")
}
這就是我們?cè)鯓影讶我獾谋磉_(dá)式或者代碼塊轉(zhuǎn)換為一個(gè)函數(shù)對(duì)象米罚。當(dāng)調(diào)用invoke方法的時(shí)候才會(huì)調(diào)用println方法钧汹,然后以函數(shù)形式存在歷史序列中。我們也可以直接定義函數(shù)录择,而不采用換名調(diào)用拔莱,但是那種方式太冗長(zhǎng)了。
優(yōu)勢(shì):
- 語法簡(jiǎn)潔
劣勢(shì):
- 通用類型
十隘竭、責(zé)任鏈模式
責(zé)任鏈模式解耦了發(fā)送方與接收方塘秦,使得有更多的對(duì)象有機(jī)會(huì)去處理這個(gè)請(qǐng)求,這個(gè)請(qǐng)求一直在這個(gè)鏈中流動(dòng)直到有個(gè)對(duì)象處理了它
責(zé)任鏈模式的一個(gè)典型實(shí)現(xiàn)是責(zé)任鏈中的所有的對(duì)象都會(huì)繼承一個(gè)基類动看,并且可能會(huì)包含一個(gè)指向鏈中下一個(gè)處理對(duì)象的引用尊剔。每一個(gè)對(duì)象都有機(jī)會(huì)處理請(qǐng)求(或者中斷請(qǐng)求),或者將請(qǐng)求推給下一個(gè)處理對(duì)象菱皆。責(zé)任鏈的順序邏輯可以要么代理給對(duì)象處理须误,要么就封裝在一個(gè)基類中。
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());
由于以上的實(shí)現(xiàn)有點(diǎn)類似于裝飾者模式仇轻,所以我們?cè)赟cala中可以使用abstract override來解決這個(gè)問題京痢。不過Scala提供了一種更加直接的方式,即基于偏函數(shù)篷店。
偏函數(shù)簡(jiǎn)單來說就是某個(gè)函數(shù)只會(huì)針對(duì)它參數(shù)的可能值的自己進(jìn)行處理祭椰。可以直接使用偏函數(shù)的isDefinedAt和apply方法來實(shí)現(xiàn)順序邏輯疲陕,更好的方法是使用內(nèi)置的orElse方法來實(shí)現(xiàn)請(qǐng)求的傳遞方淤。
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)
注意我們必須使用defaultHandler來避免出現(xiàn)“undefined”事件的錯(cuò)誤。
優(yōu)勢(shì):
- 語法簡(jiǎn)潔
- 內(nèi)置邏輯
劣質(zhì):
- 通用類型
十一蹄殃、依賴注入模式
依賴注入可以讓我們避免硬編碼依賴關(guān)系臣淤,并且允許在編譯期或者運(yùn)行時(shí)替換依賴關(guān)系。此模式是控制反轉(zhuǎn)的一個(gè)特例(用過Spring的同學(xué)都對(duì)這個(gè)模式熟爛了吧)窃爷。
依賴注入是在某個(gè)組件的眾多實(shí)現(xiàn)中選擇邑蒋,或者為了單元測(cè)試而去模擬組件。
除了使用IoC容器按厘,在Java中最簡(jiǎn)單的實(shí)現(xiàn)就是像構(gòu)造器參數(shù)需要的依賴医吊。所以我們可以利用組合來表達(dá)依賴需求。
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”)與繼承(“HAS-A”)的關(guān)系外逮京,Scala還增加一種新的關(guān)系:需要(“REQUIRES -A”), 通過自身類型注解來實(shí)現(xiàn)卿堂。(建議大家去熟悉一下自身類型的定義與使用)
Scala中可以混合使用自身類型與特質(zhì)來進(jìn)行依賴注入。
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
不同于構(gòu)造器注入,以上方式有個(gè)要求:配置中的每一種依賴都需要一個(gè)單獨(dú)的引用草描,這種技術(shù)的完整實(shí)踐就叫蛋糕模式览绿。(當(dāng)然,在Scala中穗慕,還有很多方式來實(shí)現(xiàn)依賴注入)饿敲。
在Scala中,既然特質(zhì)的混入是靜態(tài)的逛绵,所以此方法也僅限于編譯時(shí)依賴注入怀各。事實(shí)上,運(yùn)行時(shí)的依賴注入幾乎用不著术浪,而對(duì)配置的靜態(tài)檢查相對(duì)于運(yùn)行時(shí)檢查有很大的優(yōu)勢(shì)瓢对。
優(yōu)勢(shì):
- 含義明確
- 語法簡(jiǎn)潔
- 靜態(tài)檢查
劣勢(shì):
- 編譯期配置
- 形式上可能有點(diǎn)冗長(zhǎng)