對象命名為何需要避免'-er'和'-or'后綴
之前寫過兩篇關(guān)于軟件工程中對象命名的文章:開發(fā)中對象命名的一點思考與對象命名怎么上手?從現(xiàn)實世界,但感覺還是沒有說透,
在軟件工程中,如果問我什么最重要,我的答案是對象命名。良好的命名能夠反映系統(tǒng)的本質(zhì),使代碼更具可讀性和可維護性。本文通過具體例子,探討為何應(yīng)該以對象本質(zhì)而非功能來命名,以及不當(dāng)命名可能帶來的長期問題。
一個例子
這個例子是我最近看到的一段代碼,用于解釋SOLID中的依賴倒置原則的好處用來隔離變化,代碼如下:
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}
public class CreditCardPaymentProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
// 信用卡支付的具體實現(xiàn)
}
}
public class PayPalPaymentProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
// PayPal支付的具體實現(xiàn)
}
}
如之前文章提到,er或or結(jié)尾的命名,本質(zhì)上是動詞+施動者后綴組成的,本質(zhì)是詞匯匱乏的表現(xiàn),這種其實可以有很多,比如:
-
Executor(執(zhí)行者)
-
Handler(處理者)
-
Provider(提供者)
-
Builder(構(gòu)建者)
-
Dispatcher(調(diào)度者)
-
Processor(處理器)
-
Checker(檢查者)
-
Manager(管理者)
-
Converter(轉(zhuǎn)換者)
-
Watcher(觀察者)
-
Runner(運行者)
-
Fetcher(獲取者)
-
Adapter(適配者)
-
Keeper(保持者)
-
Coordinator(協(xié)調(diào)者)
這些命名在現(xiàn)代軟件工程中非常常見,但并不代表正確,本質(zhì)是面向過程的命令式編程,而不是面向?qū)ο蟾F(xiàn)代的聲明式編程,會潛移默化影響我們的思維方式。
問題在哪
這種命名方式更多強調(diào)對象的更能,而非本質(zhì),命名應(yīng)該遵循以事物本質(zhì)命名,而不是事物做什么(what the object is, not what it does)。
下面我們以另一個案例來看,例如,我希望設(shè)計一個對象,該對象用于滿足人類坐下時的支撐需求,那么應(yīng)該叫什么?如果按照IPaymentProcessor例子中提到的同樣命名規(guī)則,則應(yīng)該使用“人體支撐器”,而不是椅子。
下面是代碼示例:
class HumanSupporter {
supportHuman() { /* ... */ }
}
缺乏時間韌性
這種命名,可能是由于在當(dāng)前上下文中,我們僅考慮椅子用于坐的功能這一點,并沒有考慮未來的需求,后續(xù),例如我們希望在椅子下面儲存一些東西,該怎么做?
第一種選項是修改對象名稱,以滿足新的需求:
// 選項1:改名(同時修改所有引用...)
class HumanSupporterAndItemStorer {
supportHuman() { /* ... */ }
storeItems() { /* ... */ }
}
第二種選項,也是我們實際上使用最多的辦法,無視類名稱,直接硬加一個方法,反正過幾個月這個東西不一定是誰負責(zé)了-.-
// 選項2:保留不準確的名稱(誤導(dǎo)接盤俠)
class HumanSupporter {
supportHuman() { /* ... */ }
storeItems() { /* ... */ } // 名稱與功能不符
}
第三種選項,將功能隔離到一個單獨類中,但隨著這類需求的增多,很多分散的類之間會存在復(fù)雜的調(diào)用關(guān)系,同時新增類由于是臨時起意設(shè)計出來,很難在后續(xù)的功能中復(fù)用:
// 選項3:創(chuàng)建新類(功能分散,關(guān)系復(fù)雜)
class ItemStorer {
storeItems() { /* ... */ }
}
而當(dāng)我們使用更符合本質(zhì)的命名時,代碼演進的節(jié)奏如下:
// 初始版本
class Chair {
sitOn() { /* ... */ }
}
// 第二版本 - 增加存儲功能
class Chair {
sitOn() { /* ... */ }
storeItemsUnderneath() { /* ... */ } // 自然擴展,符合椅子的本質(zhì)
}
// 需要更專業(yè)化時,創(chuàng)建子類
class StorageChair extends Chair {
// 擴展而非替代,保持概念一致性
}
基于對象本質(zhì)的命名可以看出擁有足夠的時間韌性。
命名過于抽象或泛化可能導(dǎo)致膨脹
“人體支撐器”這種命名很容易讓類的膨脹顯得合情合理,首先從語義上來看,"-er"/"-or"結(jié)尾的詞在語法上創(chuàng)造了一個施動者(agent),但語義邊界不清。"人體支撐器"到底支撐什么?支撐到什么程度?
同時強調(diào)行為,而淡化對象的本質(zhì)。
同時"支撐器"從語義學(xué)角度存在雙重問題:
上位詞過寬:支撐器是椅子、凳子、桌子、沙發(fā)等眾多物品的上位詞,失去了分類的精確性。語言學(xué)中,這種上位詞(hypernym)過于寬泛時,語義信息密度大幅降低。同時引起抽象維度的混亂,可能導(dǎo)致很多不相干的內(nèi)容全部塞進類中。
下位詞過窄:將椅子定義為"支撐器"忽略了其他屬性——舒適性、美學(xué)價值、文化符號意義。這是語義要素(semantic features)的不當(dāng)減少。
隨著演進,我們可能看到這樣一個類的膨脹方式:
class HumanSupporter {
public void supportHuman() {
// 原始功能
}
public void maintainPosture() {
// 第二版添加的功能
}
// 存儲物品也可以解釋為"支持人類活動"的一部分
public void storeItems() {
// 存儲物品的實現(xiàn)
}
// 在模糊的功能定義下,越來越多不相關(guān)的功能被添加進來
public void provideWarmth() {
// 提供溫暖的實現(xiàn)
}
public void massageUser() {
// 按摩功能實現(xiàn)
}
// 完全不相關(guān)的功能也可以通過寬泛解釋而加入
public void playMusic() {
// "這也是支撐人類放松,對吧?"
}
public void chargeMobileDevices() {
// "現(xiàn)代人需要充電,這也是支持現(xiàn)代人類的需求!"
}
// 隨著時間推移,類可能繼續(xù)膨脹...
public void provideSnacks() {
// "提供零食也是支撐人體的一種方式!"
}
public void controlRoomLighting() {
// "控制燈光也是為了支持人類工作環(huán)境!"
}
// 很多功能都可以塞進這種不當(dāng)?shù)某橄笾?..
}
從例子中看貌似有點夸張,但只要Codebase生命周期足夠久,就能看到許多瘋狂膨脹的類,如果沒有監(jiān)督或嚴格的Code Review,人們會傾向于短平快的實現(xiàn)手段,我見過很多后綴為Handler、base、manager的類膨脹到上萬行,被上百處引用。
而使用符合本質(zhì)的命名時,新增功能如下:
* Chair - 椅子
* 初始設(shè)計:簡單的椅子類
*/
class Chair {
// 核心功能明確定義了椅子的基本用途
public void sitOn() {
}
}
/**
* Chair - 第二版
* 增加了新功能,但都嚴格符合"椅子"的本質(zhì)特性
*/
class Chair {
public void sitOn() {
// 坐的實現(xiàn)
}
// 存儲物品在椅子下方是椅子的自然擴展,符合我們對椅子的理解
public void storeItemsUnderneath() {
// 存儲功能實現(xiàn)
}
// 調(diào)整高度也是椅子可能具有的功能
public void adjustHeight() {
// 高度調(diào)整實現(xiàn)
}
// 注意:我們不會想到給椅子添加"播放音樂"的功能
// 因為這明顯不符合我們對"椅子"這個概念的理解
}
/**
* 當(dāng)需要更多功能時,我們創(chuàng)建專門的子類
* 而不是向基類添加不相關(guān)的功能
*/
class StorageChair extends Chair {
// 擴展存儲功能,而不是改變椅子的基本概念
@Override
public void storeItemsUnderneath() {
// 增強的存儲功能實現(xiàn)
}
// 添加符合"儲物椅"概念的特殊功能
public void openStorage() {
// 打開儲物區(qū)實現(xiàn)
}
}
class Massager {
// 單一職責(zé):專注于按摩功能
public void massageUser() {
}
}
// 使用組合將按摩功能添加到椅子中,直接定義,或通過構(gòu)造函數(shù)注入或DI
class MassageChair extends Chair {
private Massager massager;
// 通過組合添加按摩功能,而不是直接在Chair類中添加
public void activateMassage() {
}
}
類圖如下:

