Kerry 的筆記本
  • Table of contents
  • Kerry的Mac裝機必要
  • ASP.NET Core 教育訓練文件
    • .NET 9 OpenAPI 介紹與教學
    • 目錄
    • ASP.NET Core Authentication系列(一)理解Claim, ClaimsIdentity, ClaimsPrincipal
    • ASP.NET Core Authentication系列(三)Cookie選項
    • ASP.NET Core Authentication系列(二)實現認證、登錄和註銷
    • ASP.NET Core Authentication系列(四)基於Cookie實現多應用間單點登錄(SSO)
    • ASP.NET Core Consul 教學
    • ASP.NET Core Hangfire 排程管理
    • ASP.NET Core KeyCloak 實作
    • ASP.NET Core NLog-依照Environment使用Nlog.Config檔案
    • ASP.NET Core NLog-如何使用 NLog 將 log 寫到檔案
    • ASP.NET Core Nlog-發送訊息到ElasticSearch
    • 目錄
    • ASP.NET Core Quartz.NET 管理介面
    • ASP.NET Core RDLC 報表設計
    • ASP.NET Core SFTP (使用第三方套建 SSH.Net) - 類別庫為案例
    • ASP.NET Core 中使用 HttpReports 進行接口統計,分析, 可視化, 監控,追踪等
    • ASP.NET 使用 MassTransit 與 RabbitMQ,實現事件發佈、訂閱
    • Asp.Net Core 分散式Session – 使用 Redis
    • ASP.NET Core 前台會員修改個人資料
    • ASP.NET Core 前台會員忘記密碼與重設密碼
    • ASP.NET Core 前台會員登入
    • ASP.NET Core 前台會員註冊
    • ASP.NET Core 呼叫 API 發生 CORS 錯誤
    • ASP.NET Core 如何套網頁設計樣版
    • ASP.NET Core 客製化Model Validation 預設錯誤訊息
    • ASP.NET Core 後台查詢頁面教學
    • ASP.NET Core 網站生命週期
    • ASP.NET Feature Management 使用說明與教學
    • ASP.NET RulesEngine 介紹
    • ASP.NET WinForms APP 程式安裝檔
    • LinePay 支付完成後返回 LINE 應用而不跳出外部瀏覽器
    • EntityFramework
      • EF Core Migrations 完整教學手冊
      • EntityFramework Core DB Migrations
      • 使用 Entity Framework Core (EF Core) 的 Migrations 功能進行版本控制
    • NET 6
      • .NET 6 Autofac範例
      • .NET 6 Automapper範例
      • .NET 6 BenchmarkDotNet範例
      • .NET 6 Bogus範例
      • .NET 6 Dapper範例
      • .NET 6 Dapper語法說明
      • .NET 6 EFCore範例
      • .NET 6 EFCore語法說明
      • .NET 6 EPPlus圖表範例
      • .NET 6 EPPlus範例
      • .NET 6 Hangfire範例
      • .NET 6 HttpClient單元測試範例
      • .NET 6 MailKit前置作業
      • .NET 6 MailKit範例
      • .NET 6 Moq範例
      • .NET 6 NLog範例
      • .NET 6 NLog進階範例
      • .NET 6 Serilog範例
      • .NET 6 Serilog進階範例
      • .NET 6 Telegram.Bot前置作業
      • .NET 6 Telegram.Bot範例
      • .NET 6 Text.Json範例
      • .NET 6 swagger授權
      • .NET 6 swagger範例
      • .NET 6 xUnit範例
      • .NET 6 取得appsettings檔案內容
      • .NET 6 更改回傳Json時為大駝峰命名
      • .NET 6 解決System.Text.Json序列化後會將所有非ASCII轉為Unicode
    • WDMIS
      • CORS
      • FeatureManagement
      • Serilog
      • Spectre.Console
      • 資料模型實戰:從 MSSQL 設計到 .NET 8 WebAPI 實作(以刀具管理為例)
  • Azure
    • 如何在 ASP.NET CORE 5.0 WEB 應用程序中實現 AZURE AD 身份驗證
    • Azure App Configuration 使用教學
    • Azure Blob Storage
    • Azure DevOps 持續整合(CI) + Artifacts
  • CSharp
    • ASP.NET await 與 wait 的差異
    • AutoMapper —— 類別轉換超省力
    • C# 中的 HTTPClient — 入門指南
    • C# 正則表達式:從零到英雄指南
    • C# 集合, List<> 取交集、差集、聯集的方法
    • C#單元測試教學
    • CORS 介紹與設定方式
    • CSharp Coding Conventions
    • Using jQuery Unobtrusive AJAX in ASP.NET Core Razor Pages
    • 深入Dapper.NET源碼
    • 菜雞與物件導向
      • 菜雞與物件導向 (0): 前言
      • 菜雞與物件導向 (1): 類別、物件
      • 菜雞與物件導向 (10): 單一職責原則
      • 菜雞與物件導向 (11): 開放封閉原則
      • 菜雞與物件導向 (12): 里氏替換原則
      • 菜雞與物件導向 (13): 介面隔離原則
      • 菜雞與物件導向 (14): 依賴反轉原則
      • 菜雞與物件導向 (15): 最少知識原則
      • 菜雞與物件導向 (2): 建構式、多載
      • 菜雞與物件導向 (3): 封裝
      • 菜雞與物件導向 (4): 繼承
      • 菜雞與物件導向 (5): 多型
      • 菜雞與物件導向 (6): 抽象、覆寫
      • 菜雞與物件導向 (7): 介面
      • 菜雞與物件導向 (8): 內聚、耦合
      • 菜雞與物件導向 (9): SOLID
      • 菜雞與物件導向 (Ex1): 小結
  • DBeaver
    • 如何強制讓 DBeaver 在 Mac 上使用英文介面
  • DesignPattern
    • OAuth
    • Repository 模式 (Repository Pattern)
    • Single Sign On 實作方式介紹 (CAS)
    • 【SOP製作教學】新手適用,SOP範例、流程圖、製作流程全公開!
    • 【SOP製作教學】流程圖教學、重點範例、BPMN符號介紹!
    • 【SOP製作教學】流程圖符號整理、BPMN2.0進階符號教學!
    • 多奇數位 C# 程式碼撰寫規範 (C# Coding Guideline)
    • 軟體分層設計模式 (Software Layered Architecture Pattern)
    • 開源程式碼檢測平台 SonarQube
    • 菜雞新訓記
      • 菜雞新訓記 (0): 前言
      • 菜雞新訓記 (1): 使用 Git 來進行版本控制吧
      • 菜雞新訓記 (2): 認識 Api & 使用 .net Core 來建立簡單的 Web Api 服務吧
      • 菜雞新訓記 (3): 使用 Dapper 來連線到資料庫 CRUD 吧
      • 菜雞新訓記 (4): 使用 Swagger 來自動產生可互動的 API 文件吧
      • 菜雞新訓記 (5): 使用 三層式架構 來切分服務的關注點和職責吧
      • 菜雞新訓記 (6): 使用 依賴注入 (Dependency Injection) 來解除強耦合吧
      • 菜雞新訓記 (7): 使用 Fluent Validation 來驗證參數吧
  • DevOps
    • Repository 模式 (Repository Pattern)
    • pipeline工具研究
    • 單例模式 (Singleton Pattern)
    • 單元測試
    • 軟體分層設計模式 (Software Layered Architecture Pattern)
    • 雙重檢查鎖定模式 (Double-Checked Locking Pattern)
  • Docker
    • Docker 中部署 .NET 8 Web App 並支援 HTTPS
    • Docker指令大全
    • 第七章 安裝Nomad
    • Docker - 第三章 | 安裝 MSSQL
    • Docker - 第九章 | 安裝 datalust seq
    • 第二章 docker-compose 教學
    • Docker - 第五章 | 安裝 Redis
    • 第八章 安裝SonarQube
    • Docker - 第六章 | 安裝RabbitMQ
    • 第十一章 安裝 VtigerCRM
    • 第十二章 安裝KeyCloak
    • Docker - 第十章 | 安裝 Redmine
    • 第四章 安裝MySQL
    • Docker Desktop (含更改 Docker Image 路徑)
  • Git
    • Git Flow 指令大全(完整指令整理) 🚀
    • Git 安裝及配置SSH Key
    • Git 建立到上傳
    • 將現有專案的遠端儲存庫直接更改為新的儲存庫
    • Git 流程規劃
    • Git 語法大全
    • 30 天精通 Git 版本控管
      • 30 天精通 Git 版本控制
        • 第 01 天:认识 Git 版本控制
        • 第 02 天:在 Windows 平台必装的三套 Git 工具
        • 第 03 天:建立仓库
        • 第 04 天:常用的 Git 版本控制指令
        • 第 05 天:了解仓库、工作目录、物件与索引之间的关系
        • 第 06 天:解析 Git 资料结构 - 物件结构
        • 第 07 天:解析 Git 资料结构 - 索引结构
        • 第 08 天:关于分支的基本观念与使用方式
        • 第 09 天:比对文件与版本差异
        • 第 10 天:认识 Git 物件的绝对名称
        • 第 11 天:认识 Git 物件的一般参照与符号参照
        • 第 12 天:认识 Git 物件的相对名称
        • 第 13 天:暂存工作目录与索引的变更状态
        • 第 14 天: Git for Windows 选项设定
        • 第 15 天:标签 - 标记版本控制过程中的重要事件
        • 第 16 天:善用版本日志 git reflog 追踪变更轨迹
        • 第 17 天:关于合并的基本观念与使用方式
        • 第 18 天:修正 commit 过的版本历史记录 Part 1
        • 第 19 天:设定 .gitignore 忽略清单
        • 第 20 天:修正 commit 过的版本历史记录 Part 2
        • 第 21 天:修正 commit 过的版本历史记录 Part 3
        • 第 22 天:修正 commit 过的版本历史记录 Part 4 (Rebase)
        • 第 23 天:修正 commit 过的版本历史记录 Part 5
        • 第 24 天:使用 GitHub 远端仓库 - 入门篇
        • 第 25 天:使用 GitHub 远端仓库 - 观念篇
        • 第 26 天:多人在同一个远端仓库中进行版控
        • 第 27 天:通过分支在同一个远端仓库中进行版控
        • 第 28 天:了解 GitHub 的 fork 与 pull request 版控流程
        • 第 29 天:如何将 Subversion 项目汇入到 Git 仓库
        • 第 30 天:分享工作中几个好用的 Git 操作技巧
      • zh-tw
        • 第 01 天:認識 Git 版本控管
        • 第 02 天:在 Windows 平台必裝的三套 Git 工具
        • 第 03 天:建立儲存庫
        • 第 04 天:常用的 Git 版本控管指令
        • 第 05 天:了解儲存庫、工作目錄、物件與索引之間的關係
        • 第 06 天:解析 Git 資料結構 - 物件結構
        • 第 07 天:解析 Git 資料結構 - 索引結構
        • 第 08 天:關於分支的基本觀念與使用方式
        • 第 09 天:比對檔案與版本差異
        • 第 10 天:認識 Git 物件的絕對名稱
        • 第 11 天:認識 Git 物件的一般參照與符號參照
        • 第 12 天:認識 Git 物件的相對名稱
        • 第 13 天:暫存工作目錄與索引的變更狀態
        • 第 14 天: Git for Windows 選項設定
        • 第 15 天:標籤 - 標記版本控制過程中的重要事件
        • 第 16 天:善用版本日誌 git reflog 追蹤變更軌跡
        • 第 17 天:關於合併的基本觀念與使用方式
        • 第 18 天:修正 commit 過的版本歷史紀錄 Part 1
        • 第 19 天:設定 .gitignore 忽略清單
        • 第 20 天:修正 commit 過的版本歷史紀錄 Part 2
        • 第 21 天:修正 commit 過的版本歷史紀錄 Part 3
        • 第 22 天:修正 commit 過的版本歷史紀錄 Part 4 (Rebase)
        • 第 23 天:修正 commit 過的版本歷史紀錄 Part 5
        • 第 24 天:使用 GitHub 遠端儲存庫 - 入門篇
        • 第 25 天:使用 GitHub 遠端儲存庫 - 觀念篇
        • 第 26 天:多人在同一個遠端儲存庫中進行版控
        • 第 27 天:透過分支在同一個遠端儲存庫中進行版控
        • 第 28 天:了解 GitHub 的 fork 與 pull request 版控流程
        • 第 29 天:如何將 Subversion 專案匯入到 Git 儲存庫
        • 第 30 天:分享工作中幾個好用的 Git 操作技巧
  • Hands-On Labs - LineBotSDK 實作手札 (C#, .net core)
    • 00. 如何申請LINE Bot
    • CLI
      • 使用CLI來發送新的Channel Access Token(LINE Bot)
      • 使用CLI免費發送LINE Notify通知
    • basic
      • 如何發送LINE訊息(Push Message)
      • 如何發送LINE Template Messages
      • 如何發送ImageMap訊息
      • 如何發送Flex Message
      • 如何在訊息後面加上QuickReply快捷選項
    • liff
      • Lab 21: 建立第一個LIFF應用
    • webhook
      • 如何建立可Echo的基本LINE Bot
      • 如何在WebHook中取得用戶個人資訊(名稱、頭像、狀態)
      • 如何在WebHook中取得用戶上傳的圖片(Bytes)
  • Markdown
    • Markdown Cheatsheet 中文版
    • Markdown語法大全
    • 使用HackMD建立書本目錄
    • 使用HackMD建立簡報
  • SAP ABAP
    • ABAP開發環境和總體介紹
    • SAP MM模塊常用表總結
    • SAP QM數據庫表清單
    • SAP欄位與表的對應關係
  • SQL Server
    • [SQL SERVER] Like in
    • SQL Server 中,移除資料庫中所有的關聯限制
    • SQL Server 刪除資料庫中所有資料表
    • SQL Server View、Function 及 Stored Procedure 定義之快速備份
    • SSMS v18 清除登入畫面中,下拉選單歷史紀錄
    • [MS SQL]如何透過Database Mail進行郵件發送
    • [SQL SERVER]撰寫Stored Procedure小細節
    • 使用 Data Migration Assistant 移轉 SQL Server 資料庫與帳戶
    • 使用SSIS創建同步資料庫數據任務
  • Tools
    • 免費 FTP 伺服器 FileZilla Server 安裝教學 (新版設定)
  • VisualStudio
    • .NET CLI 指令碼介紹
    • Visual Studio 使用 Git 版本控制
    • 使用 Visual Studio 2022 可透過 .editorconfig 鎖定文字檔案的儲存編碼格式分享
  • Web API
    • ASP.NET Core 6 Web API 進行 JWT 令牌身份驗證
    • [ASP.NET Core]如何使用SwaggerAPI說明文件
    • ASP.NET Core Web Api實作JWT驗證筆記
    • ECFIT API 範例
    • JWT Token Authentication And Authorizations In .Net Core 6.0 Web API
    • 微服務架構 - 從狀態圖來驅動 API 的設計
  • Windows
    • [C#] 伺服器監控常用語法 (事件檢視器、CPU 硬碟使用率、程式執行狀況)
    • Configure IIS Web Server on Windows Server 2019
    • Log Paser Studio 分析 IIS W3C Log
    • Windows Server 2019 如何安裝 IIS 運行 ASP.NET 專案
    • 如何檢查安裝在 IIS 上的 .NET Core Hosting Bundle 版本
    • [IIS] 如何解決網站第一個請求 Request 特別慢 ?
    • IIS 不停機更版設置
    • SQL Server 2019 Standard 繁體中文標準版安裝
    • WINDOWS共用資料夾的網路認證密碼放在哪?如何清除?
    • 如何設定 ASP.NET CORE 網站應用程式持續執行在 IIS 上
  • 專案管理
    • SSDLC (Secure Software Development Life Cycle)
    • 系統開發原則
    • MIS及專案管理-使用Redmine
      • 第10章 - [日常管理]MIS部門週會工作進度追蹤
      • 第11章 - [日常管理]MIS部門主管月會報告管理
      • 第12章 - [日常管理]機房工作日誌
      • 第13章 - [日常管理]MIS部門耗用工時及工作進度檢討
      • 第14章 - [日常管理]MIS文件知識庫
      • 第15章 - [日常管理]整理及管理分享
      • 第16章 - [異常管理]使用者問題回報系統
      • 第17章 - [異常管理]資安事件及異常紀錄
      • 第18章 - [異常管理]整理及管理分享
      • 第19章 - [變革管理]MIS的專案及專案管理五大階段
      • 第1章 - [MIS及專案管理]中小企業MIS的鳥事
      • 第20章 - [變革管理]MIS的新專案管理:起始階段
      • 第21章 - [變革管理]MIS的新專案管理:規劃階段
      • 第22章 - [變革管理]MIS的新專案管理:執行階段
      • 第23章 - [變革管理]MIS的新專案管理:監控階段
      • 第24章 - [變革管理]MIS的新專案管理:結束階段
      • 第25章 - [變革管理]整理及管理分享
      • 第26章 - [ISMS管理]ISMS平台整體規劃
      • 第27章 - [ISMS管理]ISMS文管中心
      • 第28章 - [ISMS管理]ISMS表單紀錄的管理
      • 第29章 - [ISMS管理]整理及管理分享
      • 第2章 - [MIS及專案管理]專案管理的概念及MIS應用
      • 第30章 - 初心、來時路及感謝:系列文章總結回顧
      • 第3章 - [MIS及專案管理]管理工具的選擇
      • 第4章 - [Redmine]Redmine的安裝及設定
      • 第5章 - [Redmine]Redime系統邏輯說明
      • 第6章 - [Redmine]自行建立及維護表單
      • 第7章 - [Redmine]專案版面的規劃
      • 第8章 - [日常管理]AR管理
      • 第9章 - [日常管理]資訊服務申請
  • 微服務架構
    • DDD + CQRS + MediatR 專案架構
    • 微服務架構 #2, 按照架構,重構系統
    • 淺談微服務與網站架構的發展史
    • API First Workshop 設計概念與實做案例
      • API First #1 架構師觀點 - API First 的開發策略 - 觀念篇
      • API First #2 架構師觀點 - API First 的開發策略 - 設計實做篇
    • 基礎建設 - 建立微服務的執行環境
      • Part #1 微服務基礎建設 - Service Discovery
      • Part #2 微服務基礎建設 - 服務負載的控制
      • Part #3 微服務基礎建設 - 排隊機制設計
      • Part #4 可靠的微服務通訊 - Message Queue Based RPC
      • Part #5 非同步任務的處理機制 - Process Pool
    • 實做基礎技術 API & SDK Design
      • API & SDK Design #1, 資料分頁的處理方式
      • API & SDK Design #2, 設計專屬的 SDK
      • API & SDK Design #3, API 的向前相容機制
      • API & SDK Design #4, API 上線前的準備 - Swagger + Azure API Apps
      • API & SDK Design #5 如何強化微服務的安全性 API Token JWT 的應用
    • 建構微服務開發團隊
      • 架構面試題 #1, 線上交易的正確性
      • 架構面試題 #2, 連續資料的統計方式
      • 架構面試題 #3, RDBMS 處理樹狀結構的技巧
      • 架構面試題 #4 - 抽象化設計;折扣規則的設計機制
    • 架構師觀點 - 轉移到微服務架構的經驗分享
      • Part #1 改變架構的動機
      • Part #2 實際改變的架構案例
    • 案例實作 - IP 查詢服務的開發與設計
      • 容器化的微服務開發 #1 架構與開發範例
      • 容器化的微服務開發 #2 IIS or Self Host
  • 系統評估
    • RPA 與 WebAPI 評估
    • 數位轉型:從現有系統到數位化的未來
    • 數位轉型:從現有系統到數位化的未來
  • 面試
    • CV_黃子豪_2024
    • HR 問題集
    • .NET 工程師 面試問題集
    • 資深工程師 問題集
    • 資深開發人員 / 技術主管
    • 題目
Powered by GitBook
On this page
  • 前言
  • 為什麼需要依賴注入
  • 依賴注入的種類
  • 建構式注入
  • 方法注入
  • 屬性注入
  • 組合根(Composition Root)
  • .Net Core 中的依賴注入
  • 使用 AddScoped 來註冊介面對應的實作類別
  • 使用 AddScoped 和委派來註冊介面對應的實作類別的產生方法
  • 依賴注入的三種生命週期 Transient、Scoped、Singleton
  • 延伸閱讀
  • 補充:.net Core 使用 BuildServiceProvider 會建立多個實體
  • 實作
  • 開始重構為建構式注入
  • 補充:從 appsettings.json 取得組態
  • 補充:組合根請稍作分類
  • 驗證
  • 小結
  • 參考資料
  • 本系列文章
  • 其他文章
  1. DesignPattern
  2. 菜雞新訓記

菜雞新訓記 (6): 使用 依賴注入 (Dependency Injection) 來解除強耦合吧

Previous菜雞新訓記 (5): 使用 三層式架構 來切分服務的關注點和職責吧Next菜雞新訓記 (7): 使用 Fluent Validation 來驗證參數吧

Last updated 1 year ago

Image

這是俺整理公司新訓內容的第六篇文章,目標是紀錄什麼是依賴注入(Dependency Injection)。包含:

前言

西元前的某一天,憂心的皇帝在朝堂內繞著柱子走,正巧被路過的廷尉看見。

廷尉:「敢問陛下在煩惱什麼呢?」

皇帝:『朕這是在想封賞的事兒哪。前朝之所以覆滅,根本的原因就在於大肆封賞臣下,四處分封土地給他們做諸侯。

這些諸侯呢,肆意起用自己喜歡的人擔任要職、結黨營私,心情好就 new 將軍("我ㄉ朋友");, 十天就封了十個將軍。這些人若犯了錯,要處理他們還得看諸侯面子;而諸侯一聲令下,這些人便群起造反。

並且,這些諸侯之間彼此喜歡直接往來,動不動就在自家裡下命令給 隔壁諸侯.借糧草(100),哪天就變成 隔壁諸侯.揪團造反()。彼此之間偷來暗去,實在難以掌握。

最後呢,一個逆賊起來造反,若要將他給辦了,附近諸侯就一起響應,每個都一齊報錯,Exception 成千上百,國家也就這樣滅了,想到這朕就頭痛得很,不知愛卿可有法子?』

廷尉想了一想,便說:「陛下,此事要點還是在於諸侯之間相互依賴、彼此耦合,致生禍端。

臣有一計,先收回諸侯的人事任命權,使其不可私自 new 自己人,所有人事異動,須由中央進行管理與派遣。這樣即使諸侯要造反,也不知道下面這群打工仔是不是自己人。大家各司其職,諸侯做好自己的行政作業,打工仔派到崗位就做好自己的工作,彼此不直接依賴,這樣出事的機率就少了。

其次,明令禁止諸侯私自往來,對諸侯們進行隔離,若是有公務上的需要,一律藉由中央提供的接口來溝通,彼此之間明訂契約,由中央進行隔離與調派,諸侯間就只需要按照協議好的合約下去合作,這樣勾結的機會也就少了,耦合也就降低了。陛下覺得如何?」

皇帝大喜:『如此甚好!治眾如治寡,在於分而治之。此計可有名字?』

「此乃--依賴注入之計!」

為什麼需要依賴注入

各位好,我們前面引用了民明書坊的《朕的郡縣制哪有這麼耦合》,相信各位對依賴注入應該已經有初步的了解了。說到依賴注入的觀念,就得先從 SOILD 中的依賴反轉原則開始談。

註:想好好了解的朋友,也可以從依序閱讀這幾篇相關文章後再回到這篇呦:

讓我們從之前依賴反轉的範例開始吧:假設現在有間小小公司,老闆請來了小明當工程師,並請他開工撰寫產品程式碼。

當「撰寫產品程式」對「工程師」直接依賴的時候,狀況可能是這樣的:

public Product Work() // 撰寫產品程式
{
    Ming programmer = new Ming();
    var product = programmer.Programming();
    return product;
}

過一陣子,老闆發現小明寫出來的東西似乎不太行,於是把小明趕走,另外請了小華。這時候因為用到的類別不一樣了,我們就必須要改一次程式碼:

public Product Work()
{
    Hua programmer = new Hua(); // 把小明改成小華
    var product = programmer.Programming();
    return product;
}

又過了好一陣子,老闆又另外請了小美來工作。於是又要再改一次,而且小美的工作方式甚至不叫做 Programming,而是 Coding:

public Product Work()
{
    Mei programmer = new Mei(); // 把小華改成小美
    var product = programmer.Coding(); // 呼叫方法也要改
    return product;
}

現在有感覺到一點問題了嗎?如果一直換人,Work 的程式碼豈不是每次都要修改?

但我們平常開發程式的思維,會習慣從大範圍到小細節、從抽象到具體、從整體目標逐漸拆解成各個步驟的方向去處理,也就是從高階模組往低階模組的方向設計。

例如說我們需要「會員查詢」功能,才用「DB 連線方法」和「資料篩選方法」等具體方式去達成我們要「會員查詢」這個目標。

然而以上面工程師的例子來看:低階模組的變更,卻會導致使用它的高階模組連帶受到影響,在我們決定大方向大目標的時候卻被實作細節綁手綁腳,實在是很怪的一件事,和我們上述的習慣是相悖的。

面對這樣的困境,依賴反轉原則告訴我們:高階模組不應該依賴於低階模組。兩者都應該依賴抽象。

在這裡的重要前提是,我們必須了解到:並不是高階模組去依賴低階模組,而是高階模組提出它需要的功能,低階模組去實作出這些功能、達成高階模組的目標,這也比較接近我們開發程式時的思維。

例如前面的會員查詢:我們並不是因為有「DB 的連線方法」和「處理會員資料的方法」所以才說「我們有這兩個東西欸,那我們來組成會員查詢功能吧」;而是「我們想做一個會員查詢功能,所以我們需要連線到 DB,然後對這些資料做篩選和處理」

而用工程師的例子來看,應該是要這樣的:「老闆為了製造產品(高階模組的目標),開出了工程師的應徵條件(介面),而小明前來應徵(低階模組的實作)」

如此一來,依賴就「反轉」了。原本是 高階模組 → 低階模組 的關係,變成了 高階模組 → 介面 ← 低階模組。

現在讓我們把上面例子的「工程師」改成介面,如此一來就會變成:

// 工程師的介面
public interface IProgrammer
{
    void Programming();
}

// 小明,一位工程師
public class Ming : IProgrammer
{
    public void Programming() { /* Work */ }
}

public Product Work()
{
	// 這邊需要一名工程師,呼叫小明前來
    IProgrammer programmer = new Ming();

	// 根據介面的契約,工程師一定都有 Programming 方法
    var product = programmer.Programming();
    return product;
}

這邊就會遇到我們介面篇結束時所問的問題:我們使用功能之前,必須先建立該類別的實例,也就是 new Ming(),那麼,我們不就還是直接依賴了實作嗎?

面對這個問題,大大們提出了許多個解決的方法,其中最常見的就是:控制反轉 (Inversion of Control, IoC)。

思路非常的簡單:我們把實例的建立和實例的使用切分開來就好了,讓建立的去建立、讓使用的去使用,不再是由高階模組去建立並控制低階模組,而是我們讓一個控制反轉中心去建立低階模組,然後高階模組要使用的時候再把這個低階模組交給高階模組使用。

如此一來,控制權也跟著反轉過來了,高階模組從主動建立低階模組,變成被動接收低階模組;也就是從原先的 高階模組 —(建立)→ 低階模組,變成了 高階模組 ←(傳遞低階模組)— 控制反轉中心。

也就是說,高階模組再也不需要關心如何建立,該建立哪個實體,只專注於使用功能,真正達到介面的精神。低階模組也只需要等待控制反轉中心分發,到了崗位就把份內事做好,專心在自己的職責身上即可。如此一來就能解除兩者之間的耦合。

但是,要怎麼把控制中心建立的低階模組,交給高階模組做使用呢?這時候的實作方式就是我們今天的主角:依賴注入 (Dependency Injection)。

依賴注入的種類

白話一點來說,「注入」也就是「丟進去」的意思。所以依賴注入就是指用各種方法把低階模組丟到高階模組裡。

主要常見的有三種作法:建構式注入、方法注入、屬性注入。也就是從建構式丟進去、從方法丟進去、從屬性丟進去。

建構式注入

建構式注入顧名思義就是從建立物件時的建構式進行注入。

現在假設我們有個「法師」的類別,並且它有個屬性用來表示他目前裝備的法術:

public class Wizard
{
    // 只有建構式時會給值,所以可以順手加上 readonly 防止被變動
    private readonly ISpell _spell;

    // 在建構式決定要裝備什麼法術
    public Wizard(ISpell spell)
    {
        this._spell = spell;
    }
}

public interface ISpell { } // 法術介面

而建立物件時也使用建構式來傳遞:

void Main()
{
    var spell = new Fireball();
    var wizard = new Wizard(spell);
}

public class Fireball : ISpell { } // 火球術

由於建構式注入比較符合封裝的「管控邊界」精神、能明確地讓維護者一看就知道哪些東西會被注入,因此絕大部分的時候都應該使用建構式注入,只有特殊情況可以使用方法注入和屬性注入。而到了 .net Core 的時代,預設的 DI 容器更是只提供建構式注入。所以理想的情況下,建構式注入應該要是最熟悉的注入方式。

補充:要注意,對建構式做多載可能會造成 DI 容器混淆,不知道要選哪個建構式才好。因此設計類別時盡量以一個建構式為主,或是先了解一下使用的 DI 容器有沒有特別的處理方式再決定。

方法注入

方法注入適用於「呼叫方法時需要注入不同的依賴對象」時。例如說該方法在不同地方被呼叫的依賴對象不一樣,又或者是第一次呼叫和第二次呼叫時的依賴對象不一樣。

這時候我們就可以在呼叫方法的時候才把依賴對象一起丟進去,讓使用端來決定要注入什麼。

例如說我們的法師可以隨身攜帶法術卷軸,使用卷軸就可以放出對應的法術,因此法師類別就會有一個使用法術卷軸的方法。而我們想要等到施法的時候再決定要用哪個卷軸的咒語,這時候就可以把這件事情交給外部決定:

public class Wizard
{
    // 施放指定的法術卷軸
    public void Enchant(ISpellScroll scroll)
    {
        scroll.CastSpell(); // 詠唱
    }
}

// 法術卷軸介面
public interface ISpellScroll
{
    void CastSpell();
}

那我們等到實際使用(使用卷軸施法)的時候再把依賴對象(卷軸)丟進去即可

void Main()
{
	var wizard = new Wizard();
	var scroll = new LevitationCharmScroll();

	wizard.Enchant(scroll); // 使用卷軸施放漂浮咒
}

// 漂浮咒
public class LevitationCharmScroll : ISpellScroll
{
    public void CastSpell() {}
}

如此一來就可以處理一些每次操作依賴對象都會不同的狀況了。但在使用方法注入的時候要注意:由於該方法的呼叫端需要準備依賴對象給方法當作參數使用,整個過程是在方法被呼叫的時候才動態處理的,所以呼叫端還是需要想辦法弄到該依賴對象,也就是再從上一層注入或是工廠製造之類的。

在這個過程中就會增加類別或介面之間的依賴關係、並且讓注入的位置散落在各地等等,維護的時候就必須多注意一下。

我個人比較常在一些輔助工具,例如擴充方法或是 Helper 看到方法注入,像這類單純操作邏輯的場合,就可以考慮採用方法注入的方式去處理。

註:絕對不要把方法注入的依賴對象留在物件內部給其他方法使用,例如A方法注入了某個物件,保留給之後呼叫B方法的時候用,這樣容易造成時序耦合(Temporal Coupling)的問題,也就是使用者(通常是後續的維護人員)不照你想好的順序呼叫的話,方法就會直接死去。

如果使用者不清楚這些方法之間的關係,就很容易踩到地雷,而這樣挖坑的行為很明顯違反了封裝精神,並且也容易產生預期外的副作用。如果真的有需要針對依賴對象做初始化,還是考慮用建構式注入吧。

屬性注入

接著讓我們來看看屬性注入,顧名思義就是從公開的屬性丟進去,因此有時候也會被叫做設值注入。

通常我們會在 「外部使用者要能夠隨時切換依賴對象」或是「類別已經有預設值了,但希望提供使用者可以覆寫掉預設值的彈性」時用到屬性注入,例如說我們的法師同時只能裝備/記得一個法術,預設是火球術,但同時我們又想要可以從外部來替換裝備中的法術:

public class Wizard
{
	// 因為屬性注入的特性,請不要 readonly
    private ISpell _spell; 
    
    // 提供屬性給外部控制
    public ISpell Spell
    {
        get 
        { 
            // 屬性注入的時候要注意: 如果沒有預設值很容易會發生錯誤
            if (this._spell is null)
            {
                this._spell = new Fireball();
            }
            return this._spell; 
        }
        set 
        { 
            this._spell = value; 
        }
    }
}

public interface ISpell {} // 法術介面
public class Fireball : ISpell { } // 火球術

使用的時候就可以直接對屬性賦值,例如我們現在有個法師就不想用火球術,而是使用邪王炎殺黑龍波:

void Main()
{
    var wizard = new Wizard();
    var spell = new DragonOfTheDarknessFlame();
    wizard.Spell = spell;
}

public class DragonOfTheDarknessFlame : ISpell { } // 邪王炎殺黑龍波

我個人是覺得屬性注入的範例其實就是使用介面的封裝小範例啦…。

提到封裝,由於使用者並不會知道物件內部的狀態,所以屬性注入沒有提供預設值的話就很容易壞掉,但給預設值的時候又不可避免地產生耦合(例如上例的法師為了預設是火球術,所以和火球術產生了耦合)

因此如果有為了將來的可擴充性而設計成「預設值使用內建的、通常會存在的類別,但允許外部隨時替換」,也就是讓呼叫端決定「要不要」依賴的場合,再考慮使用會比較合適。否則一律推薦建構式注入。

組合根(Composition Root)

認識完注入的方式之後,讓我們來聊聊組合根吧。前面的例子可以注意到:即使我們要注入法術給法師,也還是要在呼叫端建立法術的實體。而如果呼叫端也使用依賴注入,就會需要呼叫端的呼叫端來注入實體。層層反轉之下,最終就會有一個地方來注入和分配全部的實體給各個物件。

這也就是我們在前面提到的:我們會需要一個負責把各個材料注入到需要的類別中的「控制反轉中心」--也就是叫做「組合根」的部份。

由於每一個類別會將依賴的對象交給外部,也就是呼叫者去決定。而呼叫者又會再往上拋給它的呼叫者,如此不斷往外推之後,就會集中到整個應用程式的啟動點,如此一來我們也就必須在啟動點進行依賴關係的處理。

因此這個組合根的位置通常會盡可能地靠近程式的啟動點,例如整個應用程式 Startup 的地方,或是各個 DI 容器最終註冊的部分。在我們的實作例子中,也就是 API 的專案部份。

補充:請注意,組合根並不一定是在展示層,並且從「為了管理依賴關係而產生組合根」和「為了切分職責而產生展示層」是不同的觀點,不能混為一談。退一步說,我們也可能會把啟動點、組合根之類的切分出去,減少展示層的耦合。所以還是要看專案架構怎麼設計的才能確定組合根的位置。

同時,由於組合根必須分配各個類別前往自己負責的崗位,因此它可以說是和所有模組都有直接依賴的關係(並且也應該只有組合根可以知道整體的物件關聯)。因此,我們應該要在應用程式啟動的部份,找個風水寶地去統一管理我們的組合根和依賴關係,而不能讓注入的部分散亂在各地。

不過這個部份現在的 DI 容器都已經處理好了,例如 Unity 的 UnityConfig、.net Core 的 ConfigureServices 等等,所以只要注意別在其他地方偷偷搞注入、挖坑給別人跳就好囉。

註:不要以為真的遇不到……同事維護的專案就有遇到前同事直接在單元測試案例裡把 DI 容器叫出來註冊依賴注入的,都不知道從哪裡開始吐槽囧

如果在別的地方去亂對依賴關係動手動腳,很可能就會踩到一些坑。總之,盡量別在組合根以外的地方使用 DI 容器。

.Net Core 中的依賴注入

接著讓我們回到本系列的專案吧。可喜可賀的是:在 .net Core 的時代,依賴注入已經是內建提供的功能了!

補充:使用 .net Framework 的朋友也不用擔心,可以使用 Unity, AutoFac 這幾個猛猛的 IoC 容器。教學文章也是網路一抓一大把那種。

甚至到了預設使用建構式注入的 .net Core 時代,還是不少人會為了要用動態代理之類的花式注入手段而把 Autofac 裝回來呢。

我們得有一個地方來做我們的組合根,註冊介面和實體的關係,並讓它可以將實體注入進來。也就是要告訴應用程式:「某某介面 對應的就是 某某實作,請幫我在需要到的時候丟進來。」

在 .net Core 中,我們可以在 Startup.cs 的 ConfigureServices 註冊我們需要的服務:

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllers();

	// 這邊可能還有其他註冊的服務,Swagger 之類的
}

使用 AddScoped 來註冊介面對應的實作類別

而當我們要註冊我們的介面和實作時,例如說我們有一個 ITestService 的介面,希望告訴 DI 容器對應的實作是 TestService,我們就可以使用 AddScoped<> 來註冊對應關係:

// 註冊 ITestService 的實作為 TestService
services.AddScoped<ITestService, TestService>();

如此一來,當 DI 容器發現要注入 ITestService 的場合,就會替我們建構 TestService 並注入。

例如說我們的 TestController 的建構式部分如下,可以看到我們有使用建構式注入:

public TestController(ITestService testService)
{
	this._testService = testService;
}

當 TestController 建立的時候,DI 容器就會知道需要 ITestService 來注入,並找到我們註冊的 TestService 來注入到 TestController。

這時候如果 TestService 也有需要注入的依賴對象,DI 容器就會再回來找我們註冊對應的實作,依此類推,不斷遞迴下去,直到注入都完成為止。

使用 AddScoped 和委派來註冊介面對應的實作類別的產生方法

有些朋友可能會有疑問:我的物件建立時還需要做一些處理才能建立,沒辦法直接告訴 AddScoped 就完事了。

不用擔心,AddScoped 也提供了委派的做法,讓我們可以直接告訴 DI 容器這個實作的產生方法,這個產生過程中我們就能進行一些操作,現在讓我們來示範一次。

假設我們的 TestService 必須要傳遞一個服務 Token 的字串進去,就會像是這樣:

services.AddScoped<ITestService>(sp =>
{
	var token = @"TestServiceToken";
	return new TestService(token);
});

那如果不只是字串這種寫死的狀況,而是我們基於一些原因,想要指定拿到註冊中其他服務的實作的話,就可以使用委派傳入的 ServiceProvider 來取得目前註冊的內容,例如這個 token 其實是另一個 ITokenService 提供的話:

services.AddScoped<ITestService>(sp =>
{
	var tokenService = sp.GetRequiredService<ITokenService>();
	var token = tokenService.Get();
	return new TestService(token);
});

可以看到,我們能藉由 ServiceProvider 的 GetRequiredService 這個方法來取得其他註冊的實體,並利用這個實體來完成注入所需的材料。

依賴注入的三種生命週期 Transient、Scoped、Singleton

除了 AddScoped() 以外,.net Core 還提供了另外兩種注入方法:AddTransient() 和 AddSingleton(),他們對應的是三種不同的生命週期:

  • Transient(一次性):每次注入都建立一個新的

  • Scoped(作用域):每次 Request 都建立一個新的,同個 Request 重複利用同一個

  • Singleton(單例):只建立一個新的,每次都重複利用同一個

假設我們有一個 ILogger 類別,專門幫我們寫 Log。然後我們的 API 會經過 TestController、TestService、TestRepository 這三層物件去查詢資料,其中每一層都注入了 ILogger。那麼:

  • Transient:每一層物件都有自己的、全新的 ILogger

  • Scoped:同一次 API 呼叫裡的每一層物件都是用同一個 ILogger,等到下一次呼叫才建立新的 ILogger

  • Singleton:不論哪次呼叫、不論哪一層注入,所有人都共用同一個 ILogger

一般來說最常用的會是 Scoped,例如功能服務或登入者資訊,在同一次呼叫中保持同一個即可。但面對 HttpCilent 這類能共用同個實例節省資源的,我們就可以考慮使用 Singleton。這邊就再請各位按照使用場景來決定該用哪種生命週期。

另外,注入時請注意生命週期的範圍。例如註冊為 Singleton 的類別不能依賴註冊為 Scoped 的類別,因為如果大家一起用的 Singleton 程式跑到一半,綁在 Request 的 Scoped 依賴對象先消失了,問題可就大了,不可不慎。

關於 HttpClient 的部份,更棒的做法是使用 HttpClientFactory。可以參照:

關於 Singleton 的部份,有興趣的朋友可以了解看看。

延伸閱讀

補充:.net Core 使用 BuildServiceProvider 會建立多個實體

我們在前面提到過藉由 AddScoped 傳入委派的 ServiceProvider 的 GetRequiredService 方法來取得其他註冊的實體這個做法。

那可能就有一些比較聰明的朋友,知道 ServiceProvider 能拿到其他註冊的實體之後,為了在沒有 ServiceProvider 的地方也能取得其他實體(例如想直接在 ConfigureServices 就直接拿到實體,然後經過處理再提供給多個注入使用等等)

所以 Google 了一下怎麼弄出個 ServiceProvider,就用了 BuildServiceProvider 來建立一個 ServiceProvider,但這實際上是相當危險的。

因為 BuildServiceProvider 建立的是一個全新的 ServiceProvider,並非注入時 DI 容易幫我們建來使用的那一個 ServiceProvider,這樣就會造成有兩個 ServiceProvider 在場的狀況。

如此一來如果使用一些 Singleton 的服務,可能就會產生預期外的結果。因此建議還是乖乖在 AddScoped 之類的方法內使用委派的 ServiceProvider 比較好。

參考資料:

實作

其中有直接依賴的部分,會在 Controller 銜接到 Service 以及 Service 銜接到 Repository 的部份。例如:

public class CardService : ICardService
{
	private readonly ICardRepository _cardRepository;

	/// <summary>
	/// 建構式
	/// </summary>
	public CardService()
	{
		this._cardRepository = new CardRepository();
	}
}

可以注意到我們在 CardService 裡面直接 new 了 CardRepository 來使用,這就是直接依賴。

基於依賴反轉原則,我們會希望達成「CardService 依賴的是 ICardRepository 這個介面,並由 CardRepository 實作該介面,藉由介面隔離實作來解除耦合」的目標。

開始重構為建構式注入

現在讓我們改成使用建構式注入吧:

public class CardService : ICardService
{
	private readonly ICardRepository _cardRepository;

	/// <summary>
	/// 建構式
	/// </summary>
	public CardService(ICardRepository cardRepository)
	{
		this._cardRepository = cardRepository;
	}
}

接著為了要讓 DI 容器知道 ICardRepository 對應的實作是 CardRepository,讓我們前往 Startup.cs 的 ConfigureServices 把它註冊起來:

public void ConfigureServices(IServiceCollection services)
{
	// 註冊 ICardRepository 的實作為 CardRepository
	services.AddScoped<ICardRepository, CardRepository>();

	services.AddControllers();

	// 這邊可能還有其他註冊的服務,Swagger 之類的
}

補充:請注意前面提到過的「組合根會直接依賴所有註冊的模組」因此這邊有需要的話記得把參考 using 補上呦。

接著讓我們如法炮製,把 Controller 裡直接依賴的 Service 也拆開吧:

[ApiController]
[Route("[controller]")]
public class CardController : ControllerBase
{
	private readonly ICardService _cardService;

	/// <summary>
	/// 建構式
	/// </summary>
	public CardController(ICardService cardService)
	{
		this._cardService = cardService;
	}
}

然後補上註冊。

public void ConfigureServices(IServiceCollection services)
{
	services.AddScoped<ICardService, CardService>();

	services.AddScoped<ICardRepository, CardRepository>();

	services.AddControllers();

	// 這邊可能還有其他註冊的服務,Swagger 之類的
}

那我們前面有提到,可以用「告訴 DI 容器該物件的產生方法」來做一些額外的事情,這邊就讓我們優化一下。

我們在先前的 CardRepository 有用私有欄位來存放連線字串:

/// <summary>
/// 卡片管理
/// </summary>
/// <seealso cref="ProjectN.Repository.Interface.ICardRepository" />
public class CardRepository : ICardRepository
{
	/// <summary>
	/// 連線字串
	/// </summary>
	private readonly string _connectString = @"Server=(LocalDB)\MSSQLLocalDB;Database=Newbie;Trusted_Connection=True;";
}

現在我們希望連線字串不要寫死在類別裡,而是用建構式注入的方式丟進去,就會變成:

/// <summary>
/// 卡片管理
/// </summary>
/// <seealso cref="ProjectN.Repository.Interface.ICardRepository" />
public class CardRepository : ICardRepository
{
	/// <summary>
	/// 連線字串
	/// </summary>
	private readonly string _connectString;

	public CardRepository(string connectString)
	{
		this._connectString = connectString;
	}
}

接著讓我們回到註冊 ICardRepository 的地方,因為 CardRepository 的建構式現在必須要提供連線字串了,所以我們要改一下 AddScoped 的寫法,把連線字串丟給它:

services.AddScoped<ICardRepository>(sp =>
{
	var connectString = @"Server=(LocalDB)\MSSQLLocalDB;Database=Newbie;Trusted_Connection=True;";
	return new CardRepository(connectString);
});

可以看到我們用 AddScoped 提供了 ICardRepository 對應物件的產生方法。

這樣 .net Core 就會知道要先執行裡面的委派與匿名函式 sp => {},就能拿到 CardRepository 來用囉!

補充:從 appsettings.json 取得組態

另外補充一下,在連線字串這類字串的注入時,我個人偏好更進一步使用 appsettings.json(以前 .net framework 時用過 web.config 的朋友可能會比較熟)

關於 appsettings.json 的用法這邊就不再贅述,想了解的朋友可以參考這幾篇:

因此這邊就把連線字串丟到 appsettings.json,增加一欄 ConnectionString:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionString": "Server=(LocalDB)\\MSSQLLocalDB;Database=Newbie;Trusted_Connection=True;"
}

