iT邦幫忙

2021 iThome 鐵人賽

DAY 7
0
Software Development

成為乾淨的開發者吧! Clean Code, Clean Coder, Clean Architecture 導讀之旅系列 第 7

Day 07: 類別、系統、羽化

「在函式裡,我們計算程式行數,來衡量函式的大小;在類別裡,我們使用不同的量測方式,我們計算職責的數量

取自: Clean Code (p.152)

CH10: 類別 (Class)

類別的結構

  • 在 Day 02 我們曾經看過各種語言的 Coding Style & Convention,讓我們再複習一遍
    以 C# 為例:
    https://ithelp.ithome.com.tw/upload/images/20210923/20138643DyE3mo7Hgs.png
    P.S. 類別結構沒有制式規定,整齊一致即可

封裝

  • 盡量讓所有變數或函式都保持 「私有性(Private)」,除非同一個套件裡的測試程式需要呼叫,不得以才令它為 「保護的(Protected)」

類別要夠簡短

  • 應該多短才好? 這邊我們以類別所負責的職責數量作為劃分邏輯
  • 經典例子:
    [1]上帝物件 / 上帝類別 (God Object / God Class)
    public class EmployeeUtils {  
        // 取資料
        public void FetchEmployeeDetails(string employeeId) 
        // 存資料
        public void SaveEmployeeDetails(EmployeeModel employeeDetails)
        // 驗證資料
        public void ValidateEmployeeDetails(EmployeeModel employeeDetails)
        // 輸出資料
        public void ExportEmpDetailsToCSV(EmployeeModel employeDetails)
        // 引入資料
        public void ImportEmpDetailsForDb(EmployeeModel employeeDetails) 
    
        // 員工資料細節
        private class EmployeeModel {  
            public string EmployeeId;
            public string EmployeeName;
            public string EmpplyeeAddress;
            public string EmployeeDesignation;
            public double EmployeeSalary;
        }  
    }  
    
    上述的類別有 5 個職責,有可能導致日後的維護性下降 (最理想的狀況是 1 個)
  • 如果一個類別的名稱愈模稜兩可,就愈有可能擁有太多的職責
    e.g., "Processor"、"Manager" 通常暗示這裡有很多的職責聚集

單一職責原則 (Single Responsibility Principle, SRP)

注意: 不要把它跟「函式只做一件事」搞混!

  • SRP: 一個類別或一個模組應該且只能有一個修改的理由
    確認職責 (修改的理由) 能幫助我們建立更好的抽象概念,我們可以輕易地擷取函式並分離到新的類別
  • 許多人擔心大量的小型類別會讓程式的全貌難以理解
    事實是,由許多小類別組成的系統不會比有著少數大型類別的系統,多出更多移動部位
  • 思考:你喜歡將元件有組織地放在定義良好和有標記的小型抽屜裡,還是用少量的大抽屜將所有東西丟進去?
    一個有著大型、多重目標的類別,會強迫我們了解許多現在不必要了解的事物。良好的系統是由許多小型類別所組成,並與其它少數幾個類別合作

P.S. 關於 SOLID 設計原則在之後介紹 Clean Architecture 時,筆者會再次介紹

凝聚性

  • 類別應該只有少量的變數,而類別裡的每個方法都應該操縱一或更多個變數
  • 一個具凝聚性的類別
    public class Stack 
    {
      private int topOfStack = 0;
      List<Integer> elements = new LinkedList<Integer>();
    
      public int size() {
        return topOfStack;
      }
    
      public void push(int element) {
        topOfStack++;
        elements.add(element);
      }
    
      public int pop() throws PoppedWhenEmpty {
        if (topOfStack == 0)
          throw new PoppedWhenEmpty();
        int element = elements.get(--topOfStack);
        elements.remove(topOfStack);
        return element;
      }
    }
    

上例中只有 Size() 沒有同時使用到類別的 2 個變數,這是一個非常有凝聚力的類別

  • 注意: 保持函式夠簡短和保持參數夠少的策略,有時會導致類別的凝聚性下降
    可以試著去分離類別中的方法和變數,並讓新類別擁有更高的凝聚性

