动态

详情 返回 返回

對象命名為何需要避免'-er'和'-or'後綴 - 动态 详情

之前寫過兩篇關於軟件工程中對象命名的文章:開發中對象命名的一點思考對象命名怎麼上手?從現實世界,但感覺還是沒有説透,

在軟件工程中,如果問我什麼最重要,我的答案是對象命名。良好的命名能夠反映系統的本質,使代碼更具可讀性和可維護性。本文通過具體例子,探討為何應該以對象本質而非功能來命名,以及不當命名可能帶來的長期問題。

一個例子

這個例子是我最近看到的一段代碼,用於解釋SOLID中的依賴倒置原則的好處用來隔離變化,代碼如下:

public interface IPaymentProcessor
{
    void ProcessPayment(decimal amount);
}

public class CreditCardPaymentProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        // 信用卡支付的具體實現
    }
}

public class PayPalPaymentProcessor : IPaymentProcessor
{
    public void ProcessPayment(decimal amount)
    {
        // PayPal支付的具體實現
    }
}

 

如之前文章提到,er或or結尾的命名,本質上是動詞+施動者後綴組成的,本質是詞彙匱乏的表現,這種其實可以有很多,比如:

  • Executor(執行者)
  • Handler(處理者)
  • Provider(提供者)
  • Builder(構建者)
  • Dispatcher(調度者)
  • Processor(處理器)
  • Checker(檢查者)
  • Manager(管理者)
  • Converter(轉換者)
  • Watcher(觀察者)
  • Runner(運行者)
  • Fetcher(獲取者)
  • Adapter(適配者)
  • Keeper(保持者)
  • Coordinator(協調者)

 

這些命名在現代軟件工程中非常常見,但並不代表正確,本質是面向過程的命令式編程,而不是面向對象更現代的聲明式編程,會潛移默化影響我們的思維方式。

 

問題在哪

這種命名方式更多強調對象的更能,而非本質,命名應該遵循以事物本質命名,而不是事物做什麼(what the object is, not what it does)。

下面我們以另一個案例來看,例如,我希望設計一個對象,該對象用於滿足人類坐下時的支撐需求,那麼應該叫什麼?如果按照IPaymentProcessor例子中提到的同樣命名規則,則應該使用“人體支撐器”,而不是椅子。

 

下面是代碼示例:

class HumanSupporter {
  supportHuman() { /* ... */ }
}

 

缺乏時間韌性

這種命名,可能是由於在當前上下文中,我們僅考慮椅子用於坐的功能這一點,並沒有考慮未來的需求,後續,例如我們希望在椅子下面儲存一些東西,該怎麼做?

第一種選項是修改對象名稱,以滿足新的需求:

// 選項1:改名(同時修改所有引用...)
class HumanSupporterAndItemStorer { 
  supportHuman() { /* ... */ }
  storeItems() { /* ... */ }
}

 

第二種選項,也是我們實際上使用最多的辦法,無視類名稱,直接硬加一個方法,反正過幾個月這個東西不一定是誰負責了-.-

// 選項2:保留不準確的名稱(誤導接盤俠)
class HumanSupporter {
  supportHuman() { /* ... */ }
  storeItems() { /* ... */ } // 名稱與功能不符
}

 

第三種選項,將功能隔離到一個單獨類中,但隨着這類需求的增多,很多分散的類之間會存在複雜的調用關係,同時新增類由於是臨時起意設計出來,很難在後續的功能中複用:

// 選項3:創建新類(功能分散,關係複雜)
class ItemStorer {
  storeItems() { /* ... */ }
}

 

而當我們使用更符合本質的命名時,代碼演進的節奏如下:

// 初始版本
class Chair {
  sitOn() { /* ... */ }
}

// 第二版本 - 增加存儲功能
class Chair {
  sitOn() { /* ... */ }
  storeItemsUnderneath() { /* ... */ } // 自然擴展,符合椅子的本質
}

// 需要更專業化時,創建子類
class StorageChair extends Chair {
  // 擴展而非替代,保持概念一致性
}

 

基於對象本質的命名可以看出擁有足夠的時間韌性。

 

命名過於抽象或泛化可能導致膨脹

“人體支撐器”這種命名很容易讓類的膨脹顯得合情合理,首先從語義上來看,"-er"/"-or"結尾的詞在語法上創造了一個施動者(agent),但語義邊界不清。"人體支撐器"到底支撐什麼?支撐到什麼程度?

