---
# System prepended metadata

title: 擴充方法與靜態設計：可維護的 C# 實務
tags: [.NET Framework]

---

# 擴充方法與靜態設計：可維護的 C# 實務

## 引發動機 ─ 為何選用自訂擴充方法及靜態設計？
### 說明
擴充方法（Extension Method）讓你在不修改原始型別與不繼承的前提下，為既有型別添加行為；而靜態設計（Static Design）則強調使用 static 類別／方法來承載與物件狀態無關的邏輯。兩者在現代後端架構極為普遍：擴充方法提升 API 可讀性與可發現性（IntelliSense），靜態方法則提供輕量、零配置的公用邏輯。然而，濫用會導致邏輯分散、可測試性下降、在多執行緒中產生共享狀態問題。理解兩者的優缺點與關係，是擴張可讀性與維護成本之間取得平衡的關鍵。

### 重點列表
- 可讀性與發現性：擴充方法讓語意貼近使用端（obj.DoX()），提升 API 探索效率；但過度擴充會稀釋聚合邏輯，使行為四散在多個命名空間。
- 關注點分離：靜態方法適合純函式、工具型邏輯與不可變規則；若含外部狀態或 I/O，應改以介面注入與實例服務承載。
- 可測試性與替身策略：靜態方法難以替換；若必須，包裝為介面或以委派注入（例如 Func<DateTime>）。擴充方法本質是靜態方法，也需同樣考量。
- 效能與記憶體：擴充方法為編譯期綁定的靜態呼叫，幾乎零額外成本；靜態快取可降重複計算，但需小心同步與壽命。
- 團隊一致性：制定命名、命名空間與適用準則（例如只在核心型別提供擴充）可降低維護摩擦。

### 補充範例／常見問答
```csharp
using System;
using System.Collections.Generic;
using System.Linq;

public static class EnumerableExtensions
{
    // 為 IEnumerable<int> 增加一個求中位數的擴充方法
    public static double Median(this IEnumerable<int> source)
    {
        // 簡單防禦：避免 NullReference
        if (source == null) throw new ArgumentNullException(nameof(source));

        // 將序列排序後物化為陣列，避免重複列舉
        var arr = source.OrderBy(x => x).ToArray();

        // 空集合時丟出明確例外，避免回傳錯誤的 0
        if (arr.Length == 0) throw new InvalidOperationException("序列不可為空");

        // 若長度為奇數，回傳中間元素；偶數則回傳中間兩數平均
        int mid = arr.Length / 2;
        return arr.Length % 2 == 1
            ? arr[mid]
            : (arr[mid - 1] + arr[mid]) / 2.0;
    }
}

public static class MathUtil
{
    // 靜態工具方法：不依賴任何外部狀態，適合做成 static
    public static int Clamp(int value, int min, int max)
    {
        // 確保回傳值落在 [min, max] 區間
        if (min > max) throw new ArgumentException("min 不可大於 max");
        return Math.Min(Math.Max(value, min), max);
    }
}

// 範例使用
var data = new List<int> { 5, 1, 9, 2, 8 };
// 擴充方法：語意上貼近「對集合做事」
double m = data.Median();
// 靜態方法：語意上貼近「純運算工具」
int c = MathUtil.Clamp(120, 0, 100);
```

- 問：擴充方法會比較慢嗎？
- 答：不會。編譯器會將擴充方法轉為對對應 static 方法的呼叫，成本等同靜態呼叫，幾乎可忽略。真正的效能差異往往來自額外列舉、裝箱或不必要的配置，與「是否擴充」無關。

---

## 設計準則與使用時機 ─ 擴充方法與靜態設計的適用場景
### 說明
擴充方法在「強化使用者語意」與「封裝重複樣板」時特別有價值；靜態設計則擅長「純計算」、「跨域共用常值」與「零狀態協助」。決策時需兼顧效能、可測試性、多執行緒安全、API 發現性，以及團隊風格一致性。以下以決策樹與範例，幫助你判斷何時該用擴充、靜態或實例服務。

### 重點列表
- 決策樹（簡化版）：
  - 邏輯是否依賴可變狀態或 I/O？
    - 是：優先使用實例服務（可注入），必要時用介面隔離。
    - 否：是否屬於某型別的自然行為？是→擴充方法；否→靜態工具。
