🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

第1章 美麗的源碼基本原則

第1章橫幅


『AI時代的優美程式設計教材』目次


章節概要

本章的目的

本章將明確定義程式設計的根本概念「美麗的源碼」,並學習編寫高品質代碼的基本原則,這是從單純能運行的代碼轉變為易於擴展和維護的美麗代碼的重要知識。

本章所學習的內容

  • 美麗源碼的準確定義及其所帶來的經濟價值
  • 對於「美麗源碼的七項原則」的實用指導
  • 品質屬性和美麗代碼之間的關係,特別是對非功能性需求的影響
  • 示範學習階段的「守破離」原則
  • 評估代碼品質的具體方法

為什麼從本章開始

許多程式設計師容易陷入「能運行就可以」的思維。然而,軟體開發的現實是,從最初的實作開始,就進入到「持續擴展與維護的作業」中。閱讀本章將提供解決您六個月後無法理解自己代碼的根本途徑。此處包含了理解程式設計的根本價值及避免技術負債的必要基礎知識。


1.1 美麗的源碼是什麼

1.1.1 美麗源碼的定義

在此,我們為美麗的源碼定義如下。

「美麗的源碼是指易於擴展和維護的代碼」

這一定義反映了軟體開發的現實。因為程式設計並不僅僅是創造一次能運行的東西,而是「進行持續擴展與維護的作業」。

「源碼是否美觀並不重要,程式能運行就好。」

這樣的說法曾經被提及。

然而,網絡可動作性應該是理所當然的前提。

「源碼雖然骯髒卻能運行的程式」對比於「源碼美觀卻無法運行的程式」

這並非我們想要的比較。

「源碼雖然骯髒卻能運行的程式」對比於「源碼美觀且能運行的程式」

這才是我們應該的比較。

而「程式運行」,並非只是一次性運行全過程,它需要從創建開始,直到使用者不再需要,可能在過程中增加功能,持續運行。只有能夠持續運行的程式,才能稱作真正運行的程式。

程式設計是從寫下第一行代碼開始,隨後進行「持續擴展和維護的作業」。開發是逐步增量進行的。在很多情況下,滿足最初的規格並不意味著開發結束,隨著需求的增減,擴展與維護仍然會不斷進行。

開發必須是可持續的(sustainable),因此,美麗的源碼(=易於擴展與維護的源碼)變得至關重要。

而為了能夠輕易擴展和維護,需要具備以下特性:

  • 易於更改
  • 可測試
  • 易於理解

1.1.2 美麗的代碼帶來的品質

美麗的源碼對軟體的非功能性品質影響巨大。具體提高以下品質屬性:

直接提升的品質

  • 易理解性(Understandability): 易於理解
  • 易更改性(Ease of Change): 易於修改與擴展
  • 可測試性(Testability): 容易測試

間接提升的品質

  • 可移植性(portability): 容易移植到其他環境
  • 可重用性(reusability): 元件可重用

直接效果有限的品質

  • 可靠性(reliability)
  • 可用性(usability)
  • 效率(efficiency)

這一分類的重要性在於正確理解美麗代碼的價值。美麗代碼並不直接提升功能,但能顯著提高開發效率和可維護性。

代碼品質屬性與美麗代碼的關係

品質屬性的影響度矩陣
品質屬性 美麗代碼的影響度 具體效果 改進範例
易理解性 ★★★ 直接 代碼意圖明確 適當命名、注釋
易更改性 ★★★ 直接 修改與擴展容易 單一責任、松耦合
可測試性 ★★★ 直接 結構易於測試 依賴注入、Mock化
可移植性 ★☆ 間接 隔離平台依賴 抽象化、介面
可重用性 ★☆ 間接 易於元件化 模組設計、松耦合
可靠性 ★☆☆ 限定 降低錯誤混入風險 明確邏輯、測試
可用性 ★☆☆ 限定 提升API易用性 直觀介面
效率性 ★☆☆ 限定 可讀性與性能的權衡 適當算法選擇
美麗代碼的經濟價值
開發階段 美麗代碼 醜陋代碼
功能添加 順利 漸漸困難
錯誤修復 原因辨識容易 調查需時
規範變更 影響範圍明確 風險高
團隊參與 易於理解 學習成本高
整體來看 長期低成本 技術負債累積