同時強調行為,而淡化對象的本質。

同時"支撐器"從語義學角度存在雙重問題:

上位詞過寬:支撐器是椅子、凳子、桌子、沙發等眾多物品的上位詞,失去了分類的精確性。語言學中,這種上位詞(hypernym)過於寬泛時,語義信息密度大幅降低。同時引起抽象維度的混亂,可能導致很多不相干的內容全部塞進類中。

下位詞過窄:將椅子定義為"支撐器"忽略了其他屬性——舒適性、美學價值、文化符號意義。這是語義要素(semantic features)的不當減少。

 

隨着演進,我們可能看到這樣一個類的膨脹方式:

class HumanSupporter {
    public void supportHuman() {
        // 原始功能
    }
    
    public void maintainPosture() {
        // 第二版添加的功能
    }
    
    // 存儲物品也可以解釋為"支持人類活動"的一部分
    public void storeItems() {
        // 存儲物品的實現
    }
    
    // 在模糊的功能定義下,越來越多不相關的功能被添加進來
    public void provideWarmth() {
        // 提供温暖的實現
    }
    
    public void massageUser() {
        // 按摩功能實現
    }
    
    // 完全不相關的功能也可以通過寬泛解釋而加入
    public void playMusic() {
        // "這也是支撐人類放鬆,對吧?"
    }
    
    public void chargeMobileDevices() {
        // "現代人需要充電,這也是支持現代人類的需求!"
    }
    
    // 隨着時間推移,類可能繼續膨脹...
    public void provideSnacks() {
        // "提供零食也是支撐人體的一種方式!"
    }
    
    public void controlRoomLighting() {
        // "控制燈光也是為了支持人類工作環境!"
    }
    
    // 很多功能都可以塞進這種不當的抽象中...
}

 

從例子中看貌似有點誇張,但只要Codebase生命週期足夠久,就能看到許多瘋狂膨脹的類,如果沒有監督或嚴格的Code Review,人們會傾向於短平快的實現手段,我見過很多後綴為Handler、base、manager的類膨脹到上萬行,被上百處引用。

而使用符合本質的命名時,新增功能如下:

* Chair - 椅子
 * 初始設計:簡單的椅子類
 */
class Chair {
    // 核心功能明確定義了椅子的基本用途
    public void sitOn() {
    }
}

/**
 * Chair - 第二版
 * 增加了新功能,但都嚴格符合"椅子"的本質特性
 */
class Chair {
    public void sitOn() {
        // 坐的實現
    }
    
    // 存儲物品在椅子下方是椅子的自然擴展,符合我們對椅子的理解
    public void storeItemsUnderneath() {
        // 存儲功能實現
    }
    
    // 調整高度也是椅子可能具有的功能
    public void adjustHeight() {
        // 高度調整實現
    }
    
    // 注意:我們不會想到給椅子添加"播放音樂"的功能
    // 因為這明顯不符合我們對"椅子"這個概念的理解
}

/**
 * 當需要更多功能時,我們創建專門的子類
 * 而不是向基類添加不相關的功能
 */
class StorageChair extends Chair {
    // 擴展存儲功能,而不是改變椅子的基本概念
    @Override
    public void storeItemsUnderneath() {
        // 增強的存儲功能實現
    }
    
    // 添加符合"儲物椅"概念的特殊功能
    public void openStorage() {
        // 打開儲物區實現
    }
}

class Massager {
    // 單一職責:專注於按摩功能
    public void massageUser() {
    }
}

// 使用組合將按摩功能添加到椅子中,直接定義,或通過構造函數注入或DI
class MassageChair extends Chair {
    private Massager massager;
    
    // 通過組合添加按摩功能,而不是直接在Chair類中添加
    public void activateMassage() {
    }
}

 

類圖如下:

 

我們可以看到,HumanSupporter (功能性命名) 隨着需求增加容易變得臃腫,因為幾乎任何功能都可以歸為"支持人類",Chair (實體命名) 自然限制了類的職責範圍,不相關功能明顯感覺格格不入,當需要添加新功能時,具體命名引導我們創建專門的子類或使用組合,而不是膨脹基類。

 

命名增加認知負載

