『AI時代的優美程式設計教材』目次
本章將明確定義程式設計的根本概念「美麗的源碼」,並學習編寫高品質代碼的基本原則,這是從單純能運行的代碼轉變為易於擴展和維護的美麗代碼的重要知識。
許多程式設計師容易陷入「能運行就可以」的思維。然而,軟體開發的現實是,從最初的實作開始,就進入到「持續擴展與維護的作業」中。閱讀本章將提供解決您六個月後無法理解自己代碼的根本途徑。此處包含了理解程式設計的根本價值及避免技術負債的必要基礎知識。
在此,我們為美麗的源碼定義如下。
「美麗的源碼是指易於擴展和維護的代碼」
這一定義反映了軟體開發的現實。因為程式設計並不僅僅是創造一次能運行的東西,而是「進行持續擴展與維護的作業」。
「源碼是否美觀並不重要,程式能運行就好。」
這樣的說法曾經被提及。
然而,網絡可動作性應該是理所當然的前提。
「源碼雖然骯髒卻能運行的程式」對比於「源碼美觀卻無法運行的程式」
這並非我們想要的比較。
「源碼雖然骯髒卻能運行的程式」對比於「源碼美觀且能運行的程式」
這才是我們應該的比較。
而「程式運行」,並非只是一次性運行全過程,它需要從創建開始,直到使用者不再需要,可能在過程中增加功能,持續運行。只有能夠持續運行的程式,才能稱作真正運行的程式。
程式設計是從寫下第一行代碼開始,隨後進行「持續擴展和維護的作業」。開發是逐步增量進行的。在很多情況下,滿足最初的規格並不意味著開發結束,隨著需求的增減,擴展與維護仍然會不斷進行。
開發必須是可持續的(sustainable),因此,美麗的源碼(=易於擴展與維護的源碼)變得至關重要。
而為了能夠輕易擴展和維護,需要具備以下特性:
美麗的源碼對軟體的非功能性品質影響巨大。具體提高以下品質屬性:
這一分類的重要性在於正確理解美麗代碼的價值。美麗代碼並不直接提升功能,但能顯著提高開發效率和可維護性。
品質屬性 | 美麗代碼的影響度 | 具體效果 | 改進範例 |
---|---|---|---|
易理解性 | ★★★ 直接 | 代碼意圖明確 | 適當命名、注釋 |
易更改性 | ★★★ 直接 | 修改與擴展容易 | 單一責任、松耦合 |
可測試性 | ★★★ 直接 | 結構易於測試 | 依賴注入、Mock化 |
可移植性 | ★☆ 間接 | 隔離平台依賴 | 抽象化、介面 |
可重用性 | ★☆ 間接 | 易於元件化 | 模組設計、松耦合 |
可靠性 | ★☆☆ 限定 | 降低錯誤混入風險 | 明確邏輯、測試 |
可用性 | ★☆☆ 限定 | 提升API易用性 | 直觀介面 |
效率性 | ★☆☆ 限定 | 可讀性與性能的權衡 | 適當算法選擇 |
開發階段 | 美麗代碼 | 醜陋代碼 |
---|---|---|
功能添加 | 順利 | 漸漸困難 |
錯誤修復 | 原因辨識容易 | 調查需時 |
規範變更 | 影響範圍明確 | 風險高 |
團隊參與 | 易於理解 | 學習成本高 |
整體來看 | 長期低成本 | 技術負債累積 |
對於「能運行就好」的觀點,美麗代碼的經濟優位顯而易見:
在實際項目中,維護階段佔開發期間的最大部分,因此美麗代碼的價值隨時間增長。
「美麗源碼的七項原則」是一系列為了撰寫美麗代碼提供的實用指導原則。這些原則彼此相互關聯,通過綜合應用可以達到最大的效用。
C#
return a + b;
與
var sum = a + b;
return sum;
嚴格意義上,兩者表達的意圖不同。
前者表達的是「返回 a + b」,而後者表達的是「將 a + b 置為總和,返回總和」,意圖稍有不同。
與人類對話般撰寫
代碼不是對計算機的指令,而是人與人之間的溝通方式。比較以下描述:
C#
// 描述 How - 意圖不明確
for (int i = 0; i < employees.Count; i++)
{
Console.WriteLine(employees[i].Name);
}
// 描述 What - 意圖明確
employees.ForEach(employee => employee.DisplayTo(console));
後者意圖明確地表達為「顯示所有員工到控制台」。此處排除了「意圖以外的噪音」,如循環變量和索引。
自 C# 3.0 以來的功能,使得意圖表達變得更為自然:
C#
// 傳統寫法 (混雜了 How)
List<Employee> result = new List<Employee>();
foreach (Employee employee in employees)
{
if (employee.Department == "Sales")
result.Add(employee);
}
// 聲明式寫法 (明確表示 What)
var salesEmployees = employees.Where(employee => employee.Department == "Sales");
請根據意圖表達重點改進以下代碼:
C#
// 改進前
string result = "";
for (int i = 0; i < names.Length; i++)
{
if (i > 0)
result += ", ";
result += names[i];
}
return result;
解答範例:
C#
// 改進後
return string.Join(", ", names);
滿足這兩個條件稱為 高內聚性(high cohesion)。在高內聚性中,責任應當單一且盡可能不洩漏至其他方面。
羅伯特·C·馬丁提出的單一責任原則定義為「一個類別只能因為一個原因而變更」。要實踐理解,可以參考以下範例:
C#
// 違反單一責任的例子
public class Employee
{
public string Name { get; set; }
public decimal Salary { get; set; }
// 責任1: 薪資計算
public decimal CalculateBonus()
{
return Salary * 0.1m;
}
// 責任2: 數據保存
public void SaveToDatabase()
{
// 數據庫保存處理
}
// 責任3: 生成報告
public string GenerateReport()
{
return $"Employee: {Name}, Salary: {Salary}";
}
}
在這個例子中,Employee 類別擁有三個不同的責任:
這些責任因不同的理由可能會改變,因此應當分開它們:
C#
// 改進後: 將責任分開
public class Employee
{
public string Name { get; set; }
public decimal Salary { get; set; }
}
public class BonusCalculator
{
public decimal Calculate(Employee employee)
{
return employee.Salary * 0.1m;
}
}
public class EmployeeRepository
{
public void Save(Employee employee)
{
// 數據庫保存處理
}
}
public class EmployeeReportGenerator
{
public string Generate(Employee employee)
{
return $"Employee: {employee.Name}, Salary: {employee.Salary}";
}
}
高內聚性是使類內元素密切相關並具共同目的的狀態:
C#
// 高內聚性的例子: 僅包含計算相關的元素
public class TaxCalculator
{
private readonly decimal _taxRate;
public TaxCalculator(decimal taxRate)
{
_taxRate = taxRate;
}
public decimal CalculateIncomeTax(decimal income)
{
return income * _taxRate;
}
public decimal CalculateAfterTaxIncome(decimal income)
{
return income - CalculateIncomeTax(income);
}
}
命名不僅是選擇標識符,它是建模的中心行為。通過命名,可以:
C#
// 不佳的例子: 責任不明確
public class DataProcessor
{
public void Process(List<object> data) { }
}
// 良好的例子: 責任明確
public class CustomerOrderValidator
{
public ValidationResult Validate(CustomerOrder order) { }
}
名稱應該從客戶端(使用者)視角決定:
C#
// 實作人員的視角命名(差的例子)
public class SqlDataReader
{
public DataTable ExecuteQuery(string sql) { }
}
// 使用者的視角命名(好的例子)
public class CustomerRepository
{
public Customer FindById(int customerId) { }
public List<Customer> FindByName(string customerName) { }
}
對使用者而言,重要的是「能做什麼」,而非「如何實作」。
應避免的命名模式:
Customer1
, Customer2
Cust
, Ord
, Mgr
Thing
, Object
, Data
CustomerClass
, OrderList
GetCustomer()
, RetrieveOrder()
, FetchProduct()
C#
// 反模式的例子
public class OrderMgr
{
private List<OrderData> orderList;
public OrderData GetOrder1(int id) { }
public OrderData RetrieveOrder2(string code) { }
}
// 改進後
public class OrderService
{
private readonly List<Order> _orders;
public Order FindById(int orderId) { }
public Order FindByCode(string orderCode) { }
}
Once and Only Once (OAOO) 原則是避免代碼重複的思想。
同一意圖的代碼不應重複,從而使程式更為簡潔。同時避免在實作、擴展和維護過程中重複相同的工作。
C#
// 有重複的糟糕例子
public class OrderCalculator
{
public decimal GetSubtotal(List<OrderItem> items)
{
decimal subtotal = 0;
foreach (var item in items)
{
subtotal += item.Price * item.Quantity;
}
return subtotal;
}
public decimal GetTotal(List<OrderItem> items, decimal taxRate)
{
decimal subtotal = 0;
foreach (var item in items) // 重複
{
subtotal += item.Price * item.Quantity; // 重複
}
return subtotal + (subtotal * taxRate);
}
}
C#
// 排除重複的改進例子
public class OrderCalculator
{
public decimal GetSubtotal(List<OrderItem> items)
{
return items.Sum(item => item.Price * item.Quantity);
}
public decimal GetTotal(List<OrderItem> items, decimal taxRate)
{
var subtotal = GetSubtotal(items);
return subtotal + (subtotal * taxRate);
}
}
在方法內保持描述的抽象度相同至關重要:
C#
// 抽象度混雜的糟糕例子
public void ProcessOrder(Order order)
{
// 高層處理
ValidateOrder(order);
// 低層實作細節混入
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
var command = new SqlCommand("UPDATE Orders SET Status = 'Processed'", connection);
command.ExecuteNonQuery();
}
// 高層處理
SendConfirmationEmail(order);
}
C#
// 抽象度一致的改進例子
public void ProcessOrder(Order order)
{
ValidateOrder(order);
UpdateOrderStatus(order, OrderStatus.Processed);
SendConfirmationEmail(order);
}
private void UpdateOrderStatus(Order order, OrderStatus status)
{
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
var command = new SqlCommand("UPDATE Orders SET Status = @status WHERE Id = @id", connection);
command.Parameters.AddWithValue("@status", status.ToString());
command.Parameters.AddWithValue("@id", order.Id);
command.ExecuteNonQuery();
}
}
方法應以人類自然思考的粒度撰寫:
C#
// 自然的粒度範例
public class CustomerService
{
public void RegisterNewCustomer(CustomerInfo info)
{
ValidateCustomerInfo(info);
var customer = CreateCustomer(info);
SaveCustomer(customer);
SendWelcomeEmail(customer);
}
private void ValidateCustomerInfo(CustomerInfo info)
{
if (string.IsNullOrEmpty(info.Email))
throw new ArgumentException("Email is required");
if (string.IsNullOrEmpty(info.Name))
throw new ArgumentException("Name is required");
}
// 其他方法的實作...
}
通常來說,簡單易懂的表達數量應少於九個。
「接下來我會講20件重要的事情」
「第一件是……」
「第二件是……」
……
「第二十件是……」
過多的解釋會使人感到複雜。
「接下來我會講三件重要的事情」
反而讓人更易於接受。
方法也應該越短越易於理解。
例如,句子如果少於九個則簡單明瞭。
在代碼基礎中應用一致的規則會帶來:
C#
// 不一致的例子(糟糕)
public class InconsistentNaming
{
public string getUserName() { } // camelCase
public string GetUserEmail() { } // PascalCase
public string get_user_phone() { } // snake_case
}
// 一致的例子(良好)
public class ConsistentNaming
{
public string GetUserName() { } // 統一的 PascalCase
public string GetUserEmail() { }
public string GetUserPhone() { }
}
設計需能驗證代碼的正確性是相當重要的:
C#
// 不易測試的例子
public class OrderProcessor
{
public void ProcessOrder(int orderId)
{
// 直接依賴於數據庫
using (var connection = new SqlConnection("..."))
{
// 複雜的處理聚集在一個方法中
// 直接訪問外部系統
// 直接依賴當前時間
var now = DateTime.Now;
// ...
}
}
}
C#
// 容易測試的例子
public class OrderProcessor
{
private readonly IOrderRepository _orderRepository;
private readonly IEmailService _emailService;
private readonly ITimeProvider _timeProvider;
public OrderProcessor(IOrderRepository orderRepository, IEmailService emailService, ITimeProvider timeProvider)
{
_orderRepository = orderRepository;
_emailService = emailService;
_timeProvider = timeProvider;
}
public OrderProcessingResult ProcessOrder(int orderId)
{
var order = _orderRepository.GetById(orderId);
var processedAt = _timeProvider.Now;
var result = ValidateAndProcessOrder(order, processedAt);
if (result.IsSuccess)
{
_emailService.SendConfirmation(order.CustomerEmail);
}
return result;
}
internal OrderProcessingResult ValidateAndProcessOrder(Order order, DateTime processedAt)
{
// 實現業務邏輯
// 排除外部依賴的純粹處理
}
}
可測試的設計根本目的在於最大化回饋。快速且頻繁的回饋能提高軟體的品質發展效率。
C#
// 透過類型安全性檢測編譯時錯誤
public class TypeSafeOrder
{
public OrderId Id { get; } // 使用專用類型而非 int
public CustomerId CustomerId { get; } // 防止類型誤用
public TypeSafeOrder(OrderId id, CustomerId customerId)
{
Id = id;
CustomerId = customerId;
}
}
// 編譯時發現錯誤
// var order = new TypeSafeOrder(customerId, orderId); // 編譯錯誤!
C#
[Test]
public void ProcessOrder_ValidOrder_ReturnsSuccess()
{
// Arrange: 準備測試數據
var mockRepository = new Mock<IOrderRepository>();
var mockEmailService = new Mock<IEmailService>();
var mockTimeProvider = new Mock<ITimeProvider>();
var order = new Order { Id = 1, CustomerEmail = "[email protected]" };
mockRepository.Setup(r => r.GetById(1)).Returns(order);
mockTimeProvider.Setup(t => t.Now).Returns(new DateTime(2024, 1, 1));
var processor = new OrderProcessor(mockRepository.Object, mockEmailService.Object, mockTimeProvider.Object);
// Act: 執行測試
var result = processor.ProcessOrder(1);
// Assert: 驗證結果
Assert.IsTrue(result.IsSuccess);
mockEmailService.Verify(e => e.SendConfirmation("[email protected]"), Times.Once);
}
[Test]
public async Task OrderWorkflow_EndToEnd_CompletesSuccessfully()
{
// 測試完整工作流程
var order = await _orderService.CreateOrderAsync(customerRequest);
var payment = await _paymentService.ProcessPaymentAsync(order.Id, paymentInfo);
var shipment = await _shipmentService.CreateShipmentAsync(order.Id);
Assert.IsNotNull(order);
Assert.IsTrue(payment.IsSuccessful);
Assert.IsNotNull(shipment);
}
C#
// 不好的例子: 緩慢的回饋
public class SlowFeedbackService
{
public void ProcessData()
{
// 連接實際數據庫(測試速度慢)
using var connection = new SqlConnection("...");
// 調用外部 API(測試不穩定)
var response = HttpClient.Get("https://external-api.com/data");
// 文件系統訪問(依賴環境)
File.WriteAllText("output.txt", response);
}
}
// 好的例子: 快速的回饋
public class FastFeedbackService
{
private readonly IDataRepository _repository;
private readonly IExternalApiClient _apiClient;
private readonly IFileSystem _fileSystem;
public FastFeedbackService(IDataRepository repository, IExternalApiClient apiClient, IFileSystem fileSystem)
{
_repository = repository;
_apiClient = apiClient;
_fileSystem = fileSystem;
}
public async Task<ProcessingResult> ProcessDataAsync()
{
// 所有依賴都可被模擬
// 測試快速且穩定
var data = await _repository.GetDataAsync();
var apiResponse = await _apiClient.GetDataAsync();
var result = ProcessBusinessLogic(data, apiResponse);
await _fileSystem.WriteTextAsync("output.txt", result.Content);
return result;
}
}
C#
// 測試驅動開發的循環
public class TddExample
{
// 1. Red: 編寫失敗的測試
[Test]
public void CalculateDiscount_VipCustomer_Returns20Percent()
{
var calculator = new DiscountCalculator();
var discount = calculator.CalculateDiscount(CustomerType.Vip, 1000);
Assert.AreEqual(200, discount); // 首次必然失敗
}
// 2. Green: 寫出最小能通的代碼
// 3. Refactor: 改進代碼
}
軟體的品質可以劃分為內部品質與外部品質。
美麗的代碼主要提升內部品質,並且這會隨時間推移促進外部品質的提升。
沃德·卡寧漢提出的「技術負債」是指選擇短期解決方案而導致的未來成本。
C#
// 技術負債的例子: 急就章的結果
public class QuickAndDirtyService
{
public string ProcessData(string input)
{
// 例外處理或輸入驗證不足,需要重構
// 為了暫時運行而寫下的技術負債示例
if (input == null) return "";
if (input.Length == 0) return "";
---
原文出處:https://qiita.com/Fujiwo/items/8b175bd399733ee89fba