Clean Code: Class 類別

Chapter 10 : Class 類別

  • 類別的結構

    降層法則(stepdown rule)

      public class Test {
          公用靜態常數
          私有靜態變數
          私有實體變數
          應減少使用公用實體變數
          公用函式
          私有函式
          ...    
      }
    

    封裝: 盡量將變數跟函式保持私有型態,如有測試需求才開放成protected。

  • 類別要夠簡短

    類別的命名可以幫忙決定類別的大小,如果無法取個簡明的名稱,則此類別可能過大。

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

    每個小類別「封裝單一職責」、「只有一個修改的理由」以及「與其他少數幾個類別合作來完成系統要求的行為

  • 凝聚性

    類別應該只有少量的實體變數,類別裡的每個方法都應該操作一個或多個這個類別的變數。在方法裡操作越多的變數,代表這個方法更凝聚於該類別。

     

    public class Stack { 
    	private int topOfStack = 0; 
    	List<lnteger> 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; 
            }
        }
    }
    

    你應該保持函式簡短和參數列夠小並試著去分離類別中的變數與方法,讓類別具有更高的凝聚性。

     

  • 保持凝聚性會得到許多小型類別

    當一個大型函式被拆解成許多小函式,通常可分割出更多的類別。

    public class PrintPrimes { 
        public static void main (String[] args) { 
            final int M = 1000; 
            final int RR = 50; 
            final int CC = 4; 
            final int WW = 10; 
            final int ORDMAX = 30; 
            int P[] = new int[M + 1] ; 
            int PAGENUMBER; 
            int PAGEOFFSET; 
            int ROWOFFSET; 
            int C; 
            int J; 
            int K; 
            boolean JPRIME; 
            int ORD; 
            int SQUARE; 
            int N; 
            int MULT[] = new int [ORDMAX + 1]; 
    
            J = 1; 
            K = 1; 
            P[l] = 2; 
            ORD = 2; 
            SQUARE = 9; 
    
    		while (K < M) { 
    			do { 
    				J = J + 2; 
    				if (J == SQUARE) { 
    					ORD = ORD + 1; 
    					SQUARE = P[ORD] * P[0RD]; 
    					MULT[ORD - 1] = J; 
    				}
    				N = 2; 
    				JPRIME = true; 
    				while (N < ORD && JPRIME) { 
                        while (MULT[N] < J) 
    					MULT[N] = MULT[N] + P[N] + P[N]; 
    					if (MULT[N] == J) 
    						JPRIME = false; 
                          N = N + 1; 
    				} 
    			} while (!JPRIME); 
                    K = K + 1; 
                    P[K] = J; 
    			} 
    			{ 
                    PAGENUMBER = 1; 
                    PAGEOFFSET = 1; 
    			    while (PAGEOFFSET <= M) { 
                        System.out.println("The First " + M + " Prime Numbers — Page " + PAGENUMBER); 
                        System.out.printing(""); 
                        for (R0W0FFSET = PAGEOFFSET; R0W0FFSET < PAGEOFFSET + RR; ROWOFFSET++) { 
                            for (C = 0; C < CC; C++) {
                                if (ROWOFFSET + C * RR <= M) 
                                    System.out.format("%10d", P[ROWOFFSET + C * RR]); 
                                System.out.println(""); 
                            }
                    } 
                    System.out.println("\f"); 
                    PAGENUMBER = PAGENUMBER + 1; 
                    PAGEOFFSET = PAGEOFFSET + RR * CC;
                    }
                }   
           }
        }
    }
    
  • 重構(refactored)後程式被拆解成三個不同的職責:

    PrimePrinter類別本身包含了主程式,它的職責是處理執行環境的相關事務,可被喚起的方法有更改時,本類別也會受到影響。

    Listing 10-6 
    PrimePrinter.java 
        
    package literatePrimes; 
    
    public class PrimePrinter { 
        public static void main (String[] args) {
        	final int NUMBER_OF_PRIMES = 1000; 
    		int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES);
    		final int ROWS_PER_PAGE = 50; 
    		final int COLUMNS_PER_PAGE = 4; 
    
    		RowColumnPagePrinter tablePrinter = 
    		new RowColumnPagePrinter(ROWS_PER_PAGE, COLUMNS_PER_PAGE, 
                                     "The First " + NUMBER_OF_PRIMES + " Prime Numbers"); 
    		tablePrinter.print(primes); 
    	}
    }    
    

    RowColumnPagePrinter類別為編排數字並輸出至頁面,如果輸出的編排需更動,這個類別會受到影響。

    Listing 10-7 
    RowColumnPagePrinter.java 
    
    package literatePrimes; 
    import java.io.PrintStream; 
    
    public class RowColumnPagePrinter { 
        private int rowsPerPage; 
        private int columnsPerPage; 
        private int numbersPerPage; 
        private String pageHeader; 
        private PrintStream printStream;
        
    	public RowColumnPagePrinter(int rowsPerPage, int columnsPerPage, String pageHeader) { 
            this.rowsPerPage = rowsPerPage; 
            this.columnsPerPage = columnsPerPage; 
            this.pageHeader = pageHeader; 
            numbersPerPage = rowsPerPage * columnsPerPage; 
            printStream = System.out;
        }
        
        public void print(int data[]) { 
            int pageNumber = 1; 
            for (int firstlndexOnPage = 0; firstlndexOnPage < data.length; 
    			firstlndexOnPage += numbersPerPage) { 
                    int lastlndexOnPage = 
                    Math.min(firstlndexOnPage + numbersPerPage-1, data.length-1); 
                    printPageHeader(pageHeader, pageNumber); 
                    printPage(firstlndexOnPage, lastlndexOnPage, data); 
                    printStream.println("\f"); 
                    pageNumber++; 
    		} 
        } 
        
        private void printPage(int firstlndexOnPage, int lastlndexOnPage, int[] data) { 
            int firstlndexOfLastRowOnPage = firstlndexOnPage + rowsPerPage - 1; 
            for (int firstlndexInRow = firstlndexOnPage; 
                firstlndexInRow <= firstlndexOfLastRowOnPage; 
                firstIndexInRow++) { 
                printRowffirstlndexInRow, lastlndexOnPage, data); 
                printStream.println(""); 
            } 
        } 
        
        private void printRow(int firstlndexInRow, int lastlndexOnPage, int[] data) { 
        for (int column = 0; column < columnsPerPage; column++) { 
            int index = firstlndexInRow + column * rowsPerPage; 
            if (index <= lastlndexOnPage)  
                printStream.format("%10d", data[index]); 
        	} 
    	}
        
        private void printPageHeader(String pageHeader, int pageNumber) { 
            printStream.println(pageHeader + " — Page " + pageNumber); 
            printStream.println(""); 
    	} 
    
        public void setOutput(PrintStream printStream) { 
            this.printStream = printStream; 
        } 
        
    

    PrimeGenerator類別為產生一串質數,並不一定要實體化成物件,當計算質數演算法變更時,這個類別也會改變。

    Listing 10-8
    PrimeGenerator.java
    
    package literatePrimes;
    import java.util.ArrayList;
    
    public class PrimeGenerator {
        private static int[] primes;
        private static ArrayList<Integer> multiplesOfPrimeFactors;
    
        protected static int[] generate(int n) {
            primes = new int[n];
            multiplesOfPrimeFactors = new ArrayList<Integer>();
            set2AsFirstPrime();
            checkOddNumbersForSubsequentPrimes();
            return primes;
        }
    
        private static void set2AsFirstPrime() {
            primes[0] = 2;
            multiplesOfPrimeFactors.add(2);
        }
    
        private static void checkOddNumbersForSubsequentPrimes() {
            int primelndex = 1;
            for (int candidate = 3; primelndex < primes.length; candidate += 2) {
                if (isPrime(candidate))
                    primes[primelndex++] = candidate;
                }
        }
    
        private static boolean isPrime(int candidate) {
            if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
                multiplesOfPrimeFactors.add(candidate);
                return false;
            }
            return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
        }
    
        private static boolean isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
            int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
            int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
            return candidate == leastRelevantMultiple;
        }
    
        private static boolean isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
            for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
                if (isMultipleOfNthPrimeFactor(candidate, n))
                    return false;
            }
            return true; 
        }
    
        private static boolean isMultipleOfNthPrimeFactor(int candidate, int n) {
            return candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
        }
    
        private static int smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
            int multiple = multiplesOfPrimeFactors.get(n);
            while (multiple < candidate)
                multiple += 2 * primes[n];
            multiplesOfPrimeFactors.set(n, multiple);
            return multiple;
        }
    }
    
  • 為了變動而構思組織

    對大部分的系統而言,系統的改變是持續性的,每次的改變都讓我們承受某些風險。我們將組織類別,以減少改變所帶來的風險。

    以下是一個必須被打開來進行修改的類別

    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 preparedlnsert() 
        private String columnList(Column[] columns) 
        private String valuesList(Object[] fields, final Column[] columns) 
        private String selectWithCriteria(String criteria) 
        private String placeholderList(Column[] columns) 
    }
    

    重構為一組封閉的類別,函式會造成其他函式異常的風險趨近於零。當你要新增一個新的update類別時,不需要修改已存在的類別,只需要新增一個UpdateSql類別即可。

    它符合單一職責原則與開放封閉原則,且類別應對擴充具有開放性,但對修改具有封閉性。能在不修改現有程式碼條件下,利用擴充系統方式來併入新功能。

    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 generated()
    }
    
    public class SelectSql extends Sql {
        public SelectSql(String table, Column[] columns)
        @Override public String generated()
    }
    
    public class InsertSql extends Sql {
        public InsertSql(String table, Column[] columns, Object[] fields)
        @Override public String generated()
        private String valuesList(Object[] fields, final Column[] columns)
    }
    
    public class SelectWithCriteriaSql extends Sql {
        public SelectWithCriteriaSql(
            String table, Column[] columns, Criteria criteria)
        @Override public String generated()
    }
    
    public class SelectWithMatchSql extends Sql {
        public SelectWithMatchSql(
            String table, Column[] columns, Column column, String pattern)
        @Override public String generated()
    }
    
    public class FindByKeySql extends Sql {
        public FindByKeySql(String table, Column[] columns,
         String keyColumn, String keyValue)
        @Override public String generated()
    }
    
    public class PreparedlnsertSql extends Sql {
        public PreparedlnsertSql(String table, Column[] columns)
        @Override public String generated() {
            private String placeholderList(Column[] columns)
        }
    }    
    
    public class Where {
        public Where(String criteria)
        public String generated()
    }
    
    public class ColumnList { 
        public ColumnList(Column[] columns) 
        public String generated() 
    }
    
  • 隔離修改

    建立介面,此介面只有一個方法

    public interface StockExchange { 
    	Money currentPrice(String symbol); 
    } 
    

    建立一個Portfolio類別,且於建構子中傳入StockExchange當作參數

    public Portfolio { 
        private StockExchange exchange; 
        public Portfolio(StockExchange exchange) { 
        this.exchange = exchange; 
    } 
    

    撰寫測試程式

    public class PortfolioTest {
        private FixedStockExchangeStub exchange;
        private Portfolio portfolio;
    
        ©Before
        protected void setup() throws Exception {
            exchange = new FixedStockExchangeStub();
            exchange.fix("MSFT", 100);
            portfolio = new Portfolio(exchange);
        }
    
        ©Test
        public void GivenFiveMSFTTotalShouldBe500() throws Exception {
            portfolio.add(5, "MSFT");
            Assert.assertEquals(500, portfolio.value());
        }
    }
    

    利用介面與抽象類別來幫助我們隔離細節所帶來的風險,利用這樣的方式進行耦合最小化,類別即遵守了相依性反向原則(Dependency Inversion Principle, DIP)的類別設計原則,本質上類別應相依於抽象概念,而非相依於具體細節上。

    Ref:https://archive.org/stream/CleanCode_201607/Clean%20Code_djvu.txt