在 java 中,相信很多人曾看過或寫過只有一個實作(implementation) 的介面 (interface)
,並且以 Interface-Impl 的風格命名,如圖:
上圖中的 FooService, BarService 只有一個實作,而且它們都放在同一個 package 或 module 裡,通常只給專案或產品內部使用,並不會當作 library 提供給其它專案參考。不曉得大家有沒有思考過為什麼要這樣寫 ?
據我觀察,可能是有些人將 C++ 的標頭檔 .h 和 實作檔 .cpp 結構的習慣帶進來 ;也有可能是拜 naming convention 或常見的 practice 所賜,習慣成自然;又或者是考量到未來的擴充性,所以就事先定義好 interface;又或者是許多文章提到:物件之間應該盡量依賴於抽象而不是實作。
但是,像這樣只有一個實作的介面
(本文簡稱為 interface-impl) ,其實會對程式品質與開發帶來負面影響。
我認為 interface-impl 這樣的設計是個 Anti Pattern。它會產生幾個問題:
據我觀察,許多人寫出 interface-impl 的理由是:
現在的確只有用 A 方式實作,但搞不好未來會有另一 B 實作
萬一沒有呢 ?
人性使然,人們在面對未知事物時傾向於未雨綢繆,因為能為自己帶來確定感和安心感,但在設計程式時也許要有不同思維。
如果 interface-impl 成對出現時,表示已經違反 YAGNI 原則,原因是,設計時不需為了未來有可能使用的理由就事先建立 interface,但世事難料,往往計畫趕不上變化,它反而很有可能不會有第二個實作,應該是等到當下有明確需求時再建。
如果沒有第二個實作,換言之,實作並沒有被替換的可能,那這種 interface 在用法上、在依賴上與 concrete class 是沒有差異的,表面上是 interface,本質上是個 duplicated type,並不是 interface 該提供的價值,沒有抽象概念,更沒有解耦,也失去了使用 interface 的初衷與目的。
當你定義出 interface-impl 時,當其中一者發生改變時,無論是重構或是任何程式修改,都迫使你需要花費額外的成本去同步另一者,否則 compile 會失敗,但我們不應該將同樣的事情再重複做一次 (DRY 原則)。如果事先建立 interface 的理由是"以後"會用到,則開發者會不斷地面臨這個問題,直到所謂的"以後"來臨時。
此外,以 java 的 List 以及常見的兩個實作 ArrayList, LinkedList 為例:
這兩個 List 具有不同的實作與特性,透過 interface,我們可以用很少的改動成本,再根據應用情境(例如效能、時間空間複雜度等因素)決定要使用 ArrayList 或 LinkedList,甚至可在 run-time 時才決定,如此也增加了程式的彈性,一個好的 interface 就應如此設計。
但 JDK 中肯定沒有 ListImpl、ArrayListImpl、LinkedListImpl,對吧 ? 因此,我認為 interface-impl 這種設計,不僅沒有講到重點,反而只是換個方式再將 interface 重複講一遍而已 (tautology)。
想像一下,如果一個 interface-impl 真的有第二個 implementation 出現時,你該如何為它命名呢? -impl2 ? 再者,要如何在 java doc 裡清楚地描述 interface 與 -impl,或是只能重複地描述呢?
可能有些人會問:
很多文章或書上介紹 pattern 或 architecture時,常會看到它的 class diagram 有出現 interface 和單一的 impl 阿,但我認為它的表達方式也許只是教學、範例用途而已,並不是真的要你也一起 impl,另外也要考量自身需求,不需硬套用某個 pattern 或 architecture,造成過度設計。
想像一下,專案中有超過 500 個檔案,如果其中包含許多 interface-impl,當你需要一層一層 trace code 時,IDE 會使你無法很流暢地進行,在如此龐大的系統裡遇到這種事,肯定會降低 trace code/debug 的效率,雖然只要是 interface 都會遭遇同樣的問題,但若能減少非必要的干擾,對於開發者的工作效率或心理面是有助益的。
此外,當 interface-impl 出現時,表示檔案的數量是比原本多一倍的,這不僅讓專案更龐大,也隱含的增加了專案的複雜度,因為多了這層非必要的 abstract layer,讓系統變得更不直觀,也會讓開發者永遠好奇眼前的 interface 是否有其他的 implementation。
我認為這種 interface-impl 不應存在,反而使用一般的 concrete class 即可,開發程式不需要過度包裝與設計,Keep it simple, straightforword。可能有些人會認為專案中即使有一些 interface-impl 也無傷大雅,但我認為大問題往往是從小問題引起的,一旦病入膏肓,就算想改也改不動了。因此,稱職的 clean coder 應盡量維持專案的乾淨與健康。
如果是為了寫單元測試,也就是說在 production 裡有一對 interface 與 implementation,而在 test 裡會有另一個 implementation 時,我建議可以使用 mock library,如 java 的 Mocktio,如此就不必特地為了單元測試而建立新的實作,以致於可以刪除這個 interface,使專案保持簡潔。
因此,若開發者當下不確定是否需要一個 interface 時,我的建議是:暫時不要。因為仰賴於現代 IDE 的強大,若等到有明確需要一個 interface 時再進行 extract interface,只要滑鼠點幾下就可以達成,幾乎無成本,隨時都可以 extract interface,下圖是 IntelliJ 的 extract interface:
關於 interface 的正確用法,在我簡單舉例前,先引用一段話:
Programming to an interface, not an implementation
意思是,設計時應專注於程式能提供什麼功能,而不是如何辦到的。
因此,首先描述你的 interface 能提供什麼
功能,例如你有一個提供檔案存取服務的 interface 命名為 FileService ,那它的 implementation 應該要描述如何
存取檔案,例如可能有 DiskService, FtpService, MyMagicService …,而不應該是 FileServiceImpl。
再舉個更簡單的例子,有個 interface 名為 Worker:
public interface Worker {
String getJobTitle();
void work();
}
// bad, it's duplicated
public class WorkerImpl implements Worker {
@Override
public String getJobTitle() {
return "Worker";
}
@Override
public void work() {
doWork();
}
}
// better, more concrete and descriptive
public class Engineer implements Worker {
@Override
public String getJobTitle() {
return "Engineer";
}
@Override
public void work() {
fixSomething();
}
}
public class Teacher implements Worker {
@Override
public String getJobTitle() {
return "Teacher";
}
@Override
public void work() {
teach();
}
}
光是簡短的程式就可以發現 Engeineer, Teacher 都明顯比 WokerImpl 更清楚且有意義,也看到了 Worker interface 帶來的價值。
本文描述了許多人對於 java interface 的誤用,導致這種只有一個實作的介面 interface-impl
成對出現在許多專案中,這並沒有利用 interface 的優點,若開發者只為了盲目追求設計原則,而沒有理解 interface 實際的涵義與價值,否則將會導致許多負面影響,並不是只要有 interface 就等於抽象、解耦,誤用比未用更糟糕。
因此,當你有個 interface 時,它應該會提供給許多 client 並實作,並且能清楚描述實作的細節,開發者也可以輕易的替換不同實作;反之,則表示開發者可能誤用了 interface,或者是根本不需要 interface。也許你可以檢視你的專案是否有類似的情況,試著讓專案更乾淨、簡潔、明確,提高可讀性。
最後,用兩句話總結本文:
關於設計的議題,並沒有絕對的對或錯,以上是我蒐集來的資料與個人經驗分享,如果你有其他想法或回饋,歡迎你提出。
Martin Fowler- InterfaceImplementationPair
Adam Bien - Service s = new ServiceImpl() - Why You Are Doing That?
Ted Kaminski - Why an interface with only one implementation?
Jérôme Loisel - IMPL CLASSES ARE EVIL
Do I need to use an interface when only one class will ever implement it?