---
tags: 編程哲學
---
# 打造可維護軟體|編寫可維護程式碼的10項法則 (C#版) 讀書筆記
國際標準 ISO/IEC 25010:2011(本書簡稱作 ISO 250101)將軟體品質劃分為八個特徵 :可維護性、功能可適性、效能、兼容性、可用性、可靠性、安全性及可移植性,而本書聚焦於可維護性。
>記住,你不只是在為自己寫程式,還要考慮到後面接手而又缺乏經驗的開發人員,這樣的思維有助於簡化你的編程解決方案。
## 撰寫簡短的程式碼單元(第2章)
> 簡短的程式碼單元(亦即方法與建構式)比較容易分析、測試和重利用。
> 傻瓜也可寫出電腦能懂的程式碼,但優秀的工程師才能寫出人類能夠理解的程式碼。─ Martin Fowler
程式碼要簡短到多短呢?作者的建議是不要超過15行,以下是一段超過15行的範例。
```csharp=
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();
}
}
}
```
### 重構技巧:提取方法
就是把程式碼拆到不同的Function裡,讓所有的的Function行數都小於15行。
```csharp=
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();
}
}
}
```
拆得更小,有沒有必要拆到這麼小呢?我覺得可以的,只要每個Function 名字都起得好,程式的可讀性會更好,看得人更容易理解,讀超來就像在讀一本故事書。也更容易測試及重覆利用。
```csharp=
public 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();
}
}
}
```
### 重構技巧:以方法物件取代方法
考慮以下超過15行的方法
```csharp=
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);
}
}
}
return board;
}
```
用**提取方法**重構後,SetLink 參數還是很長
```csharp=
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);
}
}
}
return board;
}
```
把方法變成物件
```csharp=
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);
}
}
```
```csharp=
public Board CreateBoard(Square[, ] grid) {
return new BoardCreator(grid).Create();
}
```
>切勿為了最佳化效能而犧牲可維護性,除非有可靠的效能測試能夠證明效能問題確實存在,而且你的效能最佳化措施真的有效果。
## 撰寫簡單的程式碼單元(第3章)
程式碼單元包含越少決策點就越容易分析和測試。
就是少一點If , Case 判斷的語句。
> 每個問題都是由一些小問題組成的。— Martin Fowler
考慮以下程式, case 好長, 少打一個break 就出錯了,
```csharp=
public IList < Color > GetFlagColors(Nationality nationality) {
List < Color > result;
switch (nationality) {
case Nationality.DUTCH:
result = new List < Color > {
Color.Red,
Color.White,
Color.Blue
};
break;
case Nationality.GERMAN:
result = new List < Color > {
Color.Black,
Color.Red,
Color.Yellow
};
break;
case Nationality.BELGIAN:
result = new List < Color > {
Color.Black,
Color.Yellow,
Color.Red
};
break;
case Nationality.FRENCH:
result = new List < Color > {
Color.Blue,
Color.White,
Color.Red
};
break;
case Nationality.ITALIAN:
result = new List < Color > {
Color.Green,
Color.White,
Color.Red
};
break;
case Nationality.UNCLASSIFIED:
default:
result = new List < Color > {
Color.Gray
};
break;
}
return result;
}
```
用 **Dictionary** 取締 **Case**
```csharp=
private static Dictionary < Nationality, IList < Color >> FLAGS = new Dictionary < Nationality, IList < Color >> ();
static FlagFactoryWithMap() {
FLAGS[Nationality.DUTCH] = new List < Color > {
Color.Red,
Color.White,
Color.Blue
};
FLAGS[Nationality.GERMAN] = new List < Color > {
Color.Black,
Color.Red,
Color.Yellow
};
FLAGS[Nationality.BELGIAN] = new List < Color > {
Color.Black,
Color.Yellow,
Color.Red
};
FLAGS[Nationality.FRENCH] = new List < Color > {
Color.Blue,
Color.White,
Color.Red
};
FLAGS[Nationality.ITALIAN] = new List < Color > {
Color.Green,
Color.White,
Color.Red
};
}
public IList < Color > GetFlagColors(Nationality nationality) {
IList < Color > colors = FLAGS[nationality];
return colors ?? new List < Color > {
Color.Gray
};
}
```
更進一步, 把每個旗都物件化
```csharp=
public interface IFlag {
IList < Color > Colors {
get;
}
}
public class DutchFlag: IFlag {
public IList < Color > Colors {
get {
return new List < Color > {
Color.Red,
Color.White,
Color.Blue
};
}
}
}
public class ItalianFlag: IFlag {
public IList < Color > Colors {
get {
return new List < Color > {
Color.Green,
Color.White,
Color.Red
};
}
}
}
private static readonly Dictionary < Nationality, IFlag > FLAGS = new Dictionary < Nationality, IFlag > ();
static FlagFactory() {
FLAGS[Nationality.DUTCH] = new DutchFlag();
FLAGS[Nationality.GERMAN] = new GermanFlag();
FLAGS[Nationality.BELGIAN] = new BelgianFlag();
FLAGS[Nationality.FRENCH] = new FrenchFlag();
FLAGS[Nationality.ITALIAN] = new ItalianFlag();
}
public IList < Color > GetFlagColors(Nationality nationality) {
IFlag flag = FLAGS[nationality];
flag = flag ?? new DefaultFlag();
return flag.Colors;
}
```
考慮以下更複雜的程式碼, 好多 if else 分支
```csharp=
public static int CalculateDepth(BinaryTreeNode < int > t, int n) {
int depth = 0;
if (t.Value == n) {
return depth;
} else {
if (n < t.Value) {
BinaryTreeNode < int > left = t.Left;
if (left == null) {
throw new TreeException("Value not found in tree!");
} else {
return 1 + CalculateDepth(left, n);
}
} else {
BinaryTreeNode < int > right = t.Right;
if (right == null) {
throw new TreeException("Value not found in tree!");
} else {
return 1 + CalculateDepth(right, n);
}
}
}
}
```
重構如下, 早一點 return , 減少 else,
```csharp=
public static int CalculateDepth(BinaryTreeNode < int > t, int n) {
int depth = 0;
if (t.Value == n) {
return depth;
}
if ((n < t.Value) && (t.Left != null)) {
return 1 + CalculateDepth(t.Left, n);
}
if ((n > t.Value) && (t.Right != null)) {
return 1 + CalculateDepth(t.Right, n);
}
throw new TreeException("Value not found in tree!");
}
```
再進一步, 更容易測試
```csharp=
public static int CalculateDepth(BinaryTreeNode < int > t, int n) {
int depth = 0;
if (t.Value == n) {
return depth;
} else {
return TraverseByValue(t, n);
}
}
private static int TraverseByValue(BinaryTreeNode < int > t, int n) {
BinaryTreeNode < int > childNode = GetChildNode(t, n);
if (childNode == null) {
throw new TreeException("Value not found in tree!");
} else {
return 1 + CalculateDepth(childNode, n);
}
}
private static BinaryTreeNode < int > GetChildNode(BinaryTreeNode < int > t, int n) {
if (n < t.Value) {
return t.Left;
} else {
return t.Right;
}
}
```
## 不撰寫重複的程式碼(第4章)
任何時候都應避免原始碼重複,因為一旦需要修改,就得處理每一份副本。重複程式碼也是遞迴性臭蟲(regression bug)的滋生根源。
## 保持單元介面單純(第5章)
程式碼單元(方法和建構式)包含越少參數,就越容易測試及重利用。
>四處分散的相關資料應該被封裝成一個物件。— Martin Flowler
考慮以下6個參數的方法
```csharp=
/// <summary>
/// 在指定的長方形上繪製方塊
///
/// <param name="square"> 要繪製的方塊 </param>
/// <param name="g"> 要進行繪製的圖形上下文 </param>
/// <param name="x"> 開始繪製的 x 位置 </param>
/// <param name="y"> 開始繪製的 y 位置 </param>
/// <param name="w"> 方塊的寬度(以像素為單位)</param>
/// <param name="h"> 方塊的高度(以像素為單位)</param>
private void Render(Square square, Graphics g, int x, int y, int w, int h) {
square.Sprite.Draw(g, x, y, w, h);
foreach(Unit unit in square.Occupants) {
unit.Sprite.Draw(g, x, y, w, h);
}
}
```
新增**Rectangle**類別, 把參數減到3個
```csharp=
public class Rectangle {
public Point Position {
get;
set;
}
public int Width {
get;
set;
}
public int Height {
get;
set;
}
public Rectangle(Point position, int width, int height) {
this.Position = position;
this.Width = width;
this.Height = height;
}
}
/// <summary>
/// 在指定的長方形上繪製方塊
///
/// <param name="square"> 要繪製的方塊 </param>
/// <param name="g"> 要進行繪製的圖形上下文 </param>
/// <param name="r"> 方塊的位置與尺寸 </param>
private void Render(Square square, Graphics g, Rectangle r) {
Point position = r.Position;
square.Sprite.Draw(g, position.X, position.Y, r.Width, r.Height);
foreach(Unit unit in square.Occupants) {
unit.Sprite.Draw(g, position.X, position.Y, r.Width, r.Height);
}
}
```
整個方法更簡潔
```csharp=
private void Render(Square square, Graphics g, Rectangle r) {
square.Sprite.Draw(g, r);
foreach(Unit unit in square.Occupants) {
unit.Sprite.Draw(g, r);
}
}
```
考慮另一個方法, 參數更多
```csharp=
public void DoBuildAndSendMail(MailMan m, string firstName, string lastName, string division, string subject, MailFont font, string message1, string message2, string message3) {
// 格式化電子郵件地址
string mId = $ "{firstName[0]}.{lastName.Substring(0, 7)}" + $ "@{division.Substring(0, 5)}.compa.ny";
// 根據指定的內容型態與原始訊息進行格式化
MailMessage mMessage = FormatMessage(font, message1 + message2 + message3);
// 發送訊息
m.Send(mId, subject, mMessage);
}
```
相關的參數都整到一個類別去
```csharp=
public void DoBuildAndSendMail(MailMan m, MailAddress mAddress, MailBody mBody) {
// 建構電子郵件
Mail mail = new Mail(mAddress, mBody);
// 發送電子郵件
m.SendMail(mail);
}
public class Mail {
public MailAddress Address {
get;
set;
}
public MailBody Body {
get;
set;
}
public Mail(MailAddress mAddress, MailBody mBody) {
this.Address = mAddress;
this.Body = mBody;
}
}
public class MailBody {
public string Subject {
get;
set;
}
public MailMessage Message {
get;
set;
}
public MailBody(string subject, MailMessage message) {
this.Subject = subject;
this.Message = message;
}
}
public class MailAddress {
public string MsgId {
get;
private set;
}
public MailAddress(string firstName, string lastName, string division) {
this.MsgId = $ "{firstName[0]}.{lastName.Substring(0, 7)}" + $ "@{division.Substring(0, 5)}.compa.ny";
}
}
```
考慮另一個更多參數的方法
```csharp=
public static void DrawBarChart(Graphics g, CategoryItemRendererState state, Rectangle graphArea, CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis, CategoryDataset dataset) {
// ..
}
```
利用多載添加預設值, 現在C# 也可以直接在參數聲明上直接給定預設值, 呼叫的時候可以省略輸入有預設值的參數, 參考[具名和選擇性引數](https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/classes-and-structs/named-and-optional-arguments)
```csharp=
public static void DrawBarChart(Graphics g, CategoryDataset dataset) {
Charts.DrawBarChart(g, CategoryItemRendererState.DEFAULT, new Rectangle(new Point(0, 0), 100, 100), CategoryPlot.DEFAULT, CategoryAxis.DEFAULT, ValueAxis.DEFAULT, dataset);
}
```
更進一步, 使用*流暢介面*, 方法return 當前物件
```csharp=
public class BarChart {
private CategoryItemRendererState state = CategoryItemRendererState.DEFAULT;
private Rectangle graphArea = new Rectangle(new Point(0, 0), 100, 100);
private CategoryPlot plot = CategoryPlot.DEFAULT;
private CategoryAxis domainAxis = CategoryAxis.DEFAULT;
private ValueAxis rangeAxis = ValueAxis.DEFAULT;
private CategoryDataset dataset = CategoryDataset.DEFAULT;
public BarChart Draw(Graphics g) {
// ..
return this;
}
public ValueAxis GetRangeAxis()
{
return rangeAxis;
}
public BarChart SetRangeAxis(ValueAxis rangeAxis) {
this.rangeAxis = rangeAxis;
return this;
}
// 更多 getter 與 setter
}
```
流暢地呼叫
```csharp=
private void ShowMyBarChart() {
Graphics g = this.CreateGraphics();
BarChart b = new BarChart().SetRangeAxis(myValueAxis).SetDataset(myDataset).Draw(g);
}
```
## 不同模組之間的關注點分離(第6章)
鬆散耦合的模組(類別)比較容易修改,並且促成更模組化的系統。
>在既複雜又緊密耦合的系統中,意外無可避免。─ Charles Perrow 的正常意外理論(Normal Accidents)
一開始的時候並沒有太多功能
```csharp=
public class UserService {
public User LoadUser(string userId) {
// ...
}
public bool DoesUserExist(string userId) {
// ...
}
public User ChangeUserInfo(UserInfo userInfo) {
// ...
}
}
```
```csharp=
public class UserController: System.Web.Http.ApiController {
private readonly UserService userService = new UserService();
// ...
public System.Web.Http.IHttpActionResult GetUserById(string id) {
User user = userService.LoadUser(id);
if (user == null) {
return NotFound();
}
return Ok(user);
}
}
```
日子久了, 添加了一些功能
```csharp=
public class UserService {
public User LoadUser(string userId) {
// ...
}
public bool DoesUserExist(string userId) {
// ...
}
public User ChangeUserInfo(UserInfo userInfo) { // ...
}
public List < NotificationType > GetNotificationTypes(User user) {
// ...
}
public void RegisterForNotifications(User user, NotificationType type) {
// ...
}
public void UnregisterForNotifications(User user, NotificationType type) {
// ...
}
}
```
```csharp=
public class NotificationController: System.Web.Http.ApiController {
private readonly UserService userService = new UserService();
// ...
public System.Web.Http.IHttpActionResult Register(string id, string notificationType) {
User user = userService.LoadUser(id);
userService.RegisterForNotifications(user, NotificationType.FromString(notificationType));
return Ok();
}
[System.Web.Http.HttpPost]
[System.Web.Http.ActionName("unregister")]
public System.Web.Http.IHttpActionResult Unregister(string id, string notificationType) {
User user = userService.LoadUser(id);
userService.UnregisterForNotifications(user, NotificationType.FromString(notificationType));
return Ok();
}
}
```
又增加了一些功能
```csharp=
public class UserService {
public User LoadUser(string userId) {
// ...
}
public bool DoesUserExist(string userId) {
// ...
}
public User ChangeUserInfo(UserInfo userInfo) { // ...
}
public List < NotificationType > GetNotificationTypes(User user) {
// ...
}
public void RegisterForNotifications(User user, NotificationType type) {
// ...
}
public void UnregisterForNotifications(User user, NotificationType type) {
// ...
}
public List < User > SearchUsers(UserInfo userInfo) { // ...
}
public void BlockUser(User user) {
// ...
}
public List < User > GetAllBlockedUsers() {
// ...
}
}
```
應該把不相關的功能分開
```csharp=
public class UserNotificationService {
public IList < NotificationType > GetNotificationTypes(User user) {
// ...
}
public void Register(User user, NotificationType type) {
// ...
}
public void Unregister(User user, NotificationType type) { // ...
}
}
public class UserBlockService {
public void BlockUser(User user) {
// ...
}
public IList < User > GetAllBlockedUsers() {
// ...
}
}
public class UserService {
public User LoadUser(string userId) {
// ...
}
public bool DoesUserExist(string userId) {
// ...
}
public User ChangeUserInfo(UserInfo userInfo) {
// ...
}
public IList < User > SearchUsers(UserInfo userInfo) {
// ...
}
}
```
```csharp=
public class DigitalCamera {
public Image TakeSnapshot() {
// ...
}
public void FlashLightOn() {
// ...
}
public void FlashLightOff() {
// ...
}
}
```
### 將特定實作隱藏在介面背後
```csharp=
public class SmartphoneApp {
private static DigitalCamera camera = new DigitalCamera();
public static void Main(string[] args) {
// ...
Image image = camera.TakeSnapshot();
// ...
}
}
```
```csharp=
public class DigitalCamera {
public Image TakeSnapshot() {
// ...
}
public void FlashLightOn() {
// ...
}
public void FlaslLightOff() {
// ...
}
public Image TakePanoramaSnapshot() {
// ...
}
public Video Record() {
// ...
}
public void SetTimer(int seconds) {
// ...
}
public void ZoomIn() {
// ...
}
public void ZoomOut() {
// ...
}
}
```
降低耦合度, 使用了ISimpleDigitalCamera 介面後(line 10), 保證了只用到基本的功能(ISimpleDigitalCamera的 function), 而且就算DigitalCamera 被修改 或者SDK.GetCamera() 返回的是其他Object, SmartphoneApp 也不要修改, 只要返回的object實現了ISimpleDigitalCamera就可以.
```csharp=
public interface ISimpleDigitalCamera {
Image TakeSnapshot();
void FlashLightOn();
void FlashLightOff();
}
public class DigitalCamera: ISimpleDigitalCamera {
// ...
}
public class SmartphoneApp {
private static ISimpleDigitalCamera camera = SDK.GetCamera();
public static void Main(string[] args) {
// ...
Image image = camera.TakeSnapshot();
// ...
}
}
```
## 讓架構元件保持鬆散耦合(第7章)
系統的頂層元件之間越是鬆散耦合,就越容易修改,也會導致更加模組化的系統。
```csharp=
public interface ICloudServerFactory {
ICloudServer LaunchComputeServer();
ICloudServer LaunchDatabaseServer();
ICloudStorage CreateCloudStorage(long sizeGb);
}
```
```csharp=
public class AWSCloudServerFactory: ICloudServerFactory {
public ICloudServer LaunchComputeServer() {
return new AWSComputeServer();
}
public ICloudServer LaunchDatabaseServer() {
return new AWSDatabaseServer();
}
public ICloudStorage CreateCloudStorage(long sizeGb) {
return new AWSCloudStorage(sizeGb);
}
}
public class AzureCloudServerFactory: ICloudServerFactory {
public ICloudServer LaunchComputeServer() {
return new AzureComputeServer();
}
public ICloudServer LaunchDatabaseServer() {
return new AzureDatabaseServer();
}
public ICloudStorage CreateCloudStorage(long sizeGb) {
return new AzureCloudStorage(sizeGb);
}
}
public class ApplicationLauncher {
public static void Main(string[] args) {
ICloudServerFactory factory;
if (args[1].Equals("-azure")) {
factory = new AzureCloudServerFactory();
} else {
factory = new AWSCloudServerFactory();
}
ICloudServer computeServer = factory.LaunchComputeServer();
ICloudServer databaseServer = factory.LaunchDatabaseServer();
}
}
```
```mermaid
flowchart TD
subgraph 設計從分層到分層的單向依賴
使用者介面-->服務層-->業務邏輯層-->資料抽象層-->資料庫分層
end
```
```mermaid
flowchart TD
subgraph 隨著時間,違反依賴原則,直接調用,導致纏結與混亂
使用者介面-->服務層-->業務邏輯層-->資料抽象層-->資料庫分層
使用者介面-->業務邏輯層
服務層-->資料抽象層
使用者介面-->資料抽象層
end
```
## 保持架構元件平衡(第8章)
平衡度良好的架構擁有不多不少的元件、尺寸一致的程式碼單元,以及最大程度的模組化,並且透過關注點分離,讓修改工作變得很容易。
> 建構封裝邊界是設計軟體架構的重要技巧。— George H. Fairbanks,《Just Enough Architecture》
## 保持小規模的程式碼基礎(第9章)
大型系統很難維護,因為更多程式碼需要分析、修改及測試,同時,在大型系統中,每一行程式碼的維護效率也比小型系統來得低。
## 自動化開發與測試(第10章)
自動化測試(亦即,無需人工干預即可執行測試)能夠針對修改是否有效,獲得近乎即時的回饋。手動測試無法形成規模。撰寫乾淨的程式碼(第11章)程式碼基礎存在越多 TODO(待辦事項)、無用程式碼等不相關的遺留物,新加入的團隊成員就越難保持生產力,進而影響維護工作的執行效率。
不同類型的測試需要不同的自動化框架。對單元測試來說,有一些著名的 C# 框架,像是 NUnit(http://nunit.org);對自動化的端到端測試來說,你需要某種模擬使用者輸入並且捕捉輸出的框架。例如,在 Web 開發中相當有名的 Selenium 框架(http://www.seleniumhq.org)。對整合測試來說,這完全依賴你所在的環境及測試的品質特徵。SoapUI(http://www.soapui.org)是聚焦於 web service 和訊息傳遞中間件的整合測試框架,而 Apache JMeter(http://jmeter.apache.org)框架用來測試 C# 應用程式在高負載下的效能。
一般來說,使用 Moq(https://github.com/Moq/moq4)之類的mocking框架最有效率。mocking框架利用 .Net 執行時期環境的特性,自動從普通介面或類別建立 mock 物件,並且提供各種辦法測試 mock 物件的某些方法是否被調用,同時記錄調用引數。有些 mocking 框架還能夠為 mock 物件指定預先定義的行為,讓它們同時具有 stub 和 mock 的特徵。