我們可以看到,HumanSupporter (功能性命名) 隨著需求增加容易變得臃腫,因為幾乎任何功能都可以歸為"支持人類",Chair (實體命名) 自然限制了類的職責(zé)范圍,不相關(guān)功能明顯感覺格格不入,當(dāng)需要添加新功能時,具體命名引導(dǎo)我們創(chuàng)建專門的子類或使用組合,而不是膨脹基類。
命名增加認知負載
HumanSupporter這種不符合我們?nèi)粘=涣髦械牧?xí)慣,屬于開發(fā)人員在開發(fā)過程中的臨場發(fā)揮,現(xiàn)實世界中并沒有“人體支撐器”這種抽象的概念。而椅子(Chair)的概念在現(xiàn)實生活中非常容易理解,其職責(zé)和邊界在現(xiàn)實世界這么多年的演化中基本穩(wěn)定,那么在短暫的軟件生命周期中也應(yīng)該是穩(wěn)定的。
同時在代碼抽象角度,現(xiàn)實生活中的概念更容易進行抽象,同時抽象維度也會比較合理,例如:
HumanSupporter可能繼承自Supporter,但這個繼承層次是否有意義?這種功能性抽象通常是臨時起意,并不健壯,而Chair、Table可以更自然的抽象成Furniture,這反映了真實世界的抽象規(guī)則。
同時在和其他開發(fā)人員或業(yè)務(wù)人員溝通時,請把“請把人體支撐器搬過來”,這種溝通會不會讓人抓狂?

