貧血領域模型是如何導致糟糕的軟件產生
使用貧血領域模型通常被認為是一種反模式,因為它鼓勵程序員無意義地重復編寫代碼。下面我將簡短(而瑣碎)地用一個例子來闡述這個是如何產生的。我們可以通過細致的規劃以及嚴格的編碼規范來避免其發生,但是同樣可以獲得較好的封裝。防止陷入貧血領域模型深坑的難度隨項目人數呈指數級增長。
我相信所有人對面向對象都有所認識,但我卻有趣地發現一些看似毫無意義的小舉措卻導致了最終一場大災難。
第一步:編寫貧血實體
在軟件開發的某些情況下,我們會在一個領域實體之外實現一些邏輯。這可能是由于一個明確的設計決定或者,更有可能,持久類不能引用外部服務造成了不能將這段邏輯實現在領域對象的內部。把外部服務(依賴)添加到實體對象中將會造成與數據庫的交互變的復雜而晦澀難懂。
- 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需要。
- AbstractUserService
- /\
- |
- |
- ------------------------------------
- | |
- UserValidationService UserReminderService
與此同時另一個開發者也寫了另一個service,這個service是用來給某個Department實體(同樣也使用email地址)發送消息的.這位開發者想要使用AbstractUserService中的郵箱驗證和名字格式化功能,但他的代碼是為Departments服務的,而不是Users,因此,代碼結構中另一層又出現了:AbstractEntiryService.
哪里錯了呢?
我們已經失去了對我們程序結構的控制,我們的開發團隊開始發現很難再寫出干凈的代碼. 我們的類需要比實際需求更多的公共方法來維護復雜的類關系
總結
通過貧血的領域模型來保持代碼結構整潔并且可維護是當然不可能的.然而,當我們能夠使用充血領域模型的時候,維護代碼并且保持類接口簡潔就變得非常容易了.
- public class User {
- //Dependencies
- private 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)抽象出來.生成消息應該被如何抽象/封裝可能是更值得商榷的,但這篇文章就會比我與其的更長了.
我希望你會喜歡這篇文章.
英文原文:How Anaemic Domain Models Cause Bad Software
譯文鏈接:http://www.oschina.net/translate/how-anaemic-domain-models-cause-bad-software