洞見與分析:貧血領域模型是如何導致糟糕的軟體產生?

使用貧血領域模型通常被認為是一種反模式,因為它鼓勵程序員無意義地重複編寫代碼。下面我將簡短(而瑣碎)地用一個例子來闡述這個是如何產生的。我們可以通過細緻的規劃以及嚴格的編碼規範來避免其發生,但是同樣可以獲得較好的封裝。防止陷入貧血領域模型深坑的難度隨項目人數呈指數級增長。

我相信所有人對面向對象都有所認識,但我卻有趣地發現一些看似毫無意義的小舉措卻導致了最終一場大災難。

第一步:編寫貧血實體

在軟件開發的某些情況下,我們會在一個領域實體之外實現一些邏輯。這可能是由於一個明確的設計決定或者,更有可能,持久類不能引用外部服務造成了不能將這段邏輯實現在領域對象的內部。把外部服務(依賴)添加到實體對象中將會造成與數據庫的交互變的複雜而晦澀難懂。

public class User { private final String name; private final String emailAddress; public User(final String name, final String emailAddress) { this.name=name; this.emailAddress=emailAddress;} public String getName() { return this.name;} public String getEmailAddress() { return this.emailAddress;}}

第二步:邏輯被實現在外部類中

一個開發組的成員決定他們需要一個用來操作這個實體的方法。這個方法(在我們的例子中)要調用到User對象,但它還需要用到一個User類所不知道的外部服務。這段邏輯被實現在一個幫助類(helper)或者說一個服務類(service)的方法中,並且以某種方式協助了這個實體。這個幫助類不包含自帶的數據,並且僅僅從這個實體中獲取數據、修改其狀態。