那么開頭例子該如何重構(gòu)?
通過易于理解的椅子代碼示例,理解對象命名的重要性,那么對于開頭的例子IPaymentProcessor接口,直接重構(gòu)為更符合本質(zhì)的IPayment即可,有什么好處?
功能擴展對比
IPaymentProcessor:添加功能需修改接口
// 原始接口
public interface IPaymentProcessor {
void ProcessPayment(decimal amount);
}
// 需要添加退款功能 - 所有實現(xiàn)類都必須修改
public interface IPaymentProcessor {
void ProcessPayment(decimal amount);
void ProcessRefund(string transactionId, decimal amount); // 新增方法
}
// 所有實現(xiàn)類都被迫實現(xiàn)新方法
public class PayPalPaymentProcessor : IPaymentProcessor {
public void ProcessPayment(decimal amount) { /* 原有代碼 */ }
// 即使此支付方式不支持退款,也必須實現(xiàn)
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);
}
// 只有支持退款的支付方式實現(xiàn)新接口
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 };
}
}
狀態(tài)管理
IPaymentProcessor 沒有合適的狀態(tài)管理位置
// 處理器沒有內(nèi)部狀態(tài)
public class CreditCardPaymentProcessor : IPaymentProcessor {
// 狀態(tài)必須在外部管理
public void ProcessPayment(decimal amount) {
// 從哪里獲取卡號和有效期?
}
}
IPayment:狀態(tài)自然封裝
// 支付對象封裝所需的所有狀態(tài)
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) {
// 直接使用內(nèi)部保存的狀態(tài)
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);
}
小結(jié)
對象命名是軟件工程中最基礎(chǔ)也最重要的環(huán)節(jié)之一。遵循"以事物本質(zhì)命名,而非事物功能"的原則,能夠創(chuàng)建更清晰、更穩(wěn)定、更易于理解和維護的代碼。
一個簡單的辦法是,在日常開發(fā)中遇到使用"er"/"or"結(jié)尾的對象命名時,需要引起警覺,考慮如何使用反映領(lǐng)域?qū)嶓w本質(zhì)的命名方式。
浙公網(wǎng)安備 33010602011771號