面向“接口”編程和面向“實現(xiàn)”編程
如果你已經(jīng)讀了我的前幾篇關(guān)于面向?qū)ο蠓妒揭驗槭艿?a >Rust and Go等語言的影響而發(fā)生變化的文章,看到了我正在研究的Rust設(shè)計模式,你會發(fā)現(xiàn)我對Rust語言十分的偏愛。
除此之外,就在上周末,我讀完了經(jīng)典的《設(shè)計模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》。這些種種,引起了我對這本書中談及的一個核心原則的思考:
面向‘接口’編程,而不是面向‘實現(xiàn)’。
這是什么意思?
首先我們需要理解什么是‘接口’,什么是‘實現(xiàn)’。簡言之,一個接口就是我們要調(diào)用的一系列方法的集合,有對象將會響應(yīng)這些方法調(diào)用。
一個實現(xiàn)就是為接口存放代碼和邏輯的地方。
本質(zhì)上講,這個原則倡導(dǎo)的是,當(dāng)我們寫一個函數(shù)或一個方法時,我們應(yīng)該引用相應(yīng)的接口,而不是具體的實現(xiàn)類。
面向‘實現(xiàn)’編程
首先我們看看,如果不遵循這個原則會發(fā)生什么。
假設(shè)你是《華氏451度》這本書里的“Montag”這個人。大家都知道,書在華氏451度會燒著的。小說中的消防隊員只要看到了書就會把它們丟到火里。我們用面向?qū)ο蟮囊暯钦f問題,書有一個叫做burn()
的方法。
書并不是唯一會燃燒的東西。假設(shè)我們還有另外一個東西,比如木頭,它也有一個方法叫做burn()
。我們用Rust語言來寫這段代碼,看看在不是面向‘接口’編程的情況下它們是如何燃燒的。
- struct Book {
- title: @str,
- author: @str,
- }
- struct Log {
- wood_type: @str,
- }
很直接。我們創(chuàng)建了兩個結(jié)構(gòu)體來表示一本書(Book)和一個木頭(Log)。下面我們?yōu)榻Y(jié)構(gòu)體實現(xiàn)它們的方法:
- impl Log {
- fn burn(&self) {
- println(fmt!("The %s log is burning!", self.wood_type));
- }
- }
- impl Book {
- fn burn(&self) {
- println(fmt!("The book %s by %s is burning!", self.title, self.author));
- }
- }
現(xiàn)在Log
和 Book
都有了 burn()
方法,讓我們把它們放到火上。
我們首先把木頭放到火上:
- fn start_fire(lg: Log) {
- lg.burn();
- }
- fn main() {
- let lg = Log {
- wood_type: @"Oak",
- length: 1,
- };
- // Burn the oak log!
- start_fire(lg);
- }
非常順利,我們得到了輸出 “The Oak log is burning!”.
現(xiàn)在,因為我們已經(jīng)寫了一個 start_fire
函數(shù),是否我們可以把書也傳進(jìn)去,因為它們都有 burn()
。讓我們試一下:
- fn main() {
- let book = Book {
- title: @"The Brothers Karamazov",
- author: @"Fyodor Dostoevsky",
- };
- // Let's try to burn the book...
- start_fire(book);
- }
可行嗎?不行。出現(xiàn)了下面的錯誤:
mismatched types: expected Log
but found Book
(expected struct Log but
found struct Book)
#p#
說的非常清楚,因為我們寫出的函數(shù)需要的是一個Log
結(jié)構(gòu)體,而不是我們傳進(jìn)去的 Book
結(jié)構(gòu)體。如何解決這個問題?我們可以再寫一個這樣的方法,把參數(shù)改成Book
結(jié)構(gòu)體。然而,這并不是一個好的方案。我在兩個地方有了兩個幾乎一樣的函數(shù),如果一個修改,我們需要記得修改另外一個。
現(xiàn)在讓我們看看面向‘接口’編程如何能解決這個問題。
面向接口編程
我們?nèi)匀皇褂们懊娴慕Y(jié)構(gòu)體,但這次我們加一個接口。在Rust語言里,接口叫做traits:
- struct Book {
- title: @str,
- author: @str,
- }
- struct Log {
- wood_type: @str,
- }
- trait Burnable {
- fn burn(&self);
- }
現(xiàn)在,除了兩個結(jié)構(gòu)體外,我們又多了一個叫做Burnable
的接口。它的定義里只有一個叫做burn()
的方法。我們來為每個結(jié)構(gòu)體實現(xiàn)它們的接口:
- impl Burnable for Log {
- fn burn(&self) {
- println(fmt!("The %s log is burning!", self.wood_type));
- }
- }
- impl Burnable for Book {
- fn burn(&self) {
- println(fmt!("The book \"%s\" by %s is burning!", self.title, self.author));
- }
- }
看起來并沒有多大的變化。這就是面向接口編程的強(qiáng)大之處:
- fn start_fire<T: Burnable>(item: T) {
- item.burn();
- }
不僅僅只能接收一個Book對象或Log對象做參數(shù),我們可以往里面?zhèn)魅肴魏螌崿F(xiàn)了 Burnable
接口的類型(我們叫它類型T
)。這使得我們的主函數(shù)可以寫成這樣:
- fn main() {
- let lg = Log {
- wood_type: @"Oak",
- };
- let book = Book {
- title: @"The Brothers Karamazov",
- author: @"Fyodor Dostoevsky",
- };
- // Burn the oak log!
- start_fire(lg);
- // Burn the book!
- start_fire(book);
- }
正如期望的,我們得到了下面的輸出:
The Oak log is burning!
The book “The Brothers Karamazov” by Fyodor Dostoevsky is burning!
這跟我們期望的完全一致。
結(jié)論
遵循“面向‘接口’編程”原則,我們可以寫出一個函數(shù),使其能完全能復(fù)用任何實現(xiàn)了Burnable
接口的對象。因為很多的程序員都是按小時收費的,我們寫出越多可復(fù)用的代碼,用于維護(hù)它們的時間就會越少,也就是更好。
因此,這是一個非常強(qiáng)大的編程思想。
并不是什么時候都可以面向接口編程的,但遵循這種原則會讓你更容易的寫出可復(fù)用的更優(yōu)雅的代碼。接口提供了非常優(yōu)秀的抽象歸納,讓我們的開發(fā)工作變得容易很多。