雖然上面 AI 產的圖有些文字重複,但有時還是會讓我笑一下。
記不記得之前在提到 SRP 時,有說到 :
每個元件都應該僅有一種且唯一種被『 修改的理由 』
其中一個 class 的兩個可能被修改理由 :
所以這裡我比較會看幾點 :
第 1 點我覺得比較需要拿來說說的是『 context 情境 』,這個事實上有點模糊,有時後我也很難說用啥來判斷,但通常我有以下的選項 :
但要用那種呢 ? 我也說不準呢 ~
然後第 2 種就是如下程式碼,一看就知道依賴很多對吧 ?
class OrderService {
constructor(
private paymentService: PaymentService,
private notificationService: NotificationService,
private inventoryService: InventoryService,
private shippingService: ShippingService,
private discountService: DiscountService,
private taxService: TaxService,
private customerService: CustomerService,
private loggingService: LoggingService,
) {}
processOrder(orderId: string, productId: string) {
this.inventoryService; // 檢查庫存
this.paymentService; // 處理付款
this.shippingService; // 處理運輸
this.discountService; // 計算折扣
this.taxService; // 計算稅額
this.customerService; // 確認客戶資料
this.notificationService; // 發送通知
this.loggingService; // 記錄操作
}
}
~小備註~
通常這個東西,我都是用建議的情況下,如果有時間就做,然後這時沒時間我覺得合理的話開張 task 留存之後在重構就好。
簡單的說就是這個 class 是不是只支援到半殘的功能,不論是沒開發,還是放在另一個 class 都算是。
不過認真的說,如果半殘的情況下,還符合業務與 PM 需求,我自已還算可以接受。
下面這個是購物車的範例,事實上有用過購物車的應該都知道,他應該還缺了不少支援購物車行為,例如 removeProduct 。
class ShoppingCart {
private products: Product[] = [];
addProduct(product: Product): void {
this.products.push(product);
}
checkout(){
...
}
}
這裡有幾個方法可以避免或是發現這種情況 :
其中第 3 點就是 :
我們前面提到的 CRP Common Reuse Principle)相同,就是經常被一起使用的,就應該放在一起,不要分開它。
這裡除了指 class 名稱相同的,還有就是名稱不同,但是主職責幾乎是很相同的。
這題如果是在 1 ~ 2 個人開發與維護的小專案,我是覺得很容易發現,因為範圍小,但是如果是同時有 4 個團隊一起在某個 repo 開發,那就會真的每個人都會建出很接近的東西,這題我真的覺得比較麻煩。
還有一種情況就是,有一些比較喜歡自幹的人,不太喜歡去看舊 code 有沒有人有寫過的,通常三不五十就直接重新建個新的。這一題我也只有以下幾個建議 :
但是團隊有沒有時間重構又是一回事了……
例如最常見的 database 範例,UserService 是依賴於抽象 interface Database 而不是依賴 MySQLDatabase 。
如果依賴具體實現,會依賴源變動,可能修改它整個 UserService 都需要修改。像我在最近一次的經驗上,我們有直接依賴整個 logger service,然後要修改就發現每一個地方都要改,變動非常大。
// 定義一個穩定的抽象接口,不會頻繁變動
interface Database {
connect(): void;
query(queryString: string): any;
disconnect(): void;
}
// MySQL 的具體實現
class MySQLDatabase implements Database {
connect() {
console.log("Connected to MySQL");
}
query(queryString: string) {
console.log(`Executing MySQL query: ${queryString}`);
return {};
}
disconnect() {
console.log("Disconnected from MySQL");
}
}
// 使用者只依賴於抽象,不依賴於具體實現
class UserService {
constructor(private db: Database) {}
getUser(id: string) {
this.db.connect();
const result = this.db.query(`SELECT * FROM users WHERE id = ${id}`);
this.db.disconnect();
return result;
}
}
-------------------------------------------
const userService = new UserService(new MySQLDatabase());
userService.getUser("123");
這裡主要會看這幾個點 :
首先第 1 個,還記得 LSP 嗎 ? 如果違反了,然後通常在 class 中的方法直接不做任何事,或是 throw error,通常時都是會給後人留坑。
1. 子型態 (subtype) 必須能夠替換掉它們的基底型態 ( base type )
> 2. IS-A 是關於行為的
~備註~
我自已覺得 Refused Bequest 這個 Bad Smell 本質上和違反 LSP 概念是一樣的。
然後由於繼承事實上有不少缺點如下 :
所以不少本書都有提到一件事 :
用組合來代替繼承
例如下面的範例
// 定義行為接口
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
// 飛行行為的實現
class CanFly implements Flyable {
fly() {
console.log('I can fly');
}
}
class CannotFly implements Flyable {
fly() {
console.log('I cannot fly');
}
}
// 游泳行為的實現
class CanSwim implements Swimmable {
swim() {
console.log('I can swim');
}
}
class CannotSwim implements Swimmable {
swim() {
console.log('I cannot swim');
}
}
// 動物類別,不使用繼承,而是組合具體的行為
class Animal {
private flyBehavior: Flyable;
private swimBehavior: Swimmable;
constructor(flyBehavior: Flyable, swimBehavior: Swimmable) {
this.flyBehavior = flyBehavior;
this.swimBehavior = swimBehavior;
}
performFly() {
this.flyBehavior.fly();
}
performSwim() {
this.swimBehavior.swim();
}
}
// 創建不同的動物,根據具體情況組合行為
const eagle = new Animal(new CanFly(), new CannotSwim());
const penguin = new Animal(new CannotFly(), new CanSwim());
const dolphin = new Animal(new CannotFly(), new CanSwim());
然後這裡還有第二種方法來實現組合,那就是 Mixin 這個特性在不少語言都有,它們允許將功能增加到類別中。
// 定義一個基礎類
class Animal {
name: string
constructor(name: string) {
this.name = name
}
}
// 定義一個混入行為,要求基類必須有 name 屬性
function Flyable<T extends { new (...args: any[]): { name: string } }>(
Base: T
) {
return class extends Base {
fly() {
console.log(`${this.name} is flying!`)
}
}
}
// 定義另一個混入行為,要求基類必須有 name 屬性
function Swimmable<T extends { new (...args: any[]): { name: string } }>(
Base: T
) {
return class extends Base {
swim() {
console.log(`${this.name} is swimming!`)
}
}
}
// 使用 Mixin 創建具有多重行為的類
class Duck extends Flyable(Swimmable(Animal)) {
constructor(name: string) {
super(name)
}
}
const duck = new Duck('Daffy')
duck.fly() // Output: Daffy is flying!
duck.swim() // Output: Daffy is swimming!
在這篇文章中,我們探討了許多與 class 設計 有關的常見檢查點,涵蓋了單一職責原則、具體實現的依賴、繼承問題、class 重複等方面的問題,然後列出了以下幾點看點。
總結來說,良好的 class 設計 應該:
這些原則不僅能提升代碼的可讀性和可擴展性,還能使團隊在長期開發和維護中更加高效。但是團隊有沒有時間做這些事情,又是另一個世界了……