- 可測試性：若需替換外部依賴（時間、隨機數、環境），避免靜態直接呼叫，改用介面或委派注入。
- 效能與配置：熱路徑避免多餘物件建立與重複列舉；偏向純函式與不可變資料結構。
- API 發現性：對外開放的 SDK 或常用核心型別，優先擴充以提升 IntelliSense 探索；內部工具偏向靜態。
- 命名空間與衝突：擴充方法以「[Type]Extensions」命名並放入明確命名空間，降低同名方法衝突。

### 補充範例／常見問答
```csharp
using System;

public interface IClock
{
    // 可替換的時間來源，利於測試
    DateTime UtcNow { get; }
}

public sealed class SystemClock : IClock
{
    // 真實環境使用系統時間
    public DateTime UtcNow => DateTime.UtcNow;
}

public static class DateTimeExtensions
{
    // 擴充方法：判斷是否為工作時間（簡化示意）
    public static bool IsBusinessHour(this DateTime dt)
    {
        // 平日的 9~18 視為工作時段
        var isWeekday = dt.DayOfWeek != DayOfWeek.Saturday && dt.DayOfWeek != DayOfWeek.Sunday;
        var isOfficeHour = dt.Hour >= 9 && dt.Hour < 18;
        return isWeekday && isOfficeHour;
    }
}

public static class Retry
{
    // 靜態純函式：重試邏輯，不依附於任何狀態
    public static T Execute<T>(Func<T> action, int retries)
    {
        // 簡化版重試：遇例外則重試，直到次數用完
        Exception last = null;
        for (int i = 0; i <= retries; i++)
        {
            try { return action(); }
            catch (Exception ex) { last = ex; }
        }
        throw last!;
    }
}

public sealed class TokenService
{
    // 以介面注入避免直接依賴 DateTime.UtcNow
    private readonly IClock _clock;
    public TokenService(IClock clock) => _clock = clock;

    public bool IsTokenExpired(DateTime issuedAtUtc, TimeSpan ttl)
    {
        // 測試時可用 FakeClock 固定時間，測試邏輯更穩定
        return _clock.UtcNow - issuedAtUtc >= ttl;
    }
}
```

- 問：為何不直接在 TokenService 裡呼叫 DateTime.UtcNow？
- 答：那會降低可測試性。把時間抽象成 IClock，可在單元測試中注入固定時間，避免脆弱測試。真正需要「靜態」的，應是純計算與與外界無關的邏輯。

---

## 技術實踐 ─ 實戰案例與實現步驟
### 說明
在既有系統中導入擴充方法與調整靜態設計，核心是「逐步收斂分散邏輯」、「引入可替代點」、「避免破壞既有行為」。以下以微服務 API 正規化為例，展示重構步驟與整合策略。

### 重點列表
- 盤點與分群：找出重複片段（字串清理、驗證、時間計算），標記可純化為擴充／靜態工具的候選。
- 設計替代界面：對外部依賴（時間、隨機、I/O）先拉出介面與預設實作。
- 小步重構：以路由／模組為單位替換呼叫點，加入回歸測試確保等價行為。
- 提升發現性：將常用操作改為擴充方法，並規範命名空間以利 IntelliSense。
- 觀測與回饋：以 A/B 或灰度釋出，搭配指標（延遲、錯誤率）驗證重構效果。

### 補充範例／常見問答
重構前：控制器充滿樣板與分散工具呼叫
```csharp
// 重構前：字串清理與驗證分散各處，測試難、可讀性差
public class UsersController
{
    public string NormalizeName(string name)
    {
        if (name == null) return "";
        name = name.Trim();
        name = name.Replace("\t", " ");
        name = System.Text.RegularExpressions.Regex.Replace(name, @"\s+", " ");
        return name;
    }
}
```