接著回到 Startup 的建構式,建立一個連線字串的欄位 _connectionString 然後把連線字串讀出來:

public class Startup
{
	private readonly string _connectionString;

	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
		this._connectionString = configuration.GetValue<string>("ConnectionString");
	}
}

這樣我們就完成了 Startup 的連線字串注入囉,只要再把這個字串提供給 CardRepository 就行了:

services.AddScoped<ICardRepository>(sp =>
{
	return new CardRepository(_connectionString);
});

如果沒有多個類別共用同一個連線方法的話,我們也可以直接從組態拿就行了:

services.AddScoped<ICardRepository>(sp =>
{
	var connectionString = this.Configuration.GetValue<string>("ConnectionString");
	return new CardRepository(connectionString);
});

再請根據狀況靈活地運用 Configuration.GetValue 吧!

補充:組合根請稍作分類

這邊補充一下,在實務上由於一個應用程式的注入可能有數十個,因此我們會稍微用註解或可摺疊的 #Region 來分段一下,例如:

public void ConfigureServices(IServiceCollection services)
{
    // Service
	services.AddScoped<ICardService, CardService>();
	
    // Repsitory
	services.AddScoped<ICardRepository, CardRepository>();
	
    // Others
	services.AddControllers();
}

// 或是使用 region,數量很多的時候就可以收攏
public void ConfigureServices(IServiceCollection services)
{
    #region -- Service --
	
	services.AddScoped<ICardService, CardService>();
	
	#endregion
	
	#region -- Repository --
	
    // Repsitory
	services.AddScoped<ICardRepository, CardRepository>();
	
	#endregion
		
    // Others
	services.AddControllers();
}

