面向“介面”程式設計和麵向“實現”程式設計

類別: IT

如果你已經讀了我的前幾篇關於物件導向正規化因為受到Rust and Go等語言的影響而發生變化的文章,看到了我正在研究的Rust設計模式,你會發現我對Rust語言十分的偏愛。

除此之外,就在上週末,我讀完了經典的《設計模式:可複用物件導向軟體的基礎》。這些種種,引起了我對這本書中談及的一個核心原則的思考:

面向‘介面’程式設計,而不是面向‘實現’。

這是什麼意思?

首先我們需要理解什麼是‘介面’,什麼是‘實現’。簡言之,一個介面就是我們要呼叫的一系列方法的集合,有物件將會響應這些方法呼叫。

一個實現就是為介面存放程式碼和邏輯的地方。

本質上講,這個原則倡導的是,當我們寫一個函式或一個方法時,我們應該引用相應的介面,而不是具體的實現類。

面向‘實現’程式設計

首先我們看看,如果不遵循這個原則會發生什麼。

假設你是《華氏451度》這本書裡的“Montag”這個人。大家都知道,書在華氏451度會燒著的。小說中的消防隊員只要看到了書就會把它們丟到火裡。我們用物件導向的視角說問題,書有一個叫做burn()的方法。

書並不是唯一會燃燒的東西。假設我們還有另外一個東西,比如木頭,它也有一個方法叫做burn()。我們用Rust語言來寫這段程式碼,看看在不是面向‘介面’程式設計的情況下它們是如何燃燒的。

struct Book {    title: @str,    author: @str,}struct Log {    wood_type: @str,}

很直接。我們建立了兩個結構體來表示一本書(Book)和一個木頭(Log)。下面我們為結構體實現它們的方法:

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));    }}

現在LogBook 都有了 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!”.

現在,因為我們已經寫了一個 start_fire 函式,是否我們可以把書也傳進去,因為它們都有 burn()。讓我們試一下:

fn main() {    let book = Book {        title: @"The Brothers Karamazov",        author: @"Fyodor Dostoevsky",    };    // Let's try to burn the book...    start_fire(book);}

可行嗎?不行。出現了下面的錯誤:

mismatched types: expected Log but found Book (expected struct Log but
found struct Book)

說的非常清楚,因為我們寫出的函式需要的是一個Log結構體,而不是我們傳進去的 Book 結構體。如何解決這個問題?我們可以再寫一個這樣的方法,把引數改成Book結構體。然而,這並不是一個好的方案。我在兩個地方有了兩個幾乎一樣的函式,如果一個修改,我們需要記得修改另外一個。

現在讓我們看看面向‘介面’程式設計如何能解決這個問題。

面向介面程式設計

我們仍然使用前面的結構體,但這次我們加一個介面。在Rust語言裡,介面叫做traits

struct Book {    title: @str,    author: @str,}struct Log {    wood_type: @str,}trait Burnable {    fn burn(&self);}

現在,除了兩個結構體外,我們又多了一個叫做Burnable的介面。它的定義裡只有一個叫做burn()的方法。我們來為每個結構體實現它們的介面:

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));    }}

看起來並沒有多大的變化。這就是面向介面程式設計的強大之處:

fn start_fire<T: Burnable>(item: T) {    item.burn();}

不僅僅只能接收一個Book物件或Log物件做引數,我們可以往裡面傳入任何實現了 Burnable 介面的型別(我們叫它型別T)。這使得我們的主函式可以寫成這樣:

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!

這跟我們期望的完全一致。

結論

遵循“面向‘介面’程式設計”原則,我們可以寫出一個函式,使其能完全能複用任何實現了Burnable介面的物件。因為很多的程式設計師都是按小時收費的,我們寫出越多可複用的程式碼,用於維護它們的時間就會越少,也就是更好。

因此,這是一個非常強大的程式設計思想。

並不是什麼時候都可以面向介面程式設計的,但遵循這種原則會讓你更容易的寫出可複用的更優雅的程式碼。介面提供了非常優秀的抽象歸納,讓我們的開發工作變得容易很多。

面向“介面”程式設計和麵向“實現”程式設計原文請看這裡

推薦文章