什麼是封裝?提升開發速度的重要工具

Z-xuan Hong
6 min readMar 20, 2021

--

學習物件導向的過程中,我們會學到封裝的概念,然而大部分人對於封裝的認知都只停留在幫物件增加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物件提供以下功能:

  1. 加入新的Item
  2. 刪除既有的Item

因此Order物件就應該要有兩隻API來達到客戶的期望:

  1. addItem
  2. 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,需要閱讀其實作細節,浪費時間。

結論:封裝能夠限制客戶端對於物件實作細節的存取、確保物件的狀態完整性。

--

--

Z-xuan Hong
Z-xuan Hong

Written by Z-xuan Hong

熱愛軟體開發、物件導向、自動化測試、框架設計,擅長透過軟工幫助企業達到成本最小化、價值最大化。單元測試企業內訓、導入請私訊信箱:t107598042@ntut.org.tw。

No responses yet