而前幾個月的時候,敝司某服務的註冊數量多到一個連滑鼠中鍵滾輪都會痛哭的程度。咱同事就利用對 IServiceCollection 做擴充方法的方式,將組合根分類並切出去管理:

// ProjectN.DIExtensions

/// <summary>
/// Service 相關註冊
/// </summary>
public static class ServiceDIExtensions
{
	public static IServiceCollection AddServices(this IServiceCollection services)
	{
		services.AddScoped<ICardService, CardService>();

		return services;
	}
}

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
	services.AddServices(); // 呼叫 ServiceDIExtensions 進行註冊

	services.AddRepositories();

	services.AddControllers();
}

雖然可以有效把註冊切分出去,但也會沒辦法在同個地方管理所有註冊,略麻煩,還是請真的有需要的時候再嘗試。這邊就當作分享這個做法給大家。

驗證

現在我們已經將三層的內容都改成使用注入了,最後就來測試一下是否有串接成功吧!

下中斷點,可以看到我們 CardService 要求的 ICardRepository 的確傳入了實作的 CardRepository

也成功從資料庫中查詢到卡片了!打完收工~

小結

這篇文章介紹了一些依賴注入的作法,並在 .net Core 的專案上進行實作。這邊就做個小總結:

  • 為什麼要依賴注入

    • 解除類別與類別間的直接耦合

    • 由組合根來負責物件的建立和傳遞,更能讓依賴雙方專注於自己的職責

  • 依賴注入常見的做法有

    • 建構式注入:把依賴對象從建構式扔進去

    • 屬性注入:把依賴對象從公開屬性扔進去

    • 方法注入:呼叫方法時再把依賴對象當參數扔進去

  • .net Core 中的依賴注入

    • 是內建的功能,我們可以在 Startup.cs 的 ConfigureServices 進行註冊

    • .net Core 中,依賴注入的生命週期有

      • Transient(一次性):每次注入都建立一個新的

      • Scoped(作用域):每次 Request 都建立一個新的,同個 Request 重複利用同一個

      • Singleton(單例):只建立一個新的,每次都重複利用同一個

