什麼是Information hiding ?

Z-xuan Hong
13 min readAug 22, 2021

--

Information hiding相信很多人都聽過,但要把此概念應用在現實專案中的話,還是有許多地方需要注意。

這篇文章用來記錄我對Information hiding的理解 (個人觀點不保證正確),希望能幫助大家達到以下目標:

  • 了解什麼是Information hiding。
  • 了解為什麼Information hiding。
  • 了解為什麼getter、setter違反Information hiding。

什麼是Information hiding?

在介紹Information hiding之前,我們先從模組化說起:良好的模組化是讓軟體好修改、維護的關鍵之一,那麼什麼是模組化?

模組化的定義如下:

將大問題切割成數個子問題,這些子問題會成為數個模組,讓開發者在修改、理解系統時,只需要了解大問題中的小部分問題即可,不會被迫了解系統的全貌。

舉個例子:

  • 當修改商業邏輯時,不需要被迫理解資料庫實作。
  • 當修改商業邏輯時,不需要被迫理解使用者畫面實作。
  • 當修改折扣 (Discount)價錢計算時,不需要考慮訂單 (Order)本身。

為什麼需要模組化?

因為大腦的能力有限,一次處理一件事情能讓大腦更有效率的運作。反之,同時處理太多事情會讓大腦運作效率大幅度降低。

試著想想,當要修改一個功能時,是閱讀50行程式碼比較容易,還是閱讀散落在幾百行之間的50行程式碼比較容易呢?

後者光用想的就覺得累了,這也是我們大腦的運作方式,他不喜歡太複雜的工作,最好一次讓他做一件事情,模組化正好能讓我們一次只關注一件事情 (關注點分離)。

如何切割模組?

在了解模組化的重要性後,那我們要如何切割模組?這跟Information hiding有什麼關係呢?

在了解如何切割模組前,需要了解什麼是Information hiding,其定義如下:

可能變動的設計決策封裝到元件內部,並確保元件的介面不會暴露可能變動的設計決策。

這句話聽起來有點抽象,但如果能理解這句話,就能讓你專案的維護性獲得大幅度的提升。

接下來繼續將要介紹為什麼需要Information hiding

為什麼需要Information hiding?

答案很簡單:確保修改模組時,只需要修改唯一一個地方,且不會影響到其他模組 (Isolate Change into One Place)。

修改模組如果能限縮在一個地方,能大幅度降低專案開發的成本,不僅讓工程師開心、也讓老闆開心,可以說是雙贏的局面。

物件導向與程序式導向的差別,有一部分是兩者對於Information hiding處理有著不同的差異。下面透過程式碼進行說明:

private static class ProcedurePoint {
int x;
int y;

ProcedurePoint(int x, int y) {
this.x = x;
this.y = y;
}
}

private static class FakeObjectPoint {
private int x;
private int y;

FakeObjectPoint(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}
}

private static class ObjectPoint {
private final int x;
private final int y;
ObjectPoint(int x, int y) {
this.x = x;
this.y = y;
}

ObjectPoint right() {
return new ObjectPoint(x + 1, y);
}
}

public static void main(String[] args) {
// procedure code
ProcedurePoint p1 = new ProcedurePoint(10, 10);
p1.x += 1;
// procedure code with setter and getter
FakeObjectPoint p2 = new FakeObjectPoint(10, 10);
int x = p2.getX();
p2.setX(x + 1);
// object oriented code
ObjectPoint p3 = new ObjectPoint(10, 10);
ObjectPoint p4 = p3.right();
}

上述程式碼我們看到三種不同point的實作:

  • ProcedurePoint:對於資料的存取沒有任何限制,跟c語言中的struct一樣。
  • FakeObjectPoint:對於資料的存取有限制 (private),但客戶端可以透過getter、setter來操作其內部結構。
  • ObjectPoint:對於資料的存取有限制 (private),客戶端只能透過message來操作物件,沒辦法知道其內部結構。

哪個範例做得較好呢?在回答此問題之前,我們先進行簡單的分析:

  • ProcedurePoint

應該不用過多的討論,就是典型的c語言寫法,ProcedurePoint本身就是沒有行為的資料結構。所有使用它的客戶端都知道ProcedurePoint的內部結構 (x, y)。

  • FakeObjectPoint

這個案例就比較有趣了,看起來像是物件,但跟第一種範例比較又好像差不多,那麼他是物件還是資料結構呢?

答案:一樣是資料結構!!!

為什麼?他不是有把資料透過private keyword來限制存取?並且提供客戶端getter、setter嗎?不是許多物件導向教學都這樣做嗎?

答案:物件之所以為物件,是因為他能提供一系列的行為 (behavior)從而解決客戶端的問題。物件本身是活的 (active)、資料結構是死的 (inactive),從上述範例來看,他沒有什麼行為,也沒辦法為客戶端解決什麼問題,因此為資料結構。

上述程式碼可以看出,客戶端的用法跟ProcedurePoint沒有太大差別,一樣暴露其內部結構,反而多了更多冗余程式碼 (getter、setter)。

  • ObjectPoint

相信看到現在,大家慢慢有感覺了吧?相較於其他兩個範例,ObjectPoint提供客戶端有用的行為 (right),並且客戶端不知道ObjectPoint的內部結構,因此ObjectPoint Information hiding的效果最好!!!

