這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。## 介紹(Introduction)最近,在 Slack 上我看過大量關於介面的問題。大多數時候,答案都很有技術性,並都關注了實現的細節。實現(細節)對於調試很有協助,但實現對設計卻毫無協助。當用介面來設計代碼時,行為才是主要需要關注的。在這篇博文中,我希望提供一個不同的思考方式,關於介面,和用他們進行代碼設計。我想讓你停止關注於實現細節,而是關注於介面和具體的資料的關係。## 面向資料設計(Data Oriented Design)我相信寫 Go 代碼,應該用面向資料設計的方法,而不是物件導向。我的面向資料的第一條原則是:如果你不瞭解你要處理的資料,你肯定不懂你要解決的問題。所有你要解決的問題本質上就是資料轉換的問題。有一些輸入,然後你產生輸出。這就是程式要做的事情。每一個你寫的函數都是一個小的資料轉換,(它們只是)為了協助你解決大的資料轉換。因為你要解決的問題就是資料轉換的問題,你寫的演算法要基於具體的資料。具體資料就是你儲存在記憶體中的物理狀態,通過網路發送,寫入檔案並進行基本操作。[機器情緒](https://mechanical-sympathy.blogspot.com/)取決於具體的資料和你允許你的機器做怎樣的資料轉換。對面向資料的一個大的警告是關於如何處理修改。關於面向資料,我的第二條原則是:當資料修改時,你的問題就修改了。當你的問題修改了,那麼你的演算法就要跟著修改。一旦資料修改了,你的演算法就需要修改。這是保證可讀性和效能的最好方式。不幸的是,我們大多數人都被教導建立更多的抽象層,來處理變化。當設計需要修改時,我認為這種方式(建立更多的抽象層)將得不償失。你需要的是允許你的演算法保持精簡,來執行需要的資料轉換。當資料修改時,你需要這樣一種方式,演算法改變了但卻不會導致整個程式碼程式庫的大部分代碼發生級聯變化。這就是使用介面的時候。當你關注介面時,你其實想要關注的是行為。## 具體資料(Concrete Data)因為每種事情都跟具體的資料有關,你應該從具體的資料開始。從具體的類型開始。### 代碼清單 1```go05 type file struct {06 name string07 }```在代碼清單 1 中的第 5 行,關鍵字 `struct` 聲明了一個名為 file 的類型。有了這個具體的型別宣告,你可以建立一個這種類型的值。### 代碼清單 2```go13 func main() {14 var f file```多虧了代碼清單 2 中的第 14 行的聲明,現在你有一個類型為 file,存在記憶體中,被命名為 f 的變數,並引用了具體的資料。這個資料被變數 f 索引,而且可以被操縱。你可以再次使用關鍵字 struct 來定義第二塊具體資料。### 代碼清單 3```go09 type pipe struct {10 name string11 }```在代碼清單 3 中的第 09 行聲明了類型為 `pipe`,並擁有一部分具體的資料。再一次,有了這個類型的聲明,你可以在程式中,建立一個不同的值。### 代碼清單 4```go01 package main0203 import "fmt"0405 type file struct {06 name string07 }0809 type pipe struct {10 name string11 }1213 func main() {14 var f file15 var p pipe1617 fmt.Println(f, p) 18 }```現在,這個程式擁有兩個清晰的具體資料定義,以及對應的一個值。在第 14 行,一個類型為 file 的值被建立,在第 15 行,一個類型為 pipe 的值被建立。為了程式完整,兩個值在第 17 行都被 fmt 包展示出來。## 介面值不是實值型別(Interfaces Are Valueless)你已經用關鍵字 `struct` 定義了你程式需要的值。還有另外一個關鍵字可以用來定義類型。那就是關鍵字 `interface`。### 代碼清單 5```go05 type reader interface {06 read(b []byte) (int, error)07 }```在代碼清單 5 第 05 行,聲明了一個 `interface` 的類型。`interface` 類型跟 `struct` 類型相對應。 `interface` 類型只能聲明一組行為的方法。這意味著 `interface` 類型沒有具體的值。### 代碼清單 6```govar r reader``` 有趣的是你可以聲明一個 `interface` 類型的變數,就像代碼清單 6 中展示的一樣。這非常有趣,因為如果在 `interface` 中沒有具體的值,那麼變數 `r` 似乎就是毫無意義的。`interface` 類型定義以及建立的值是毫無價值的!Boom!大腦爆炸了。這是一個非常重要的概念。你必須明白:- 變數 `r` 不代表任何東西。- 變數 `r` 沒有具體的值。- 變數 `r` 毫無意義。有一個實現細節使得 r 在後台是真實存在的,但從我們的編程模型來看,它卻是不存在的。當你認識到 `interface` 不是實值型別,整個世界就變得清晰可以理解了。### 代碼清單 7```go37 func retrieve(r reader) error {38 data := make([]byte, 100)3940 len, err := r.read(data)41 if err != nil {42 return err43 }4445 fmt.Println(string(data[:len]))46 return nil47 }``` 在代碼清單 7 定義了 `retrieve` 函數,一個我稱之為多態的函數。在我繼續前,先說明一下,多態的定義是按順序的。看下來自於 Basic 的發明人 Tom Kurtz 的定義,這個定義會讓你覺得多態函數是如此的特別。“多態性意味著你寫的一個特定的程式,它的行為會有所不同,而這取決於它所操作的資料。”當我看到這個觀點時,它總讓我驚訝。它的簡潔,卻很好地說明了一點。多態性由具體的資料驅動。具有改變程式碼為能力的是具體的資料。正如我以上所說的,你正在解決的問題是植根於具體的資料。面向資料設計是基於具體資料的。如果你不懂你正在使用的【具體】資料,你就不懂你想要解決的問題。Tom 的觀點已經清楚地表明,具體的資料才是設計實現不同行為(多態性)抽象的關鍵。多麼聰明的觀點。再回到代碼清單 7。我將在下面重複一遍。### 代碼清單 7 - 複製```go37 func retrieve(r reader) error {38 data := make([]byte, 100)3940 len, err := r.read(data)41 if err != nil {42 return err43 }4445 fmt.Println(string(data[:len]))46 return nil47 }```當你讀到第 37 行的 retrieve 函式宣告時,函數似乎在說,傳遞給我一個類型為 reader 的值。但你知道這不可能,因為根本就沒有一個值的類型為 reader。類型為 reader 的值壓根不存在,因為 reader 是一個介面類型。我們都知道介面不是實值型別。那麼函數到底想說什嗎?它想說的是:我會接受任何實現了 reader 介面的具體資料(任何值或者指標)。但它必須實現 reader 介面定義的所有方法。這就是你如何在 Go 中實現多態的方式。retrieve 函數不綁定到單個具體資料,而是綁定到任何實現 reader 介面的具體資料。## 給資料賦予行為(Giving Data Behavior)接下來的問題是,如何給資料賦予行為?這就是方法的用處。方法提供資料的行為機制。一旦資料有了行為方法,就可以實現多態。”多態意味著你寫的一個確定的程式,但他的行為可能不同,而這依賴於它所操作的資料。“在 Go 中,你可以寫函數和方法。選擇方法而不是函數的一個原因是,資料被要求要實現給定介面的方法集。### 代碼清單 8```go05 type reader interface {06 read(b []byte) (int, error)07 }0809 type file struct {10 name string11 }1213 func (file) read(b []byte) (int, error) {14 s := "<rss><channel><title>Going Go</title></channel></rss>"15 copy(b, s)16 return len(s), nil17 }1819 type pipe struct {20 name string21 }2223 func (pipe) read(b []byte) (int, error) {24 s := `{name: "bill", title: "developer"}`25 copy(b, s)26 return len(s), nil27 }```請注意:你可能注意在接收者的方法中的第 13 行和第 23 行,聲明了但沒有給一個變數具體的名字。這其實是慣例,如果這個方法不需要使用接收者的任何資料時就可以不給接收者一個具體的名字。在代碼清單 8,在第 13 行,為類型 file 定義了一個方法,在第23 行,為 pipe 類型定義了一個方法。現在,每種類型都定義了一個名為 read 的方法,它已經實現了 reader 定義的所有方法。由於有了這些方法的定義,接下來我們可以說:“類型 file 和 pipe 現在已經實現了 reader 介面。”我在那段話中所說的每一句都很重要。如果你有看我之前關於值和指標語義的部落格文章,那麼你應該知道資料展現的行為由你正在使用的語義決定的。在這篇文章中我不會再討論這些。這裡有一個連結。[https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html](https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html)一旦這些值,值和指標,實現了這些方法,它們就可以傳遞給多態函數 retrieve。### 代碼清單 9```gopackage mainimport "fmt"type reader interface { read(b []byte) (int, error)}type file struct { name string}func (file) read(b []byte) (int, error) { s := "<rss><channel><title>Going Go</title></channel></rss>" copy(b, s) return len(s), nil}type pipe struct { name string}func (pipe) read(b []byte) (int, error) { s := `{name: "bill", title: "developer"}` copy(b, s) return len(s), nil}func main() { f := file{"data.json"} p := pipe{"cfg_service"} retrieve(f) retrieve(p)}func retrieve(r reader) error { data := make([]byte, 100) len, err := r.read(data) if err != nil { return err } fmt.Println(string(data[:len])) return nil}```代碼清單 9 在 Go 中提供了一個完整的多態執行個體,並很好的說明了介面不是實值型別這個觀點。retrieve 函數可以接受任何實現了 reader 介面的資料,任何值或者指標。這正是你在第 33 行和第 34 行的函數調用中可以看到的情況。現在,你可以看到 Go 中如何?進階別的解耦,而且這種解耦還是非常地確切。你現在完全明白了資料的行為將傳遞為函數的行為。閱讀代碼時,這不再是陌生或無法理解的了。當你接受介面不是實值型別的時候,這一切就都可以說得通。這個函數不是要求 reader 值,因為 reader 值根本不存在。該函數要求的是實現 reader 定義的方法的具體資料。## 介面值的分配(Interface Value Assignments)介面不是實值型別的觀點可以延伸到介面值的分配。看下這些介面類型。### 代碼清單 10```go05 type Reader interface {06 Read()07 }0809 type Writer interface {10 Write()11 }1213 type ReadWriter interface {14 Reader15 Writer16 }```有了這些介面聲明,你可以實現一個實現了所有這三個介面的具體類型。### 代碼清單 11```go18 type system struct{19 Host string20 }2122 func (*system) Read() { /* ... */ }23 func (*system) Write() { /* ... */ }```下面,你可以再一次確認,介面為何不是實值型別。### 代碼清單 12```go25 func main() {26 var rw ReadWriter = &system{"127.0.0.1"}27 var r Reader = rw28 fmt.Println(rw, r)29 }// OUTPUT&{127.0.0.1} &{127.0.0.1}```代碼清單 12 的第 26 行,聲明了一個類型為 ReadWriter,名字為 rw 的變數,並分配了一段具體的資料。具體資料是一個指向 system 的指標。然後在第 27 行中定義了類型為 Reader,名稱為 r 的變數。有一個賦值操作跟這個聲明相關。介面類型為 ReadWriter 的 rw 變數分配給了介面類型為 Reader 的新變數 r。這應該會導致我們暫停一秒,因為變數 rw 和 r 的類型不同。我們知道在 Go 中兩個不同名稱的類型之間不會進行隱式地轉換。但這還跟我們這種情況不一樣。因為這些變數不是具體的實值型別,它們是介面類型。如果我們回到介面不是實值型別的理解上,那麼 rw 和 r 就都不是具體的值。因此,代碼不能將介面值分配給對方。它唯一可以分配的是儲存在介面值中的具體資料。幸虧有介面的型別宣告,編譯器可以驗證一個介面內部的具體資料是否也滿足另外的介面。最後,我們只能處理具體的資料。處理介面值時,我們仍然只能處理儲存在其中的具體資料。當你將介面值傳遞給 fmt 包進行顯示時,請記住具體的資料就是顯示的內容。再一次強調,他是唯一真實的東西。## 結論(Conclusion)我希望這篇文章能給你提供一種思考介面以及如何設計代碼的不同方式的參考。我相信,一旦你擺脫了實現細節,並專註於介面與具體資料之間的關係,那麼事情就會變得更加合理。面向資料的設計是編寫更好的演算法的方式,但要求關注對行為的解耦。介面允許通過調用具體資料的方法來達到行為的解耦。
via: https://www.ardanlabs.com/blog/2018/03/interface-values-are-valueless.html
作者:William Kennedy 譯者:gogeof 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
298 次點擊