最後還是要叮嚀一下,依賴注入在實務上還會遇到許多眉角,不同的 DI 容器也會提供不同的方法,或是面對眼前的架構不知道從何拆起之類的,這些都是重構日常。

例如說:

等等的問題。

但我個人認為只要有依賴反轉、隔離耦合這些依賴注入的基本觀念,剩下的就是了解工具如何使用、開 Google 查詢有哪些坑的步驟而已了。然後上班一半時間都在 Google

除了藉由依賴注入去解耦合,避免讓類別成為控制狂(就是指類別依賴的對象都自己 new 出來,從建立到生命週期都由類別自己控制,最終變成強耦合的狀況)以外,我們使用依賴注入的時候,有時會發生一些令人困惑的事情,這些其實就是讓我們動手重構的好幫手。

例如說循環依賴,當我們發現A類別依賴了B類別,B類別又依賴A類別而發生錯誤時,就可以考慮是不是能夠整合這兩個類別,例如抽出一個更高階的類別來統合這兩個類別的動作流程等等。

又或是像過度注入:當我們用建構式注入的時候,有時候會發現其中幾個類別的建構式會變得超級肥,可能光是傳入參數就多達二三十個等等。

諸如此類,依賴注入其實可以幫助我們重新梳理我們類別的邊界和耦合關係,讓我們注意到一些需要重構的徵象。這也是最近跟著同事重構的心得哪。