1.1.3 美麗代碼的經濟價值

對於「能運行就好」的觀點,美麗代碼的經濟優位顯而易見:

  1. 降低開發成本: 易懂的代碼能提高開發速度
  2. 降低維護成本: 易於變更的代碼能大幅減少維護工時
  3. 提升品質: 易於測試的代碼可防止錯誤的混入
  4. 避免技術負債: 防止長期開發成本的增加

在實際項目中,維護階段佔開發期間的最大部分,因此美麗代碼的價值隨時間增長。


1.2 美麗源碼的七項原則

美麗源碼的七項原則

「美麗源碼的七項原則」是一系列為了撰寫美麗代碼提供的實用指導原則。這些原則彼此相互關聯,通過綜合應用可以達到最大的效用。

  • 第一條: 表達意圖
  • 第二條: 單一責任原則
  • 第三條: 準確命名
  • 第四條: Once And Only Once
  • 第五條: 準確描述的方法
  • 第六條: 規則一致性
  • 第七條: 可測試性

1.2.1 第一條: 表達意圖

基本原則

  • 意圖已被明確表達
  • 意圖易於理解
  • 意圖以外的描述非常少
  • 描述的是 What (做什麼),而不是 How (怎麼做)
  • 如可,亦應描述 Why (為何這樣做)

實踐要點

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# 的進化與意圖表達

自 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);

1.2.2 第二條: 單一責任原則

基本原則

  • 程式單元執行單一工作
  • 所有相關工作在該程式單元內部完成

滿足這兩個條件稱為 高內聚性(high cohesion)。在高內聚性中,責任應當單一且盡可能不洩漏至其他方面。

深入理解單一責任原則(SRP)

羅伯特·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 類別擁有三個不同的責任:

  1. 薪資計算的邏輯
  2. 數據持久化的方法
  3. 報告生成的格式

這些責任因不同的理由可能會改變,因此應當分開它們:

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);
    }
}

1.2.3 第三條: 準確命名

基本原則

  • 名稱能充分且準確表達其唯一的工作
  • 相同者用相同名稱,異者用不同名稱
  • 不得用已知名稱表達其他含義
  • 不在問題範疇內使用詞彙的變意

命名是建模的核心

命名不僅是選擇標識符,它是建模的中心行為。通過命名,可以:

  1. 確定概念
  2. 明確界限
  3. 限制責任
  4. 表達意圖

C#

// 不佳的例子: 責任不明確
public class DataProcessor
{
    public void Process(List<object> data) { }
}

// 良好的例子: 責任明確
public class CustomerOrderValidator
{
    public ValidationResult Validate(CustomerOrder order) { }
}

服務導向命名(SON)

名稱應該從客戶端(使用者)視角決定:

C#

// 實作人員的視角命名(差的例子)
public class SqlDataReader
{
    public DataTable ExecuteQuery(string sql) { }
}

// 使用者的視角命名(好的例子)
public class CustomerRepository
{
    public Customer FindById(int customerId) { }
    public List<Customer> FindByName(string customerName) { }
}

對使用者而言,重要的是「能做什麼」,而非「如何實作」。

命名的反模式

應避免的命名模式

  1. 附上數字: Customer1, Customer2
  2. 縮寫: Cust, Ord, Mgr
  3. 無意義: Thing, Object, Data
  4. 包含型別名: CustomerClass, OrderList
  5. 缺乏一致性: 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) { }
}

1.2.4 第四條: Once And Only Once

基本原則

  • 同一意圖的代碼不應重複
  • 應可清晰區分與不區分的事物