從上述比較我們得知,物件導向的設計方式相較於程序式導向,能隱藏更多的內部結構、實作細節 (Information hiding)。

假設有300個地方reference到point物件,當point物件需要將x與y的型態修改時,哪種實作方式能有效限制影響範圍?試著想想不同範例所影響的範圍,答案應該非常明顯了。

物件導向封裝內部結構、提供API的特性,能避免資訊散落在多個模組,降低修改時所影響的範圍。反之,程序式導向沒有封裝的特性,會使資訊散落在多個模組,增加修改時所影響的範圍。

簡單來說:封裝的資訊越多,影響的客戶端就越少,維護性就更高。

如何切割模組?

回到正題,如何切割模組?經過上述分析物件導向與程序式導向的差異,我們能得出兩種切割方式:

  • 根據執行的步驟 (procedure)的順序進行切割。
  • 根據可能變動的資訊進行切割。

哪種比較好?答案很簡單:根據可能變動的資訊進行切割。

為什麼?

根據執行的步驟去切割模組,會讓模組的介面暴露內部的設計決策 (資料結構、實作細節、使用套件)

根據可能變動的資訊進行切割 (資料結構、實作細節、使用套件),能讓模組的介面只顯示行為,不會暴露內部的設計決策。

這邊透過簡單的範例來說明兩者之間的差異,讓大家對於兩者的差別有更深的了解。

假設我們要撰寫一個讀取檔案、更新檔案資料、寫入檔案的程式

以下為透過步驟來切割模組:

private static class File {
private String filePath;
private String content;

private File(String filePath, String content) {
this.filePath = filePath;
this.content = content;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}
}

private static class FileReader {
File readFile(String filePath) {
// parse file format
String content = ""; // parsed content ...
return new File(filePath, content);
}
}

private static class FileWriter {
void save(File file) {
// convert file content into file format
// write file into its filePath
}
}

public static void main(String[] args) {
FileReader fileReader = new FileReader();
File file = fileReader.readFile("/file/path");

String content = file.getContent();
String newContent = content += "some update content";
file.setContent(newContent);

FileWriter fileWriter = new FileWriter();
fileWriter.save(file);
}

這種方式有很明顯的缺點:

  • 類別數量過多 (FileReader、FileWriter、File)。
  • 實作細節落在多個類別 (FileReaderFileWriter皆需要parse file format)。
  • 修改內部實作細節,影響範圍很大 (FileReader、FileWriter、File)。
  • 增加客戶端耦合 ,客戶端需要耦合到FileReader、FileWriter、File,才能完成功能。
  • 難以針對客戶端進行單元測試,需要Mock非常多物件 (FileReader、FileWriter、File),降低測試維護性。

附上圖片讓大家更理解這種設計的缺點:

以下為透過封裝可能變動的資訊來切割模組:

private static class ObjectFile {
private String filePath;
private String content;

public ObjectFile(String filePath) {
this.filePath = filePath;
this.content = ""; // parse from file
}

public void append(String content) {
this.content += content;
}

public void save() {
// convert content into file format
// write back to file
}
}

public static void main(String[] args) {
ObjectFile objectFile = new ObjectFile("/file/path");

objectFile.append("update content");

objectFile.save();
}

上述切割方式解決了步驟切割所造成的問題:

  • 大幅度降低類別數量。
  • 實作細節限縮在一個類別 (ObjectFile)。
  • 修改內部實作只會影響ObjectFile類別 (Isolate Change into One Place)。
  • 降低客戶端的耦合。
  • 針對客戶端進行測試很簡單,只需要先使用extract interface在使用Fake Object即可。

測試範例 (只用來示範單元測試,沒有考慮Domain Isolation的問題,介面設計有Leaky Abstraction Code Smell):

private interface Text {
void append(String content);
void save(); // leaky abstraction
}

private static class ObjectFile implements Text {
private String filePath;
private String content;

public ObjectFile(String filePath) {
this.filePath = filePath;
this.content = ""; // parse from file
}

@Override
public void append(String content) {
this.content += content;
}

@Override
public void save() {
// convert content into file format
// write back to file
}
}

private static class Client {
private final Text text;
public Client(Text text) {
this.text = text;
}

public void start() {
this.text.append("update content");
this.text.save();
}
}

private static class FakeText implements Text {
private String content = "";
@Override
public void append(String content) {
this.content += content;
}

@Override
public void save() {

}
}

@Test
void update_content() {
FakeText fakeText = new FakeText();
Client client = new Client(fakeText);

client.start();

assertEquals(fakeText.content, "update content");
}

相信看完篇文章,大家應該能回答以下問題:

  • 什麼是Information hiding?
  • 為什麼要Information hiding?
  • 違反Information hiding會造成什麼後果?
  • 什麼是模組化?
  • 為什麼要模組化?
  • 模組如何切割?
  • 物件導向為什麼維護性較好?
  • 模組化與Information hiding的關係為何?

結論:

  • 透過封裝可能變動的設計決策,能降低修改影響的範圍,提升整體維護性。
  • 透過步驟進行模組化,會導致實作細節散落在多個類別,增加修改影響的範圍,降低整體維護性。

--

--

Z-xuan Hong
Z-xuan Hong

Written by Z-xuan Hong

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

No responses yet