重構後：以擴充方法集中規範；以靜態常值定義規則
```csharp
using System.Text.RegularExpressions;

public static class StringExtensions
{
    private static readonly Regex MultiSpace = new(@"\s+", RegexOptions.Compiled);

    // 擴充方法：表意更自然，且集中維護
    public static string NormalizeSpaces(this string? input)
    {
        // null -> 空字串：避免每處都判空
        if (string.IsNullOrWhiteSpace(input)) return string.Empty;

        // 去除前後空白
        var trimmed = input.Trim();

        // 將多空白壓成單一空白
        return MultiSpace.Replace(trimmed, " ");
    }
}

public static class NameRules
{
    // 靜態常值：集中規則，易於審查與調整
    public const int MaxLength = 64;
}
```

整合步驟（逐步替換呼叫點）
```csharp
public class UsersController
{
    public string NormalizeName(string name)
    {
        // 改為擴充方法呼叫，語意清晰
        var normalized = name.NormalizeSpaces();

        // 使用靜態規則進行驗證
        if (normalized.Length > NameRules.MaxLength)
            throw new ArgumentOutOfRangeException($"Name 超過 {NameRules.MaxLength} 字元");

        return normalized;
    }
}
```

- 問：若擴充方法很多，該如何避免命名衝突與維護困難？
- 答：依領域切分命名空間與檔案，例如 StringExtensions.Text、StringExtensions.Url。公開 API 僅從一個彙整命名空間匯出（barrel）。在專案層級建立「擴充方法規約」：命名、測試覆蓋率與文件範本，降低未來摩擦。

---

## 測試策略與同步控制 ─ 靜態方法在多執行緒環境下的應對之道
### 說明
靜態方法與快取在多執行緒下易出現共享狀態競爭。測試上，靜態呼叫難以替換，導致耦合與脆弱測試。關鍵在於：以抽象包住不可替代的靜態依賴、採用正確同步原語、盡量保持純函式與不可變資料。

### 重點列表
- 測試替身策略：以介面包裝靜態依賴（時間、環境、I/O），或以委派注入；必要時考慮工具（Microsoft Fakes、JustMock、Pose）。
- 同步控制：使用 Lazy<T>、ConcurrentDictionary、lock、ReaderWriterLockSlim；優先不可變資料結構與寫入前完整建構。
- 初始化順序與可見性：靜態欄位以 Lazy 或靜態建構式確保一次性初始化；避免靜態可變狀態暴露。
- 隔離測試：避免在測試中改變靜態全域狀態；若必要，使用 Fixture 重設或在測試行程中隔離。
- 觀測與健全性：為靜態快取加入度量與上限，避免記憶體膨脹與過期資料。

### 補充範例／常見問答
靜態快取（執行緒安全）
```csharp
using System;
using System.Collections.Concurrent;

public static class CurrencyRateCache
{
    // 使用 ConcurrentDictionary 確保併發安全
    private static readonly ConcurrentDictionary<string, decimal> _rates = new();

    // 以委派取得外部資料來源，避免在此發生 I/O
    public static decimal GetOrAdd(string currency, Func<string, decimal> fetcher)
    {
        // 若不存在則呼叫 fetcher 取得資料
        return _rates.GetOrAdd(currency, c =>
        {
            // 僅在此處執行一次昂貴操作；fetcher 由呼叫端提供，利於測試
            return fetcher(c);
        });
    }
}
```

以包裝避免直接測 DateTime.UtcNow
```csharp
public interface IGuidProvider { Guid NewGuid(); }
public sealed class SystemGuidProvider : IGuidProvider { public Guid NewGuid() => Guid.NewGuid(); }

public sealed class IdFactory
{
    private readonly IGuidProvider _id;
    public IdFactory(IGuidProvider id) => _id = id;

    public string NewOrderId() => $"ORD-{_id.NewGuid():N}";
}
```

- 問：我能否在單元測試中直接修改靜態欄位以控制行為？
- 答：不建議。這會讓測試互相汙染且難以平行執行。改以注入抽象或委派提供替代資料源；若不得已，至少在 TestFixtureTearDown 中恢復原狀，並使用單執行緒執行器隔離。

---

## 常見問題 ─ 疑難解答與潛在挑戰
### 說明
整理實務上常見的踩雷：擴充方法過度、命名空間衝突、靜態狀態污染、測試替身困難、效能誤解。每題提供可落地的對策，協助你在重構與新開發過程維持可維運性。

