DI 這個詞應該在軟體工程圈中有些過程式的人,應該多多少少都有聽過,但很多情況下我聽到說 DI 的好處就是好測試,所以這一篇文章我打算來理一下,到底 DI 的好處到底是什麼呢 ? 並且還有和一些常聽到的名詞 IoC、DIP 的關係是啥 ~
簡單的說就是用注入的方式,來具備鬆耦合 ( loosely coupled )
如果以最簡單的範例來看,大概就長的如下,就是依賴的東西,用注入的方式 :
class CourseService {
private logger: ILogger;
constructor(logger: ILogger) { // <--------------- 注入
this.logger = logger;
}
public getCourses() {
this.logger.log('Fetching courses...');
return ['Course 1', 'Course 2', 'Course 3'];
}
}
---
// 使用的方式
const logger = new Logger()
const courseService = new CourseService(logger);
有什麼好處呢 ? 為什麼我不能直接在 constructor 裡直接 new 一個 logger 呢 ?
這個應該就是每個人都會想到的第一個好處理,假設我們現在準備要測試以下的程式碼 :
interface ILogger {
log(message: string): void;
}
class GcpLogger implements ILogger {
log(message: string): void {
....
}
}
class CourseService {
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
public getCourses() {
this.logger.log('Fetching courses...');
return ['Course 1', 'Course 2', 'Course 3'];
}
}
const logger = new GcpLogger()
const courseService = new CourseService(logger);
但我們測試時,就是不想要送到 GCP,所以通常會改成如下:
import { CourseService } from './course.service';
import { ILogger } from './logger.service';
describe('CourseService', () => {
let courseService: CourseService;
let logger: ILogger;
beforeEach(() => {
logger = {
log: jest.fn(),
};
courseService = new CourseService(logger);
});
it('should return a list of courses', () => {
const courses = courseService.getCourses();
expect(courses).toEqual(['Course 1', 'Course 2', 'Course 3']);
});
});
可以明確的知道這個 servcie 的職責與依賴關係
以下面程式碼為範例來看,我們很明確的知道,這個 service 依賴 Logger。
class CourseService {
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
public getCourses() {
this.logger.log('Fetching courses...');
return ['Course 1', 'Course 2', 'Course 3'];
}
}
但這樣就算寫成如下,不是也是一樣 ?
import { LoggerService } from './logger.service';
class CourseService {
private logger: ILogger;
constructor() {
this.logger = new LoggerService();
}
getCourses(name: string): void {
this.logger.log('Fetching courses...');
return ['Course 1', 'Course 2', 'Course 3'];
}
}
就是在擴展時,不會影響到原程式碼
這裡有兩個範例,首先第一個是如果只有 courseService 要用 consoleLoggerService,而 userService 要用 fileLoggerService,則只要如下修改就好。
重點在於我完全沒修改到 courseService 與 userService
~
import { ILogger } from './logger.interface';
class ConsoleLoggerService implements ILogger {
log(message: string): void {
console.log(`ConsoleLoggerService: ${message}`);
}
}
class FileLoggerService implements ILogger {
log(message: string): void {
console.log(`FileLoggerService: ${message}`);
}
}
//------------------------------------------------------
class CourseService {
private logger: ILogger;
constructor(ILogger: logger) {
this.logger = logger;
}
getCourses(name: string): void {
this.logger.log('Fetching courses...');
return ['Course 1', 'Course 2', 'Course 3'];
}
}
class UserService {
private logger: ILogger;
constructor(ILogger: logger) {
this.logger = logger;
}
createUser(name: string): void {
this.logger.log('create user...');
}
}
// 使用的地方
const consoleLogger = new ConsoleLoggerService()
const courseService = new CourseService(consoleLogger);
const fileLogger = new FileLoggerService()
const userService = new UserService(fileLogger)
根據《依賴注入 : 原理、實作與設計模式》這本書中提中的,這個答案是 不一定。
這題我在開發時,也有想過這件事,我什麼東西都要用 DI 嗎 ? 那是不是我的 import 只會剩下 interface 或 constant 而以 ? 這裡怎麼想都怪怪對吧 ?
根據上面那本書中提到的,他會將依賴關係分成以下兩個 :
書中列的穩定性 ( stable 就是可以不用的 ) 有以下的特點 :
不過我自已認真說 ~ 有點難理解與定義呢 ~ 所以我自已是這樣判斷 :
這只是純個人的想法 ~ 沒有一定是對 ~ 參考就好 ~
然後接下來,我還有找到一些會讓我有這個疑問的原因,那就是monkey patch
。
傳統上應該有些人認為因為 DI 是因為講 c# 或 java 這類靜態語言所以才需要使用,但在一起動態語言上,我們可以使用 moneky patch (指非常簡單的方式猴子般地修改現有程式碼)的方式直接修改一個物件的內容,這也才產生,我當初的疑惑。
然後下面也有兩篇也有談到相關的主題 :
Module Requiring vs Dependency Injection in Javascript
Monkey Patching vs. Simple Dependency Injection in modern Python
其中第二篇文章有提到,雖然在一些動態語言上,可以使用 monkey patch 的方式來讓我們輕鬆修改 object 然後測試,但還是建議使用 DI,主要的原因在於可能會有以下的問題 :
整體來說我還是讚成使用 DI 來進行依賴注入。
IoC 根據 Martin Fowler 的這一篇文章《 Inversion Of Control 》 來說,主要的概念如下 :
控制反轉的主要的想法是將控制流從應用程序轉移到框架,讓框架來調用應用程序的代碼,而不是應用程序自己控制什麼時候調用哪些方法。這種模式的多樣性體現在不同的實現技術上,如依賴注入、事件訂閱等。
上面有點文謅謅的,簡單的說就是 :
實體化依賴物件的方向,從我們自已控制產生,轉移到框架上。
事實上就是我們這個的範例 :
// 非 IoC
class UserService {
private userRepository: UserRepository;
constructor() {
// 直接在類中創建依賴
this.userRepository = new UserRepository(); // = <--------- 我自已決定啥時產物件
}
getUser(id: number): string {
return this.userRepository.getUserById(id);
}
}
// IoC
class UserService {
private userRepository: UserRepository;
// 依賴由外部傳入,實現 IoC
constructor(userRepository: UserRepository) { // <----------- 由框架(IoC 容器)決定
this.userRepository = userRepository;
}
getUser(id: number): string {
return this.userRepository.getUserById(id);
}
}
簡單複習一下 DIP,如下,詳細的請回頭看看這篇文章 ~ Day-03 : 設計原則之 SOLID - ISP、DIP
然後他們兩個重點都是反轉
,但兩個是指不同的東西。
IoC 的反轉是指依賴物件實體化的方向,如下圖。
DIP 反轉重點在依賴關係,如下圖。
最後為什麼會說他們又互相需要呢 ?
因為 DIP 這句
兩者都應該依賴於抽象介面
,那這樣高階模組實際上要用時,要如何使用呢 ? 因為 new 不出來,所以只能請外部產,而不是自已產。
這篇文章中我們理了一下以下幾個重點 :
事實上就已經將整個 DI 體系的基本理論概念都已經理的差不多了 ~ 這些都是前人挖掘很久解耦合的方法與實作都很常見的東西 ~ 基本上在探索軟體工程維護性的東西一定必經的一條路呢 ~