Once and Only Once (OAOO) 原則的實踐

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);
    }
}

重複的種類

  1. 實作重複:相同代碼存在多個地方
  2. 知識重複:相同的業務規則有多個實作
  3. 結構重複:類似的結構重複出現

1.2.5 第五條: 準確描述的方法

基本原則

  • 方法內部應由相同抽象度的描述組成
  • 方法內部的描述應當以自然的粒度書寫(如自然對話般)
  • 適當的數量(描述不要過多)

抽象度的一致性

在方法內保持描述的抽象度相同至關重要:

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件重要的事情」
「第一件是……」
「第二件是……」
……
「第二十件是……」

過多的解釋會使人感到複雜。

「接下來我會講三件重要的事情」

反而讓人更易於接受。

方法也應該越短越易於理解。
例如,句子如果少於九個則簡單明瞭。


1.2.6 第六條: 規則一致性

基本原則

  • 整體遵循相同的規則

一致性的重要性

在代碼基礎中應用一致的規則會帶來:

  1. 減少學習成本: 新代碼易於理解
  2. 提高可預測性: 在相似情況下可期望類似解決方案
  3. 提高維護性: 統一的模式更易於變更

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() { }
}

應統一的元素

  1. 命名規範: 類名、方法名、變數名
  2. 編碼風格: 縮排、括號位置
  3. 設計模式: 錯誤處理、日誌輸出
  4. 架構模式: 層級結構、依賴關係

1.2.7 第七條: 可測試性

基本原則

  • 能夠清楚知曉是否為正確的描述

設計可測試性

設計需能驗證代碼的正確性是相當重要的:

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)
    {
        // 實現業務邏輯
        // 排除外部依賴的純粹處理
    }
}

最大化回饋

可測試的設計根本目的在於最大化回饋。快速且頻繁的回饋能提高軟體的品質發展效率。

回饋的種類與效果
  1. 編譯時回饋

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);  // 編譯錯誤!
  1. 單元測試回饋

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);
}
  1. 整合測試回饋
[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;
    }
}
實現持續回饋
  1. 自動化測試套件: 每次代碼變更自動執行
  2. 代碼審查: 人工進行品質檢查
  3. 靜態分析工具: 檢測編碼規範或潛在錯誤
  4. 持續集成: 自動化變更的集成與部署

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: 改進代碼
}

1.3 品質屬性與美麗代碼的關係

1.3.1 內部品質與外部品質

軟體的品質可以劃分為內部品質外部品質

外部品質(使用者可見的品質)

  • 功能性: 滿足要求的功能
  • 可靠性: 障礙少,穩定運行
  • 可用性: 易於使用
  • 效率性: 在必要資源下運行
  • 可維護性: 易於變更或修改
  • 可移植性: 能在其他環境運行

內部品質(開發者可見的品質)

  • 易理解性: 代碼能夠易於閱讀和理解
  • 易更改性: 能夠輕易地增加功能或修改
  • 可測試性: 易於進行測試
  • 可重用性: 可以在其他地方重用元件

美麗的代碼主要提升內部品質,並且這會隨時間推移促進外部品質的提升。

1.3.2 技術負債的概念

沃德·卡寧漢提出的「技術負債」是指選擇短期解決方案而導致的未來成本。

C#


// 技術負債的例子: 急就章的結果
public class QuickAndDirtyService
{
    public string ProcessData(string input)
    {
        // 例外處理或輸入驗證不足,需要重構
        // 為了暫時運行而寫下的技術負債示例
        if (input == null) return "";
        if (input.Length == 0) return "";

---

原文出處:https://qiita.com/Fujiwo/items/8b175bd399733ee89fba

精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬9   ❤️4
540
🥈
我愛JS
📝3   💬9   ❤️7
186
🥉
AppleLily
📝1   💬4   ❤️1
67
#4
xxuan
💬1  
3
#5
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付