「物件將它們的資料隱藏在抽象層後方,然後將操縱這些資料的函式暴露在外。資料結構則將資料暴露在外,且未提供有意義的函式」
「它們不僅是對立的,且本質上也是互補的」
取自: Clean Code (p.107)
我們看一個簡單的例子:
public interface Vehicle
{
public double GetGallonsOfGasoline();
}
// vs.
public interface Vehicle
{
public double GetPercentFuelRemaining();
}
上述的例子中,後者隱藏了程式的實作細節,利用更為抽象化的詞彙 "PercentFuel (燃料百分比)" 取代 "GallonsOfGasoline (加侖汽油)"
如果從上述例子還無法深刻體會到封裝所帶來的好處,不彷再思考一個問題: 你會喜歡看到手機剩下的電量百分比(%),或是看到毫安培值(mAh)?
再看一個經典例子:
public class 正方形 {
...
}
public class 長方形 {
...
}
public class 圓形 {
...
}
// Procedural Programming
public class 幾何圖形 {
public const double PI = 3.14;
// 求面積
public double getArea(Object shapre) {
if (shape instanceof 正方形) {
var s = (正方形) shape;
return s.side * s.side;
}
else if (shape instanceof 長方形) {
var r = (長方形) shape;
return r.height * r.width;
}
else if (shape instanceof 圓形) {
var c = (圓形) shape;
return PI * c.radius * c.radius;
}
}
}
上述寫法為 結構化(Procedural) 導向的程式設計方式
思考1: 當我們想新增 getPerimeter(Object shape)
函式來求周長時,會影響什麼?
思考2: 當我們新增了一個圖形類別(e.g., 三角形),會影響什麼?
現在我們採用 物件導向(OOP) 的程式設計方式改寫上述例子
public class 正方形 implements 幾何圖形 {
public double getArea();
}
public class 長方形 implements 幾何圖形 {
public double getArea();
}
public class 圓形 implements 幾何圖形 {
public const double PI = 3.14;
public double getArea();
}
// Object-Oriented Programming
public abstract class 幾何圖形 {
public double getArea();
}
這是物件導向的寫法,這裡的 getArea() 方法是多型(Polymorphism)的
思考1: 當我們想新增 getPerimeter(Object shape)
函式來求周長時,會影響什麼?
思考2: 當我們新增了一個圖形類別(e.g., 三角形),會影響什麼?
謹記開頭的定義,資料和物件不僅是對立、更是互補的
「因此,使用物件導向而感到困難的事物,在結構化裡卻比較容易;反之亦然」
「模組不該知道『關於它所操縱物件的內部運作』」
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
很明顯地,outputDir 知道 ctxt 物件含有 options, 而 options 又包含 absolutePath。這裡有太多資訊被函式預先知道了Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
「也代表作者根本不確定,它們是否需要函式或型態的保護」
「最佳的資料結構形式,是一個類別裡只有公用變數,沒有任何函式」
此類資料結構通常稱為 資料傳輸物件 (DTO)
補充: [2] 除了 DTO 之外,還有 BO、DAO、VO...等不同的資料傳輸物件,是很好的解耦方式。但使用上可能要注意過度設計(Over-Design)的議題 (e.g., 前端想多一個 Input 欄位存進 DB,這中間的過程至少需要經過 3 層的資料傳輸物件...)
「優秀的軟體開發者能理解其箇中原因,在不帶有偏頗的情況下,選擇最適合的方法來完成手中的工作」
取自: Clean Code (p.114)
「有時候我們買下第三方軟體套件,或使用開放原始碼套件。不管為了哪種原因,我們都必須將這些外來的程式碼整潔地整合到我們的程式碼中」
取自: Clean Code (p.127)
接下來以 java.util.Map 作為例子,這是一個提供 Hash 資料結構相關功能的介面
假設我們需要將 Sensor (感測器) 存放進 HashMap 的資料結構中,最簡單的寫法如下
// 宣告
Map sensors = new HashMap()
// 其他程式需要存取時...
Sensor s = (Sensor) sensors.get(sensorId);
上述寫法的問題是,客戶端 (Client) 程式必須負責把來自 Map 介面裡的物件,手動轉型成正確的資料型態。這並不是整潔好讀的程式碼
接著我們透過泛型(Generics),改寫成
// 宣告
Map<Sensor> sensors = new HashMap<Sensor>()
// 其他程式需要存取時...
Sensor s = sensors.get(sensorId);
上述寫法的可讀性有顯著改善,但仍然有個問題:Map 介面改變時,系統裡會有很多地方也需要連帶修正
使用 Map 更整潔的作法如下
public class Sensors
{
private Map _sensors = new HashMap()
public Sensor getById(string id)
{
return (Sensor) sensors.get(id);
}
// ...
}
使用者無需關心實作細節是否使用泛型,因為邊界上的介面 (Map) 被封裝了。轉型和型態管理都在 Sensors 類別內部處理了
「避免在公用 API 裡回傳介面,或將介面當作參數傳遞給 API」
「在邊界的程式碼必須能清楚的分割,並定義預期的測試。避免讓我們的程式過度使用第三方軟體的特殊之處。最好是依靠 (Depend) 在你可以控制的程式上,免得最後反倒受它控制」
「只在最少處引用第三方軟體」
取自: Clean Code (p.135)
本章主要在說明該如何與第三方套件解耦,避免第三方軟體的改變影響到本身的系統。相關概念也可以衍生到團隊協作時,透過邊界來切分已知和未知 (Adapter Pattern)。之後在 Clean Architecture 篇會從軟體架構層面探討「邊界」