### 重點列表
- 擴充方法氾濫：限制對核心型別與高頻操作，其他放回內聚的服務類別；建立審查清單（命名、可讀性、測試）。
- 命名衝突：明確的命名空間與後綴規則（Extensions / Utilities）；避免在全域 using 中引入過多來源。
- 靜態污染：靜態欄位預設改為 private + readonly；若必須可變，提供受控 setter 並建立重設入口（僅測試環境可用）。
- 不易測試：以介面或委派包住靜態依賴；評估是否值得引入可攔截靜態的工具。
- 效能迷思：擴充方法非萬靈丹；真正的瓶頸常是 I/O、序列化或不必要的物件配置。

### 補充範例／常見問答
避免擴充方法命名衝突
```csharp
namespace MyApp.Text
{
    public static class StringExtensions
    {
        // 明確領域：Text
        public static string ToSlug(this string input)
        {
            // 簡化示意：小寫 + 以 - 連接
            return string.Join("-", input.Trim().ToLowerInvariant().Split(' '));
        }
    }
}

namespace MyApp.Url
{
    public static class StringExtensions
    {
        // 明確領域：Url
        public static string EnsureTrailingSlash(this string input)
        {
            return input.EndsWith("/") ? input : input + "/";
        }
    }
}

// 使用端：以 using 指定命名空間避免衝突
// using MyApp.Text;
// using MyApp.Url;
```

- 問：同名 StringExtensions 會不會衝突？
- 答：類別名可重複但命名空間不同即可共存；使用端只引入需要的命名空間，或以靜態類別完整名稱呼叫，避免歧義。

---

## 延伸資源與後續學習建議
### 說明
為持續深化擴充方法與靜態設計的掌握，以下提供文獻、工具與模板，協助落地與團隊對齊。

### 重點列表
- 深入閱讀：
  - C# language reference: Extension methods
  - Framework Design Guidelines（設計 API 的命名與一致性）
  - .NET Docs: Threading, Synchronization primitives
- 工具與框架：
  - 測試：xUnit/NUnit、FluentAssertions、NSubstitute/Moq
  - 靜態攔截（評估使用）：Microsoft Fakes、JustMock、Pose
  - 分析：Roslyn 分析器、StyleCop、SonarQube
- 團隊模板：
  - 擴充方法規約：命名、命名空間、文件與測試樣板
  - 靜態設計清單：何時可靜態、同步策略、快取失效策略
  - 設計文件樣板：動機、替代方案評估、風險、回滾計畫

### 補充範例／常見問答
簡易「擴充方法樣板」檔頭
```csharp
// [模組] 字串正規化
// [目的] 提供一致的空白與大小寫處理
// [測試] StringExtensionsTests.NormalizeSpaces_Should_...
// [注意] 僅針對輸入清理，不處理語言學斷詞
namespace MyApp.Text;

public static class StringExtensions
{
    // 方法本體...
}
```

- 問：何時導入靜態攔截工具（如 Fakes）？
- 答：在大量遺留代碼、短期內無法重構抽象時作為過渡方案。長期仍建議以介面／委派重構，以降低維護成本與工具相依。

---

## 結論
擴充方法與靜態設計是 C# 後端常用的兩把利器：前者提升語意與 API 發現性，後者提供輕量且高效的純邏輯承載。關鍵不在「用或不用」，而是「用在對的地方」。面對可測試性、多執行緒安全與團隊一致性，請遵循以下原則：
- 邏輯可純則純：純函式優先，其次靜態工具，再次擴充方法以提升語意，含外部依賴則改為可注入服務。
- 對外依賴必抽象：時間、隨機、環境變數以介面／委派包裝，測試可替換。
- 並發安全不僥倖：靜態快取與共用狀態以 Lazy、ConcurrentDictionary、不可變資料與明確失效策略管理。
- 制度化治理：擴充方法與靜態設計規約、命名空間策略、審查清單與樣板，讓團隊長期維持可維運。

將本文的準則與範例落實到你的程式庫與 API 後，你會得到更清晰的語意、更穩定的測試與更可控的效能表現；並讓未來的重構與擴展更可預期。