public class UserReminderService { // 用戶提醒服務private IMailService mailService; // 郵件服務private IMessageGeneratorService messageGeneratorService; // 消息生成服務public void sendReminderMessage(final IUser user) { // 發送一個提醒String reminderMessage = this.messageGeneratorService.generateReminderMessage(user.getName); this.mailService.sendMessage(user.getEmailAddress(), reminderMessage);}...} 

這個並不能實現在User實體中,因為我們根本無法在實體中取得郵件服務或者是消息生成器。到目前為止,這個看起來還不算很糟糕(我們很好地封裝了消息的創建以及郵件發送過程),但是這僅僅是“敗壞”的開始,然後馬上開始讓這些不警惕的開發者陷入災難。

哪裡錯了呢?

UserReminderService是一個遊手好閒的類(它掌管了太多其他類的活動)。消息的創建、把它發送出去這些都應該是User類自己的業務邏輯。

第三步:重複代碼產生

在此期間,另一個開發者開發了一個全新的組件,同樣也使用了User實體。這個新的服務被用來決定註冊用戶是真的用戶而不是一個機器人。

public class SignupVerificationService { // 註冊確認服務private IMailService mailService; // 郵件服務private IMessageGeneratorService messageGeneratorService; // 消息生成服務public void sendVerificationEmail(final IUser user) { // 發送確認郵件String verificationMessage = this.messageGeneratorService.generateVerificationMessage(user.getName); this.mailService.sendMessage(user.getEmailAddress(), reminderMessage);}}

這個開發者可能會發現這個方法與之前的sendReminderMessage方法十分的相似。在這個情況下,他覺得他把驗證功能與其他組件分開來的做法十分精明,看上去沒有必要為這短短兩行代碼重用之前的實現。

哪裡錯了呢?

這兩個方法看上去十分相似,但是又是不同的,使得開發者認為是兩個不同的活動。這裡有一種冗餘的感覺,但還沒有造成問題。

第四步:邏輯變更

從長遠來看,越簡單的代碼會變的越複雜。在這個迭代後期,我們的開發者在sendReminderMessage方法中添加了一些更復雜的邏輯(預處理用戶名和校驗郵箱地址)。

public void sendReminderMessage(final IUser user) {String formattedUserName = formatUserNameForMessage(user.getName());String reminderMessage = this.messageGeneratorService.generateReminderMessage(formattedUserName); if (isEmailAddressValid(user.getEmailAddress()) { this.mailService.sendMessage(user.getEmailAddress(), reminderMessage);}} public boolean isEmailAddressValid(final String emailAddress) { // 是否郵箱地址有效return emailAddress.contains('@');} public String formatUserNameForMessage(final String userName) { // 為消息格式化用戶名return userName.toUpperCase();}

我們現在有了sendReminderMessage方法的新版本(雖然是一個很簡陋的驗證系統),使得(曾經相似的)UserReminderService變得相當不同。

哪裡錯了呢?

用來給向用戶發送消息的過程發生了變化 (需要進行校驗). 由於該過程沒有包含在User類內部,我們就必須追蹤它在所有不同形式下的所有實現,然後對它們進行修改. 假設我們意識到SignupVerificationService也需要校驗,然後我們為它添加了校驗, 我們仍然需要一種能夠重複使用這端校驗代碼的方法.在需要校驗的情況下,我們可能會把方法封裝到mailService中,但對於其他的邏輯,比如用戶姓名格式化,已經被加入到不同的helper/service類中了,該怎麼辦呢?. 這些代碼可能會被多個service類所需要,也可能只有一個service需要.

第五步: 災難來臨

不能被很容易的移入一個外部依賴(比如,mail service)的代碼就不得不被service類所共享. 將這些方法放進一個超類中供兩個service共享貌似是個好主意. 我們可以看到那樣形成的代碼對原始的domain沒有影響. 不再是User,Message和MailService,我們最終以一群奇怪的生物,就像AbstractUserService, UserValidationService, UserReminderService等等告終. 很快,我們就很難知道新的代碼真正屬於哪部分,也很難知道需要寫那些新代碼的方法所處的位置是否會影響它的使用或者重用.

AbstractUserService/\||------------------------------------| |UserValidationService UserReminderService

與此同時另一個開發者也寫了另一個service,這個service是用來給某個Department實體(同樣也使用email地址)發送消息的.這位開發者想要使用AbstractUserService中的郵箱驗證和名字格式化功能,但他的代碼是為Departments服務的,而不是Users,因此,代碼結構中另一層又出現了:AbstractEntiryService.

哪裡錯了呢?

我們已經失去了對我們程序結構的控制,我們的開發團隊開始發現很難再寫出乾淨的代碼. 我們的類需要比實際需求更多的公共方法來維護複雜的類關係

總結

通過貧血的領域模型來保持代碼結構整潔並且可維護是當然不可能的.然而,當我們能夠使用充血領域模型的時候,維護代碼並且保持類接口簡潔就變得非常容易了.

public class User { //Dependenciesprivate IMailService mailService; private IMessageService messageService; private final String name; private final String emailAddress; public User(final String name, final String emailAddress) { this.name=name; this.emailAddress=emailAddress;} public void sendReminderMessage() {deliverMessage( this.messageGeneratorService.generateReminderMessage(this.getName));} public void sendVerificationEmail() {deliverMessage(this.messageGeneratorService.generateVerificationMessage(this.getName));} private void deliverMessage(final String message) { if (isEmailAddressValid(user.getEmailAddress()) { this.mailService.sendMessage(user.getEmailAddress(), reminderMessage);}} public String getName() { return this.name;}}

注意,我們不再需要email地址的get方法,而且,如果你能原諒我玩數字遊戲,我們增加了兩個User類的公共方法二不是引入兩個(至少)額外的類. 當我們在適當的對象上執行方法的時候比在一個不自然的service對象上執行方法看起來更直觀.

MailService和MessageServices仍被允許留在系統中因為它們的角色很明確. 傳送郵件是一個清晰的架構問題,應該被從領域對象中通過接口(IMailService)抽象出來.生成消息應該被如何抽象/封裝可能是更值得商榷的,但這篇文章就會比我與其的更長了.

我希望你會喜歡這篇文章.


洞見與分析:貧血領域模型是如何導致糟糕的軟件產生?


分享到:


相關文章: