iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0

物件將它們的資料隱藏在抽象層後方,然後將操縱這些資料的函式暴露在外。資料結構則將資料暴露在外,且未提供有意義的函式」

「它們不僅是對立的,且本質上也是互補的」

取自: Clean Code (p.107)

CH6: 物件及資料結構

資料抽象化 (Abstraction)

  • 我們看一個簡單的例子:

    public interface Vehicle
    {
        public double GetGallonsOfGasoline();
    }
    
    // vs.
    
    public interface Vehicle
    {
        public double GetPercentFuelRemaining();
    }
    
  • 上述的例子中,後者隱藏了程式的實作細節,利用更為抽象化的詞彙 "PercentFuel (燃料百分比)" 取代 "GallonsOfGasoline (加侖汽油)"

  • 如果從上述例子還無法深刻體會到封裝所帶來的好處,不彷再思考一個問題: 你會喜歡看到手機剩下的電量百分比(%),或是看到毫安培值(mAh)?

資料/物件的反對稱性 (Anti-Symmetry)

  • 再看一個經典例子:

    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., 三角形),會影響什麼?

    • 沒有任何既有函式會受到影響!

謹記開頭的定義,資料和物件不僅是對立、更是互補的

「因此,使用物件導向而感到困難的事物,在結構化裡卻比較容易;反之亦然」

  • 補充
    上述的例子比較了 Procedural-Oriented 和 Object-Oriented 程式設計,類似的概念也可以應用在 Functional Programming 與 OOP 的比較 [1]
    OOP vs. FP

The Law of Demeter

「模組不該知道『關於它所操縱物件的內部運作』」

  • 更詳細地定義:對於一個類別 C 內的方法 f 只能呼叫以下的方法
    1. C
    2. 任何由 f 所產生的物件
    3. 任何當作參數傳遞給 f 的物件
    4. C 類別裡實體變數所持有的物件
    • 白話文: 方法不該呼叫由任何函式所回傳之物件
  • 反面例子
    final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
    
    很明顯地,outputDir 知道 ctxt 物件含有 options, 而 options 又包含 absolutePath。這裡有太多資訊被函式預先知道
    上述的程式碼又稱作 火車事故(Train Wreck) ,應該加以避免一連串的連續呼叫。最好的作法是將此類程式碼分割成下列形式:
    Options opts = ctxt.getOptions();
    File scratchDir = opts.getScratchDir();
    final String outputDir = scratchDir.getAbsolutePath();
    

混合體

  • 作者稱 「一半物件一半資料結構」 的 object 為混合體。千萬避免產生此類混合體!
    它們擁有一些重要函式,又將私有變數公用化(Public)供外部取用。這將使得程式難以添加新函式,同時,也難以增加新資料

    「也代表作者根本不確定,它們是否需要函式或型態的保護」

資料傳輸物件 (Data Transfer Objects, DTO)

「最佳的資料結構形式,是一個類別裡只有公用變數,沒有任何函式」

  • 此類資料結構通常稱為 資料傳輸物件 (DTO)
    補充: [2] 除了 DTO 之外,還有 BO、DAO、VO...等不同的資料傳輸物件,是很好的解耦方式。但使用上可能要注意過度設計(Over-Design)的議題 (e.g., 前端想多一個 Input 欄位存進 DB,這中間的過程至少需要經過 3 層的資料傳輸物件...)

    https://ithelp.ithome.com.tw/upload/images/20211113/20138643SVgxMSwhvg.png

小結

  • 物件與資料結構的適用情境
    • 「彈性的增加新資料型態」 => 物件 (Object)
    • 「彈性的增加新行為」 => 資料結構 (Struct)

「優秀的軟體開發者能理解其箇中原因,在不帶有偏頗的情況下,選擇最適合的方法來完成手中的工作」

取自: Clean Code (p.114)


CH8: 邊界 (Boundary)

「有時候我們買下第三方軟體套件,或使用開放原始碼套件。不管為了哪種原因,我們都必須將這些外來的程式碼整潔地整合到我們的程式碼中」

取自: 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」

Adapter Pattern: 切分「已知」和「未知」

  • 作者曾經是某無線電通訊系統的專案成員之一,裡面有一個子系統 (Transmitter) 尚未被其他協作者設計出來
  • 為避免開發受阻,於是撰寫了通訊的介面,並實作了 FakeTransmitter (類似 Mock Server) 類別,使開發能夠繼續。並透過 TransmitterAdapter 來封裝其他團隊尚未實作好的 Transmitter API
  • 未來,當 API 實作完成或升級時,唯一需要被修改的地方只有 Adapter
    物件配接器模式

小結

「在邊界的程式碼必須能清楚的分割,並定義預期的測試。避免讓我們的程式過度使用第三方軟體的特殊之處。最好是依靠 (Depend) 在可以控制的程式上,免得最後反倒受它控制」

「只在最少處引用第三方軟體」

取自: Clean Code (p.135)

本章主要在說明該如何與第三方套件解耦,避免第三方軟體的改變影響到本身的系統。相關概念也可以衍生到團隊協作時,透過邊界來切分已知和未知 (Adapter Pattern)。之後在 Clean Architecture 篇會從軟體架構層面探討「邊界」


Reference

  1. FP vs OOP | For Dummies
  2. 彻底搞懂DAO,PO,BO,DTO,VO,DO
  3. 一篇文章讲清楚VO,BO,PO,DO,DTO的区别
  4. Adapter
  5. 配接器模式
  6. Clean Code 心得 – Chapter8 邊界

上一篇
Day 04: 函式、錯誤處理
下一篇
Day 06: 測試驅動開發 (Test Driven Development)
系列文
成為乾淨的開發者吧! Clean Code, Clean Coder, Clean Architecture 導讀之旅31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言