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)

<<:  Day07 React之CSS样式设定

>>:  Day 22-state manipulation 之四:让 terraform 遗忘过去的 state rm

(Day4) 陈述式 /表达式

前言 JavaScript 的语句分成两种 陈述式、表达式,这两种语法区分并不困难,接下来会一一介绍...

Unity自主学习(七):将Unity引擎与Unity Hub做连结

昨天我们通过Unity官方提供的"UnityDownloadAssistant-20xx....

Visited:我最喜欢的Node.js上的开源命令行工具

最近,我偶然发现了一个软件,"Visited",一个建立在Node.js上的开源...

Day 6:AWS是什麽?30天从动漫/影视作品看AWS服务应用 -伊藤计划《和谐》

(不写code,有点不懂但看起来超酷的呈现方式) 先小小悼念一下作者伊藤计划,天才英年早逝,如果仍...

[Day12]程序菜鸟自学C++资料结构演算法 – 树Tree

前言:相信大家对於「树」都不陌生,资料结构中的树其实是模拟现实生活中的树干、树枝和叶子,相当於树状结...