什麼是封裝?提升開發速度的重要工具
學習物件導向的過程中,我們會學到封裝的概念,然而大部分人對於封裝的認知都只停留在幫物件增加Getter、Setter,因此本篇文章將會詳細介紹封裝,並且讓大家能夠掌握以下幾點:
- 什麼是封裝?
- 為什麼要封裝?
- 破壞封裝會導致哪些後果?
什麼是封裝?
對於封裝的定義,大家有著不同的解讀,以下為最常見幾種:
- Information Hiding
- Data & Logic Together
事實上真的是這樣嗎?我們先針對上述兩個概念進行簡單介紹。
Information Hiding:將實作細節隱藏,提升模組的聚合性、降低客戶端對其的耦合性。
Data & Logic Together:將資料與邏輯綁定在一起,降低重複程式碼、提升API介面可讀性。
先講結論:不管是Information Hiding或是Data & Logic Together都只是達到封裝的一種手段,不是封裝本身的定義。
Encapsulation:限制客戶端對於物件實作細節的存取,保護物件的狀態完整性(Data integrity)。
相信看了上述的定義,你還是不知道封裝是什麼,因此在這提供一個具體的例子來說明,讓大家對於封裝能有更好的理解。
以下範例相信很多人都不陌生,但還是請你花一分鐘的時間來思考,下面範例是否有被正確的封裝。
public class Order {
private List<Item> items = new ArrayList<>();
public List<Item> getItems() {
return items;
}
public void setItems(List<Item> items) {
this.items = items;
}
}
答案是:完全沒有!
良好的封裝要能夠限制客戶端對於物件實作細節的存取,上述Order物件的field儘管為private,但事實上客戶端還是能透過Getter、Setter存取物件內部狀態。
假設Order物件商業邏輯的限制:Item的數量不能大於5個,上述程式碼有辦法保證此限制嗎?
答案是:完全沒辦法!為什麼?
public static class OrderClient {
public static void main(String[] args) {
List<Item> items = IntStream.range(0, 100) // 100個Item
.mapToObj(i -> new Item())
.collect(Collectors.toList());
Order order = new Order();
order.setItems(items);
order.getItems().add(new Item());
}
}
上述程式碼中,客戶端能在Order物件不知情的情況下,破壞Order的限制 (Item的數量不能大於5個),不管是透過Setter或是Getter。
良好的封裝要能夠保護物件的狀態完整性,上述程式碼明顯沒有達到此目標。
從Order物件我們得知以下幾點:
- Order沒辦法限制客戶端對於狀態的存取
- Order沒辦法保證自身的狀態完整性
如何改善?很簡單,透過Information Hiding與Data & Logic Together來達到封裝的目的。
客戶端需要Order物件提供以下功能:
- 加入新的Item
- 刪除既有的Item
因此Order物件就應該要有兩隻API來達到客戶的期望:
- addItem
- removeItem
上述API能夠達到Data & Logic Together的效果 (新增與刪除的邏輯與List<Item>綁定再一起),並且當Order物件提供客戶期望的API時,我們能夠移除其Getter與Setter,因此Information hiding。
public class Order {
private List<Item> items = new ArrayList<>();
public void addItem(Item item) {
if (items.size() > 5)
throw new RuntimeException("The size of item can't large than five");
this.items.add(item);
}
public void removeItem(Item item) {
if (!items.contains(item))
throw new RuntimeException("Can't remove nonexist item");
items.remove(item);
}
}
為什麼要封裝?
複雜度:
我們先從複雜度談起,怎樣算是太過複雜?判斷的方式很簡單。
當身而為人的你覺得看起來很糟糕、害怕、絕望時,就是太過複雜。
人腦對於處理複雜的能力其實非常有限,透過良好封裝來降低複雜度才是符合人性的開發方式。
有些工程師喜歡挑戰人腦的極限,函數寫的非常複雜,比誰能記住越多實作細節越好,那只是在浪費時間,降低自身與團隊的開發速度。
試著想想在大系統當中,物件可能有數千數萬個,每個物件都像Order範例一樣,有各自商業邏輯的限制 (Business Invariant),如果這些物件沒有良好的封裝,客戶端呼叫物件API時,就必須要非常小心才能避免破壞物件的狀態完整性,一不小心系統就會產生Bug,繼續加班。
上述開發過程的體驗是非常痛苦也不符合人性的,工程師就像伊拉克戰場上的士兵一樣,需要繃緊神經才能避免踩到敵軍的地雷,一不小心就直接回蘇州賣鴨蛋。在這種高壓環境開發,難怪工程師都有心理創傷。
閱讀實作細節:
有開發經驗的工程師都知道,閱讀程式碼的時間遠遠大於修改程式碼的時間,如果物件的封裝良好,我們能夠從函數名稱或是單元測試知道API的使用方式 (什麼!會寫單測試?),就不會為了使用此物件,還需要先了解此物件的內部實作細節,能夠大幅度提高生產力。
以下幾個生活例子,讓大家明白:
- 開車時,不需要了解車子的內部結構
- 餐廳吃飯時,不需要知道餐點怎麼煮出來的
- 用電腦時,不需要了解電腦的內部結構
如果使用產品時,消費者需要完全理解內部結構才能使用的話,是非常荒謬的事情,可惜套用到軟體開發,當我們要reuse其他工程師開發的物件時,我們卻必須要閱讀其實作細節才能使用 (可憐的工程師)。
破壞封裝會導致哪些後果?
- 永無止盡的複雜度。
- 人腦認知的極限。
- 為了使用物件API,需要閱讀其實作細節,浪費時間。
結論:封裝能夠限制客戶端對於物件實作細節的存取、確保物件的狀態完整性。