參考資料

其他參考資料

本系列文章

其他文章

並用 ,藉由將控制權轉移給注入容器,解除分層與分層間、類別與類別間的依賴和耦合關係,達到以介面分離實作的目標。

這部份我們之前在已經有詳細的說明,基於江湖道義,接下來就引用該篇的例子來快速帶過一下。

而依賴的低階模組越多,會被影響的機會就越高、修改的範圍和頻率也會急遽地拉高,變得無法掌握修改程式碼時影響的範圍,最終導致架構變得不穩固、程式碼到處都是不健康的。

也就是指,我們可以不要讓高階模組直接去依賴低階模組,而是使用抽象的、具有契約精神的來對他們進行隔離。

如此一來,只要介面的契約成立了,高階模組就可以專心做好自己的事情(),而不用去管低階模組的方法名稱之類的鳥事、低階模組也只要專注在實作介面要求的契約內容就行了。

關於在 .net Core 裡面對多個建構式的對象註冊 DI 的作法,可以參閱黑大的這篇

例如在先前的 我們就在這裡用套件提供的 AddSwaggerGen() 註冊過 Swagger UI 服務。

Image

>)

現在我們稍微了解了 .net Core 裡的注入方式,接著就延續我們在 篇的進度,來把依賴注入導入到我們本系列的 ProjectN 菜雞專案吧。在上一期,我們利用分層的概念將整個流程拆分成 Controller, Service, Repository 三個主要區塊。