為了變動而構思組織

  • 違反 SRP 的 SQL 類別

    public class Sql {
      public Sql(String table, Column[] columns)
      public String create()
      public String insert(Object[] fields)
      public String selectAll()
      public String findByKey(String keyColumn, String keyValue)
      public String select(Column column, String pattern)
      public String select(Criteria criteria)
      public String preparedInsert()
    
      private String columnList(Column[] columns)
      private String valuesList(Object[] fields, final Column[] columns)
      private String selectWithCriteria(String criteria)
      private String placeholderList(Column[] columns)
    }
    

    以上例來說,當我們想要新增指令 (e.g., Delete)、或者修改某指令的細節,都需要更動到此類別。很明顯地這個類別有超過 1 個以上的修改理由

  • 那麼,何時該做職責拆解?
    想讓系統的每一個類別都符合 SRP 原則並不是一件輕鬆的事,且可能會流於過度設計 (Over-Design)。所以關鍵在於,未來更動或新增 SQL 類別的機會多不多? 若未來須新增 Update 功能,就是一個修補設計的好機會

  • 重構後的 SQL 符合「單一職責原則 (SRP)」 和 「開放封閉原則 (OCP)」

    abstract public class Sql {
       public Sql(String table, Column[] columns)
       abstract public String generate();
    }
    
    public class CreateSql extends Sql {
       public CreateSql(String table, Column[] columns)
       @Override public String generate()
    }
    
    public class SelectSql extends Sql {
       public SelectSql(String table, Column[] columns)
       @Override public String generate()
    }
    
    public class InsertSql extends Sql {
       public InsertSql(String table, Column[] columns, Object[] fields)
       @Override public String generate()
       private String valuesList(Object[] fields, final Column[] columns)
    }
    
    public class SelectWithCriteriaSql extends Sql {
       public SelectWithCriteriaSql(
       String table, Column[] columns, Criteria criteria)
       @Override public String generate()
    }
    
    public class SelectWithMatchSql extends Sql {
       public SelectWithMatchSql(
       String table, Column[] columns, Column column, String pattern)
       @Override public String generate()
    }
    
    public class FindByKeySql extends Sql
       public FindByKeySql(
       String table, Column[] columns, String keyColumn, String keyValue)
       @Override public String generate()
    }
    
    public class PreparedInsertSql extends Sql {
       public PreparedInsertSql(String table, Column[] columns)
       @Override public String generate() {
       private String placeholderList(Column[] columns)
    }
    
    public class Where {
       public Where(String criteria)
       public String generate()
    }
    
    public class ColumnList {
       public ColumnList(Column[] columns)
       public String generate()
    }
    
  • 重構後雖然多了許多程式碼,但我們可以發現每一個小功能的可讀性都大大地上昇了,且函式之間幾乎沒有任何耦合,這也使得測試程式變得更容易撰寫。而當我們想新增指令時,只要新增一個子類別即可,沒有任何既有的程式碼會被更動


CH11: 系統 (Systems)

「整潔的程式碼幫助我們在較低抽象層次上,達成這個目標。在本章中,讓我們來思考該如何在較高的抽象層次,達成整潔的目標」

取自: Clean Code (p.170)

劃分系統的建造和使用

  • 建造使用是非常不同的過程。軟體系統應該透過以「執行邏輯」接管「起始過程」的方式,將所有關注的事分離開來
  • 延遲初始 / 延遲賦值 (LAZY INITIALIZATION / EVALUATION)
    public Service getService() {
       if (service == null)
         service = new MyServiceImpl(...);
       return service;
    }
    
    上例是一個很經典的初始化方式[3],當物件要被使用的前一刻才進行實例化 (Instantiation)。這麼做的好處除了撰寫方便外,也能增進系統效能
  • 然而,延遲賦值違反了 SRP 原則,它讓「建造邏輯」和「執行過程」相依在一起。若想打造一個良好耐用的系統,就不該讓這些方便的手法來破壞程式的模組性

主函式 Main 的劃分

  • 分離「建造邏輯」和「執行過程」模組最簡單的方式就是將所有與建造有關的程式碼都移到 Main 來執行或呼叫。並且在設計系統的其他部份時,可以假設所有的物件都已經被順利 Build 完成
    https://ithelp.ithome.com.tw/upload/images/20211121/20138643yyj7UQjDIL.png
    說明:
    1. 主程式呼叫 Builder 模組
    2. Builder 創造程式執行所需要的物件
    3. Run 應用程式

工廠

  • 有時候我們不得不讓應用程式負責創造物件,這種情況下可以使用[5]抽象工廠 (Abstract Factory) 設計模式
    https://ithelp.ithome.com.tw/upload/images/20211121/20138643QIPMPDqrop.png
    說明:
    1. 我們先定義一個負責創造訂單項目 (LineItem) 的 Interface (抽象工廠)
    2. 主程式實例化 (或 Create) 一個工廠的實作類別 (我們可以依不同需求,抽換不同工廠)
    3. 訂單處理 (OrderProcessing) 系統呼叫抽象工廠的 makeLineItem() 方法來創造訂單項目 (LineItem)

CH12: 羽化 (Emergence)

  • Kent Beck's 簡單設計四守則 (Four Rules of Software Design)
    遵守以下四個守則就能更容易使軟體善用「單一職責原則 (SRP)」及「相依性反向原則 (DIP)」

    1. 執行完所有的測試 => Passes the tests
    2. 表達程式設計師的本意 => Reveals intention (should be easy to understand)
    3. 沒有重複的部分 => No duplication (DRY)
    4. 最小化類別和方法的數量 => Fewest elements

    取自: Clean Code (p.190)

    註: 筆者發現中文書的翻譯與順序和原文有些許出入,附上原文:

    BeckDesignRules


Reference

  1. God Object - A Code Smell
  2. 使人瘋狂的 SOLID 原則:目錄
  3. 延遲初始設定
  4. Clean Code: System 系統
  5. 工廠模式 - 抽象工廠模式 (Abstract Factory Pattern)

上一篇
Day 06: 測試驅動開發 (Test Driven Development)
下一篇
Day 08: 【結語】程式碼的氣味和啟發
系列文
成為乾淨的開發者吧! Clean Code, Clean Coder, Clean Architecture 導讀之旅31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言