打造可維護軟體C#版 第二章 撰寫簡短的程式碼單元

         第二章作者說明簡短程式碼的好處,明顯作者與Clean Code的Bob大叔是同一個門派。我覺得看完第二章最大的收獲是可學到兩個重構技巧「提取方法」和「以方法物件取代方法」。


指導方針:


程式碼單元的長度應該限制在15行以內。


一開始就撰寫不超過15行的程式碼,或者將長單元分解成多個短單元,直到每個單元不超過15行。


簡短的程式碼單元容易理解、測試及重利用。


 


範例中DoGet 函式有太多職責。


 


2.1 動機


 


簡短程式碼單元容易測試


 


簡短程式碼單元容易分析


 


簡短程式碼單元容易重利用


 


2.2 如何使用本指導方針?


 


在撰寫新的程式碼單元時


 


書中用CsPacMan小精靈遊戲作為範例。


 


Start方法檢查遊戲是否已經在進行,如果是就默默重跑;否則更新私有變數inProgress


 


public void Start()


{


   if(inProgress)


   {


       return;


   }


   inProgress = true;


}


在為程式碼單元添加新功能時


public void Start()


{


   if(inProgress)


   {


       return;


   }


   inProgress = true;


 


   //如果玩家掛掉,告知所有觀察者


   if(!IsAnyPlayerAlive())


   {


       foreach(LevelObserver o in observers)


       {


           o.LevelLost();


       }


   }


 


   //如果豆子全部吃光,告知所有觀察者


   if(RemainingPellets() == 0)


   {


      foreach(LevelObserver o in observers)


       {


           o.LevelWon();


       }


   }


}


從範例程式可看出添加新功能之後程式越來越長!


使用重構技巧應用這個指導方針


重構技巧:提取方法


public void Start()


{


   if(inProgress)


   {


       return;


   }


   inProgress = true;


   UpdateObservers();


}


 


private void UpdateObservers()


{


   //如果玩家掛掉,告知所有觀察者


   if(!IsAnyPlayerAlive())


   {


       foreach(LevelObserver o in observers)


       {


           o.LevelLost();


       }


   }


   //如果豆子全部吃光,告知所有觀察者


   if(RemainingPellets() == 0)


   {


      foreach(LevelObserver o in observers)


       {


           o.LevelWon();


       }


   }


}


 


還可以再進一步重構,提取兩個方法:


 


private void UpdateObservers()


{


   UpdateObserversPlayerDied();


   UpdateObserversPelletsEaten();


}


 


private void  UpdateObserversPlayerDied()


{


   if(!IsAnyPlayerAlive())


   {


       foreach(LevelObserver o in observers)


       {


           o.LevelLost();


       }


   }


}


 


private void UpdateObserversPelletsEaten()


{


   if(RemainingPellets() == 0)


   {


      foreach(LevelObserver o in observers)


       {


           o.LevelWon();


       }


   }


}


新方法的名稱可以表達意圖,可以不需要註解。


範例用提取方法Extract Method 的重構技巧


重構技巧:以方法物件取代方法


之前的範例提取的程式碼無使用任何區域變數,也沒有回傳任何值。


有的時候提取的方法有使用區域變數或有回傳值,可使用第二種重構技巧以方法物件取代方法(Replaced Method with Method Object)


假設要重構以下的程式碼


public Board CreateBoard(Square[,] grid)


{


   Debug.Assert(grid!=null);


   


   Board board = new Board(grid);


   int width = board.Width;


   int height = board.Height;


   for(int x = 0; x < width; x++)


   {


        for(int y = 0;y<height;y++)


        {


            Square square = grid[x,y];


            foreach(Direction dir in Direction.Values)


            {


                int dirX = (width + x + dir.DeltaX)% width;


                int dirY = (height + y + dir.DeltaY)% height;


                Square neighbour = grid[dirX, dirY];


                square.Link(neighbour, dir);


            }


        }


   }


}


 