註:前篇分層時用到的 AutoMapper 套件的注入方式,請參見 ,此處暫且忽略。

對這個概念不太熟悉的朋友,可以參照 與 的說明

並且由於這個範例的組合根在展示層,因此和的三層式架構圖略有不同,展示層也會依賴到資料存取層,還請注意。

最後再將前面系列用到的一些工具,例如 , 給注入好(這部分就請根據自己專案的內容調整囉),就完成啦。

Image
Image

這時候其實就是一種警示:這個類別為什麼會依賴這個多個對象?是不是這個類別負責的太多了?

那麼今天就先到這兒,祝各位解耦順利、斷開魂結。那麼,我們見~

[

.net Core 實際跑一次依賴注入
依賴反轉原則篇
多型
介面
依賴反轉原則
耦合
介面
職責
ASP.NET Core DI 之多建構式問題
Swagger
HttpClient,該 using 還是 static? - 黑暗執行緒
在 ASP.NET Core 中使用 IHttpClientFactory 發出 HTTP 要求 | Microsoft Docs
單例模式 Singleton Pattern – LinZiyou Dev Blog
Singleton Pattern 介紹,是否為 Anti-pattern? | by Camel | 嗨,世界
ASP.NET Core 2 系列 - 依賴注入 (Dependency Injection) | John Wu’s Blog
不可不知的 ASP.NET Core 依賴注入 - 黑暗執行緒
.Net Core 服務存留期 (Service Lifetime):叡揚部落格
ASP.Net Core DI 容器中 Service 生命週期 | Ray’s Notes
ASP.NET Core 3 系列 - 自行建置 Service Provider | John Wu’s Blog
c# - What are the costs and possible side effects of calling BuildServiceProvider() in ConfigureServices() - Stack Overflow
分層架構
AutoMapper#走向注入
介面
依賴反轉原則
上篇
AutoMapper
Dapper
如何讀取 AppSettings.json 組態設定檔 | 余小章 @ 大內殿堂
ASP.NET Core 練習 - 使用 appSetting - 黑暗執行緒
如何取得 appsettings.json 組態設定 ~ m@rcus 學習筆記
要怎麼對一個介面註冊兩個實作,再根據狀況使用?
是不是可以乾脆把 DI 容器注入到對象中再彈性取用(就像服務定位器)?
職責
下次
依賴注入:原理、實作與設計模式 (Dependency Injection: Principles, Practices, Patterns, 2/e)
鐵人賽 Day04] ASP.NET Core 2 系列 - 依賴注入 (Dependency Injection) | John Wu’s Blog
Dependency Injection 筆記 (5) - Huan-Lin 學習筆記
NET Core 中的相依性插入 | Microsoft Docs
mrkt 的程式學習筆記: ASP.NET MVC 專案分層架構 Part.6 - DI/IoC 使用 Unity.MVC
DI(Dependency injection) 注入方式 - iT 邦幫忙
依賴注入 DI(Dependency Injection) - Sian
IOC(控制反轉) , DI(依賴注入) 深入淺出~~ | 石頭的coding之路 - 點部落 (dotblogs.com.tw)
控制反轉 (IoC) 與 依賴注入 (DI) - NotFalse 技術客
搬砖方法论:组合根(Composition Root)_su9257的博客-CSDN博客
通過 Microsoft.Extensions.DependencyInjection,多個實作如何註冊相同的介面 | 余小章 @ 大內殿堂
依赖注入DI的代替,服务定位器模式 - 简书
Levitation Charm | Harry Potter Wiki | Fandom
Dragon of the Darkness Flame (technique) | YuYu Hakusho Wiki | Fandom
秦帝國以後的郡縣制 - 維基百科
菜雞新訓記 (0): 目錄
菜雞新訓記 (1): 使用 Git 來進行版本控制吧
菜雞新訓記 (2): 認識 Api & 使用 .net Core 來建立簡單的 Web Api 服務吧
菜雞新訓記 (3): 使用 Dapper 來連線到資料庫 CRUD 吧
菜雞新訓記 (4): 使用 Swagger 來自動產生可互動的 API 文件吧
菜雞新訓記 (5): 使用 三層式架構 來切分服務的關注點和職責吧
菜雞新訓記 (6): 使用 依賴注入 (Dependency Injection) 來解除強耦合吧
菜雞新訓記 (7): 使用 FluentValidation 來驗證傳入參數吧
菜雞新訓記 (5): 使用 三層式架構 來切分服務的關注點和職責吧
菜雞新訓記 (2): 認識 Api & 使用 .net Core 來建立簡單的 Web Api 服務吧
C#: BenchmarkDotnet —— 效能測試好簡單
菜雞新訓記 (4): 使用 Swagger 來自動產生可互動的 API 文件吧
菜雞新訓記 (3): 使用 Dapper 來連線到資料庫 CRUD 吧
為什麼要依賴注入
依賴注入的種類(建構式注入、屬性注入、方法注入)
.net Core 中依賴注入的生命週期