HumanSupporter這種不符合我們日常交流中的習慣,屬於開發人員在開發過程中的臨場發揮,現實世界中並沒有“人體支撐器”這種抽象的概念。而椅子(Chair)的概念在現實生活中非常容易理解,其職責和邊界在現實世界這麼多年的演化中基本穩定,那麼在短暫的軟件生命週期中也應該是穩定的。

同時在代碼抽象角度,現實生活中的概念更容易進行抽象,同時抽象維度也會比較合理,例如:

HumanSupporter可能繼承自Supporter,但這個繼承層次是否有意義?這種功能性抽象通常是臨時起意,並不健壯,而Chair、Table可以更自然的抽象成Furniture,這反映了真實世界的抽象規則。

同時在和其他開發人員或業務人員溝通時,請把“請把人體支撐器搬過來”,這種溝通會不會讓人抓狂?

那麼開頭例子該如何重構?

通過易於理解的椅子代碼示例,理解對象命名的重要性,那麼對於開頭的例子IPaymentProcessor接口,直接重構為更符合本質的IPayment即可,有什麼好處?

 

功能擴展對比

IPaymentProcessor:添加功能需修改接口

// 原始接口
public interface IPaymentProcessor {
    void ProcessPayment(decimal amount);
}

// 需要添加退款功能 - 所有實現類都必須修改
public interface IPaymentProcessor {
    void ProcessPayment(decimal amount);
    void ProcessRefund(string transactionId, decimal amount); // 新增方法
}

// 所有實現類都被迫實現新方法
public class PayPalPaymentProcessor : IPaymentProcessor {
    public void ProcessPayment(decimal amount) { /* 原有代碼 */ }
    
    // 即使此支付方式不支持退款,也必須實現
    public void ProcessRefund(string transactionId, decimal amount) {
        throw new NotSupportedException("PayPal不支持退款");
    }
}

IPayment:添加功能通過擴展接口

// 原始接口保持不變
public interface IPayment {
    PaymentResult Execute(decimal amount);
}

// 新增退款接口
public interface IRefundablePayment : IPayment {
    RefundResult Refund(decimal amount);
}

// 只有支持退款的支付方式實現新接口
public class CreditCardPayment : IRefundablePayment {
    private string _lastTransactionId;
    
    public PaymentResult Execute(decimal amount) {
        // 處理支付並記錄交易ID
        _lastTransactionId = "tx_" + Guid.NewGuid().ToString();
        return new PaymentResult { Success = true };
    }
    
    public RefundResult Refund(decimal amount) {
        // 使用交易ID處理退款
        return new RefundResult { Success = true };
    }
}

// 不支持退款的支付方式不需要變更
public class GiftCardPayment : IPayment {
    public PaymentResult Execute(decimal amount) {
        // 禮品卡支付
        return new PaymentResult { Success = true };
    }
}

 

狀態管理

IPaymentProcessor 沒有合適的狀態管理位置

// 處理器沒有內部狀態
public class CreditCardPaymentProcessor : IPaymentProcessor {
    // 狀態必須在外部管理
    public void ProcessPayment(decimal amount) {
        // 從哪裏獲取卡號和有效期?
        
    }
}

 

IPayment:狀態自然封裝

// 支付對象封裝所需的所有狀態
public class CreditCardPayment : IPayment {
    private readonly string _cardNumber;
    private readonly string _expiryDate;
    
    public CreditCardPayment(string cardNumber, string expiryDate) {
        _cardNumber = cardNumber;
        _expiryDate = expiryDate;
    }
    
    public PaymentResult Execute(decimal amount) {
        // 直接使用內部保存的狀態
        return ProcessCreditCardPayment(_cardNumber, _expiryDate, amount);
    }
}

// 使用代碼簡潔明瞭
public void CheckoutCart(ShoppingCart cart, CustomerInput input) {
    var payment = new CreditCardPayment(input.CardNumber, input.ExpiryDate);
    var result = payment.Execute(cart.Total);
}

 

小結

對象命名是軟件工程中最基礎也最重要的環節之一。遵循"以事物本質命名,而非事物功能"的原則,能夠創建更清晰、更穩定、更易於理解和維護的代碼。

一個簡單的辦法是,在日常開發中遇到使用"er"/"or"結尾的對象命名時,需要引起警覺,考慮如何使用反映領域實體本質的命名方式。

 

user avatar yangjiaobaoza 头像 codejourney-blog 头像 buguge 头像
点赞 3 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.