若用「提取方法」重構,會出現參數過多的情況,SetLink函式有7個參數。


 


private void SetLink(Square square, Direction dir, int x, int y, int width, int height, Square[,] grid)


{    


   int dirX = (width + x + dir.DeltaX)% width;


   int dirY = (height + y + dir.DeltaY)% height;


   Square neighbour = grid[dirX, dirY];


   square.Link(neighbour, dir);    


}


 


public Board CreateBoard(Square[,] grid)


{


   Debug.Assert(grid!=null);


   


   Board board = new Board(grid);


   int width = board.Width;


   int height = board.Height;


   for(int x = 0; x < width; x++)


   {


        for(int y = 0;y<height;y++)


        {


            Square square = grid[x,y];


            foreach(Direction dir in Direction.Values)


            {


                SetLink(square, dir, x, y, width, height, grid);


            }


        }


   }


}


 


為了解決這個問題改用「以方法物件取代方法」的技巧建立新類別,接替CreateBoard的角色,重構如下:


 


internal class BoardCreator


{


   private Square[,] grid;


   private Board board;


   private int width;


   private int height;


 


   internal BoardCreator(Square[,] grid)


   {


       Debug.Assert(grid!=null);


       this.grid = grid;


       this.board = new Board(grid);


       this.width = board.Width;


       this.height = board.Height;


   }


 


   internal Board Create()


   {


       for(int x = 0; x < width; x++)


       {


            for(int y = 0;y<height;y++)


            {


                Square square = grid[x,y];


                foreach(Direction dir in Direction.Values)


                {


                    SetLink(square, dir, x, y);


                }


            }


       }


       return this.board;         


   }


 


   private void SetLink(Square square, Direction dir, int x, int y)


   {


       int dirX = (width + x + dir.DeltaX)% width;


       int dirY = (height + y + dir.DeltaY)% height;


       Square neighbour = grid[dirX, dirY];


       square.Link(neighbour, dir);           


   }


}


 


在新類別中,原本CreateBoard方法的三個區域變數(board、width與height)和一個參數grid都轉變為新類別的私有欄位。


 


這些欄位可被新類別的所有方法存取,不需要再被當做參數到處傳遞。


 


最內層for迴圈的4行程式碼現在變成新方法SetLink,只需要四個參數而不是七個參數。


 


原有的CreateBoard方法修改如下:


 


public Board CreateBoard(Square[,] grid)


{


   return new BoardCreator(grid).Create();


}


 


2.3 撰寫簡短單元的常見反對意見


 


反對意見:比較多個程式碼單元不利於效能


 


現代編譯器都有最佳化功能,不會對效能有很大的影響。


 


切勿為了最佳化效能而犧牲可維護性。


 


反對意見:程式碼分散四處難以閱讀


 


人的短期記憶大概只能記住七項事物。


 


為他人也為自己撰寫容易閱讀及理解的程式碼。


 


反對意見:這個指導方針助長不當的格式化


 


團隊針對格式化約定取得共識,保持程式碼單元簡短,並且遵守這些約定。


 


反對意見:這個程式碼單元無法再拆分


 


switch語句在case過多的情況下會出現過長的情況,要用別的方法重構。


 


這段文章提供的範例可以考慮用別種寫法,而不是重構。


 


當重構似乎可行,但缺乏實質意義時,請重新思考系統的架構。


 


反對意見:拆分程式碼單元沒有明顯的好處


 


拆分的好處前文已經有說明,容易測試、容易分析、容易重利用。


 


將程式碼放在簡短的程式碼單元,並且仔細挑選可以描述功能的方法名稱。


 


2.4 參考


 


 


留言

這個網誌中的熱門文章

異世界NTR web版第三章 觀後感 喧賓奪主 ,反派實力過強

泛而不精的我被逐出了勇者隊伍 web第三章 觀後感 菲莉真能打; 露娜超爽der

持有縮小技能的D級冒險者,與聖女結婚並加入勇者團隊 漫畫 01-04 觀後感 大我與小我