1. 程式人生 > >鴨子型別(Duck Typing)語言中的LSP原則(Liskov Substitution Principle)

鴨子型別(Duck Typing)語言中的LSP原則(Liskov Substitution Principle)

今天我們要說的是LSP原則在Duck Typing語言中的表現。

Duck Typing(中文翻譯為“鴨子型別”)是一個新名詞,它是面嚮物件語言中動態型別(多型)的另外一種表達形式。我們知道傳統的(強型別)的面嚮物件語言中,要確定某個物件有哪些方法和屬性通常看它繼承哪個類或實現哪個介面。而Duck Typing是通過這個物件的行為和屬性來判定它大概是什麼。Duck Typing最初的定義來自 duck test

當我看見一隻鳥,它走路時候像鴨子,游泳時候像鴨子,嘎嘎叫的時候也像鴨子,我就稱這隻鳥為鴨子。


然後我們重新溫故一下LSP原則的定義。

LSP原則:
如果S是T的子類,那麼程式碼中所有用到T的地方,都可以通過S替代

。這個原則是傳統的強型別面嚮物件語言(如Java)必須遵守的一條原則。關於LSP原則的更多介紹,參考LSP wiki

可以說LSP原則是為繼承量身定做的,繼承能夠完美遵循此原則。與使用LSP原則來描述繼承相比較,那個著名的is-a關係可以說不是足夠嚴格。不過,另外一個behaves-as-a(行為看起來像)的關係定義更是不嚴格。我們注意到is-a是定義結構之間的關係,behaves-as-a(行為看起來像)是定義行為之間的關係。

大家注意到我有一個小小的假設:LSP定義了繼承,為什麼繼承被具體定義但是替代沒有呢?因為對大多數靜態型別(強型別)語言來說,繼承是替代的表達方式。

例如,Java中通常的抽象方式是定義一個介面:

public interface Tracing {
void trace(String message);
}


client會用Tracing定義的trace裡實現紀錄日誌,client可以使用的類必須是實現Tracing介面:

public class TracerClient {
private Tracer tracer;

public TracerClient(Tracer tracer) {
this.tracer = tracer;
}

public void doWork() {
tracer.trace("in doWork():");
// ...
}
}

但是Duck Typing語言是另外一個實現替代(其實就是多型)的形式,這用語言通常是Ruby和Python。

當我看見一隻鳥,它走路時候像鴨子,游泳時候像鴨子,嘎嘎叫的時候也像鴨子,我就稱這隻鳥為鴨子。

通俗地說,Duck Typing表達的是client可以使用任何物件只要該物件實現了client想要呼叫的方法。換句話說,該物件必須響應client發給它的訊息。

只要client認為某物件是一隻鴨子,它就是。

在我們的例子中,client只關心是否有trace方法,因此,使用Ruby可以這樣實現:

class TracerClient
def initialize tracer
@tracer = tracer
end

def do_work
@tracer.trace "in do_work:"
# ...
end
end

class MyTracer
def trace message
p message
end
end

client = TracerClient.new(MyTracer.new)

這個例子中並沒有定義介面(interface),只是傳遞一個物件給TracerClient的initialize方法,使得TracerClient能夠響應client的trace訊息。這裡我給MyTracer加上了trace方法,當然你也可以給任何物件加上trace方法。

因此LSP原則還是這裡的核心原則,其意義就體現在替換上,只是Duck Typing不是用繼承表達替換而已。

那麼Duck Typing到底是好還是壞呢?現在有很多關於動態型別(弱型別)語言和靜態型別(強型別)語言的討論,我在這兒不斷言誰好誰壞,只是發表我的觀點。
Duck Typing不好的一面是,由於沒有了Tracer的抽象,我們只能根據物件方法的名字來揣測此方法能做什麼。同時,也不能找出全部提供trace方法的類。
從另外一面來說,clien實際並不關心Tracer的型別,而是隻關心trace方法。這樣實際上將client和server做了一點解耦。

我們使用Scala來作比較,看Scala中是如何實現替換的。Scala是一種靜態型別語言,不支援Duck Typing,但是它提供了一種很類似的機制被稱作結構型別。結構型別的核心思想是使得程式設計師可以申明一個方法的引數,此引數接收指定的函式。與Java中的匿名介面實現類有點像。
在我們的Java例子程式碼中,在Scala中可以做到僅實現trace方法,而不用實現介面中的所有方法。

public class TracerClient {
public TracerClient(Tracer tracer) { ... }
// ...
}
}

上面的介面在Scala中,可以這麼實現:

class ScalaTracerClient(val tracer: { def trace(message:String) }) {
def doWork() = { tracer.trace("doWork") }
}

class ScalaTracer() {
def trace(message: String) = { println("Scala: "+message) }
}

object TestScalaTracerClient {
def main() {
val client = new ScalaTracerClient(new ScalaTracer())
client.doWork();
}
}
TestScalaTracerClient.main()

從程式碼中可以看出,ScalaTracerClient的建構函式,接收一個型別是{ def trace(message:String) }的trace引數。client對server的要求就是響應trace訊息。
因此我們就做成了Duck Typing的行為,且是靜態型別的。如果在構造ScalaTracerClient傳遞的是一個不支援trace函式的物件,則在編譯時候會報錯,而不是執行時。

將上面所說的總結一下,LSP原則在Duck Typing語言中可以稍加修正地表述為:

如果S與T有相同的行為,那麼在程式碼中所有依賴T的行為的地方,都可以使用T的行為替代。

我們將子類替換為了行為。

最後要說的是,client和server之間的合約還是十分重要,無論是在Duck Typing語言還是Structer Typing語言。當然,Duck Typing語言提供給我們一種新的擴充套件系統現有功能的方法。