打造可維護軟體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 參考
留言
張貼留言