今天主要來談談耦合性三大原則 :
在元件的依賴圖中,不能有環
事實上就是我們常說的,不要有循環依賴
,也就是如下的情況 :
這事實上在實務上非常的常見,例如在我們開發時,假設有兩個 service 分別為 userService 與 courseService,那這個時後如果有個需求是取得用戶有的課程數,而另一個需求是取得這個課程有多少學生,那這樣是不是就很容易產生了 ?
class UserService {
signup(){
console.log('sign up')
}
getUserCourses(userId: string): string[] {
// UserService 依賴 CourseService
return courseService.getCoursesByUser(userId);
}
getUsersByIds(userIds: string[]){
return [UserA, userB, userC]
}
}
class CourseService {
publishCourse(){
console.log('publish a course')
}
getCourseStudents(courseId: string){
const studenetIds = [1,2,3]
const students = userService.getUsersByIds(studenetIds);
return students;
}
getCoursesByUser(userId: string){
return [CourseA, CourseB]
}
}
首先要先說一下,很多人會因為循環依賴導致載入有問題,然後就直接用 DI 就可以解決,那這樣是不是就符合 ADP 呢 ? 不是喔 ~ 它真正的目的是要 :
解耦,不然整個就是個大泥球
然後接下來簡單說一下,有幾個地方可能會發生問題 :
大部份整個循環依賴產生的原因都是想用啥就用啥
,例如 userService 因為業務需求,要去抓 course 相關的東西,就很直覺得去 courseService。整體來說就是沒有考慮元件與元件的上下游關係。
將相互依賴的元件進行拆分,並且讓元件有方向性。
最常見的,應該就是引入一個中介層來解決這件事 :
class UserService {
signup(){
console.log('sign up')
}
getUsersByIds(userIds: string[]){
return [UserA, userB, userC]
}
}
class CourseService {
publishCourse(){
console.log('publish a course')
}
}
class UserCourseService {
getUserCourses(userId: string): string[] {
return this.getCoursesByUser(userId);
}
getCourseStudents(courseId: string){
const studenetIds = [1,2,3]
const students = userService.getUsersByIds(studenetIds);
return students;
}
private getCoursesByUser(userId: string){
return [CourseA, CourseB]
}
}
然後事實上看下面的圖就很清楚他們的關係,從循環變成有方向性的依賴,然後事實上也讓職責有點模糊的,變的比較好一些 :
原本:
變成:
雖然這樣拆應該還有很多問題,但事實上就是在解這個過程中,可以慢慢看出那些是放在一起的。
一個元件,應該只依賴比它更穩定的元件
那什麼是更穩呢?簡單的說就是元件不容易被修改,然後在《無瑕的程式碼 敏捷完整篇》,它用下面這張圖來表達穩定。
我自已是覺得有點怪怪的,但也有可能我自已還沒有悟了。然後我覺得怪怪地方在於 :
一個元件如果給越多人用,某些方面來說是好事,但相對來說你為了別人而改變的機率也變大了,infra 層還合理,但 domain 層就會讓我覺得有點危險。
所以事實上我自已比較傾向根據 SRP 原則的那張圖來看再加上變動頻率來一起看。
先說一下變動頻率,假設我們現在是 logger 這種類型的元件,事實上我們通常做完後,應該也幾乎不太會動它,所以整體來說根據下面那張圖加上這個,整理如下三點。
然後為啥要依賴比它更穩定的元件呢 ?
假設我們現在有 A 元件 (穩定) 與 B 元件 (不穩定),然後現在的依賴方向如下 :
A → B(不穩定)
那結果會發生什麼事呢 ? 那就是每一次 B 的修改都可能會影響 A,而且因為 B 不穩定,所以這也代表可能你每個星期都會因為 B 的變動,而需要改 A。
這樣 A 的維護者一定會打死 B,但實際上是依賴方向錯了,是 A 自已找死。所以應該是改成
B -> A(穩定)
《Clean Architecture》書中認為 :
- 一個穩定的元件應該也是抽象的
- 一個不穩定的元件應該是具體的
首先我們來說說穩定的範例,以 logger 來說說,它有以下幾個特點 :
所以通常它會這樣設計,這樣的好處就是我們裡面如何修改,外面基本上幾乎完全不會影響到。
interface ILogger {
log(message: string): void;
error(message: string): void;
}
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[LOG]: ${message}`);
}
error(message: string): void {
console.error(`[ERROR]: ${message}`);
}
}
class FileLogger implements ILogger {
log(message: string): void {
// 假設這裡是寫入文件的實現
console.log(`[FILE LOG]: ${message}`);
}
error(message: string): void {
// 假設這裡是寫入文件的錯誤實現
console.log(`[FILE ERROR]: ${message}`);
}
}
接下來,說說不穩定應該的元件應該是具體的範例,我們以 notficationService 來當範例。我們現在的程式碼如下,然後主要幾個點在於 :
問題出在那呢 ?
那就是我們實際上需求變動時,是很容易動到 sendNotification 要帶入的東西,例如我們信件說不定要帶寄件者,然後 sms 可能還需要帶電話號碼之類的。
所以他這裡反而建議不要抽象,反而具體點比較好。
interface INotificationService {
sendNotification(userId: string, message: string): void;
}
class EmailNotificationService implements INotificationService {
sendNotification(userId: string, message: string): void {
console.log(`Sending email to user ${userId}: ${message}`);
}
}
class SmsNotificationService implements INotificationService {
sendNotification(userId: string, message: string): void {
console.log(`Sending SMS to user ${userId}: ${message}`);
}
}
簡單用一句話來整理一下今天的重點。
根據這三個原則,應該可以設計出更低耦合的服務,大概啦… 我自已感覺有些東西都還有沒有悟。