案例背景:Mary Rowan 的電商應用程式#

本章透過一個完整的案例研究,展示沒有使用 DI 的程式碼會產生什麼問題。主角 Mary Rowan 被要求建構一個電商應用程式,技術選型為:

  • UI 層:ASP.NET Core MVC
  • 資料存取層:Entity Framework Core + SQL Server
  • 架構:傳統三層式架構 (Three-Layer Architecture)

Mary 採用 inside-out 的開發方式——從資料層開始,依序往上建構到 Domain 層,最後是 UI 層。

Figure 2.1: 標準三層式應用程式架構

資料存取層#

Mary 首先建立了 Product 類別和 CommerceContext

public class CommerceContext : DbContext
{
    protected override void OnConfiguring(
        DbContextOptionsBuilder builder)
    {
        builder.UseSqlServer(
            "Server=.;Database=Commerce;...");  // 硬編碼的連線字串
    }
}

連線字串直接寫死在程式碼中,導致每次更換資料庫環境都需要修改並重新編譯程式碼。

Domain 層#

ProductService 直接引用資料存取層的 CommerceContextProduct

public class ProductService
{
    private readonly CommerceContext context;

    public ProductService()
    {
        this.context = new CommerceContext();  // 直接 new
    }

    public IEnumerable<Product> GetFeaturedProducts()
    {
        return context.Products
            .Where(p => p.IsFeatured)
            .ToList();
    }
}

問題顯而易見:Domain 層反向依賴了資料存取層,而非資料存取層依賴 Domain 層。

UI 層#

HomeController 直接建立 ProductService,折扣邏輯也散落在 Controller 和 View 中:

public class HomeController : Controller
{
    public ViewResult Index()
    {
        var service = new ProductService();  // 直接 new
        var products = service.GetFeaturedProducts();

        // 折扣邏輯在 UI 層處理
        // ...
    }
}

問題清單#

這段緊耦合的程式碼暴露了多項設計問題:

  • 依賴方向錯誤:Domain 層和 UI 層都依賴資料存取層,形成自上而下的單向依賴鏈
  • 沒有定義 Abstraction:所有類別直接依賴具體實作,沒有介面可供替換
  • 領域概念放錯位置Product(核心領域概念)定義在資料存取層中
  • 折扣邏輯外洩:業務邏輯散落在 UI 層,違反關注點分離
  • 硬編碼的連線字串:無法針對不同環境進行設定

實際影響#

這些設計問題直接導致以下後果:

問題後果
無法替換資料存取想換成 NoSQL 或 Web API?必須大幅修改 Domain 層

Figure 2.8: 嘗試替換關聯式資料存取層

| 無法進行 Unit Test | ProductService 永遠綁定真實資料庫 | | 無法平行開發 | 資料層未完成前,Domain 層和 UI 層無法獨立進行 | | 維護成本高 | 任何變更都可能產生連鎖反應 |

依賴圖分析#

從依賴圖來看,所有箭頭都指向下方(資料存取層),形成一個倒金字塔的依賴結構。這意味著最底層的實作細節變成了整個系統的核心,任何資料存取的變更都會向上波及所有層級。

Figure 2.6: Mary 應用程式的依賴圖,箭頭指向被依賴的模組

Figure 2.10: 理想的依賴圖

本章刻意呈現反面教材。這個「改善前」的版本將在第三章以 DI 原則重新設計,兩相對照之下,DI 的價值會更加清晰。