自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

DDD領(lǐng)域驅(qū)動設(shè)計:如何應(yīng)對業(yè)務(wù)需求變化

開發(fā)
關(guān)于領(lǐng)域模型的設(shè)計分享,要追溯到兩個月之前了,這中間搞了一些有的沒有的東西,比如糾結(jié)于倉儲等,說這些東西不重要,其實也蠻重要的,因為它是一個完整應(yīng)用程序所必須要考慮的東西(Demo 除外),但是相對于領(lǐng)域模型,在領(lǐng)域驅(qū)動設(shè)計中它才是最重要的。

領(lǐng)域驅(qū)動設(shè)計的核心-Domain Model(領(lǐng)域模型),這個大家都知道,可是,上次關(guān)于領(lǐng)域模型的設(shè)計分享,要追溯到兩個月之前了,這中間搞了一些有的沒有的東西,比如糾結(jié)于倉儲等,說這些東西不重要,其實也蠻重要的,因為它是一個完整應(yīng)用程序所必須要考慮的東西(Demo 除外),但是相對于領(lǐng)域模型,在領(lǐng)域驅(qū)動設(shè)計中它才是最重要的。

這篇博文我分享的思路是:一個具體的業(yè)務(wù)場景,一個現(xiàn)實項目的業(yè)務(wù)需求變化,應(yīng)用領(lǐng)域驅(qū)動設(shè)計,看我是如何應(yīng)對的???

注意:上面我用的是問號,所以,必不可少的會有一些“坑”,大家在讀的過程中,要“小心”哦。

具體業(yè)務(wù)場景

具體業(yè)務(wù)場景?沒錯,就是我們熟悉的博客園站內(nèi)短消息,詳見:[網(wǎng)站公告]8月17日14:00-15:00(周日下午)發(fā)布新版站內(nèi)短消息。

上面那次版本發(fā)布,已經(jīng)過去一個多月的時間了,說是“新版”,其實就是重寫之前短消息的代碼,然后用領(lǐng)域驅(qū)動設(shè)計的思想去實現(xiàn),界面換了個“位置”,功能和原來的沒有太大變化。發(fā)布之后,出現(xiàn)了很多的問題,比如前端界面、數(shù)據(jù)庫優(yōu)化、代碼不規(guī)范等等。有些技術(shù)問題可以很快的解決,比如數(shù)據(jù)庫的索引優(yōu)化等,但是,有些問題,比如 SELECT FileName,因為程序代碼是基于領(lǐng)域驅(qū)動設(shè)計的思想去實現(xiàn)的,那你就不能直接去寫select filename1,filename2,filename2... from tablename這樣的 SQL 代碼,所以實現(xiàn)起來需要思考很多,這個是比較頭疼的。

我為什么會說這些問題?因為這些問題,只有在實際應(yīng)用項目中才會出現(xiàn),你搞一個領(lǐng)域驅(qū)動設(shè)計的簡單 Demo,會出現(xiàn)數(shù)據(jù)庫性能問題嗎?肯定不會,那也就不會去思考倉儲的一些問題,更談不上一些改變了,所以領(lǐng)域驅(qū)動設(shè)計的推進(jìn),只有你去實際用它,而不只是做一些演示的東西,在實際應(yīng)用中,去發(fā)現(xiàn)問題并解決問題,我覺得這樣才會更有價值。

關(guān)于短消息這個業(yè)務(wù)場景,其實我之前寫的一些領(lǐng)域驅(qū)動設(shè)計博文,都是圍繞著它展開的,很多園友認(rèn)為這個業(yè)務(wù)場景是我虛構(gòu)的,就像之前 netfocus 兄問我:“你說的這個短消息是不是類似于博客園的短消息?”,我回答:“是的!”,呵呵。后來我發(fā)現(xiàn)虛構(gòu)的業(yè)務(wù)場景,有時候很難說明問題,比如之前 Jesse Liu 在一篇博文中,提到一個用戶注冊問題,關(guān)于這個問題,其實討論了很久,但***結(jié)果呢?我認(rèn)為是沒有結(jié)果,因為業(yè)務(wù)場景是虛構(gòu)的,所以就會造成“公說公有理,婆說婆有理”的情況,以至于大家很難達(dá)成一些共識的點。

博客園短消息的業(yè)務(wù)場景,真實的不能再真實了,畢竟大家都在實際用,我就不多說了,就是簡單的一個用戶和另一個用戶發(fā)消息,然后進(jìn)行回復(fù)什么的,在之前的一些博文中也有說明,大家可以參考下:我的“***次”,就這樣沒了:DDD(領(lǐng)域驅(qū)動設(shè)計)理論結(jié)合實踐。

業(yè)務(wù)需求變化

現(xiàn)在的博客園短消息,為了方便用戶看到之前回復(fù)的一些內(nèi)容,我們在底部增加了“=== 下面是回復(fù)信息 === ”,示意圖:

這種方式其實就是把之前回復(fù)內(nèi)容放到新消息內(nèi)容里面,然后作為一個新消息進(jìn)行發(fā)送,除去消息內(nèi)容“冗余”不說,還有個弊端就是,如果兩個人回復(fù)的次數(shù)很多,你會發(fā)現(xiàn),消息內(nèi)容會變成“一坨XX”,不忍直視。

后來,我們也看不下去了,所以決定做一些改變,仿照 iMessage 或 QQ 那種消息模式,示意圖:

這種方式和上面那“一坨XX”形成了鮮明對比,對話模式的消息顯示,使用戶體驗度更好,就像兩個人面對面說話一樣,很輕松也很簡潔,我想我們這種方式的改變,會讓你“愛上”我們短消息的。

對,沒錯,這就是業(yè)務(wù)需求變化,我們在應(yīng)用程序開發(fā)的過程中,需求是一直不斷變化的,我們要做的就是不斷完善和適應(yīng)這種需求變化,當(dāng)然每個人應(yīng)對的方式不同,下面看一下我“愚蠢”的應(yīng)對。

“愚蠢”的應(yīng)對

我個人覺得這一節(jié)點內(nèi)容非常重要,在領(lǐng)域驅(qū)動設(shè)計的過程中,也是很多人常掉進(jìn)的“坑”,因為我們長期受“腳本模式”的影響,在業(yè)務(wù)需求變化后,應(yīng)用程序需要做出調(diào)整,但是你會不自覺的“跑偏”,這就偏離了領(lǐng)域驅(qū)動設(shè)計的思想,***使你的應(yīng)用程序變得“不倫不類”。

當(dāng)時為了很快的在應(yīng)用程序中實現(xiàn)這種功能,我說的是技術(shù)上實現(xiàn),完全沒有用領(lǐng)域驅(qū)動的思想去考慮,我是怎么思考的呢?先從 UI 上考慮,主要是兩個界面:

  • 消息列表:收件箱、發(fā)件箱和未讀消息列表。
     
  • 消息詳情:消息詳情頁。

消息列表實現(xiàn)

之前短消息不管發(fā)送,回復(fù),還是轉(zhuǎn)發(fā),都是作為一個新短消息進(jìn)行發(fā)送的,“消息的上下文”作為一個消息的附屬體,放在新短息內(nèi)容中,也就是說,你把之前發(fā)送的消息刪掉,在新回復(fù)的短消息內(nèi)容中,是仍然看到之前發(fā)送內(nèi)容的,這個在列表的顯示就是單獨進(jìn)行顯示,但新的需求變化就不能這樣進(jìn)行操作了,這個就有點像兩個人聊一個話題,里面都是我們針對這個話題進(jìn)行討論的內(nèi)容,在列表顯示的時候,首先,標(biāo)題顯示就是這個話題的標(biāo)題,就像郵件回復(fù)一樣,我們可以加上“消息標(biāo)題(3)”,這個“3”,就表示兩個人回復(fù)了3次。

其實用話題這個邏輯是有些不準(zhǔn)確的,畢竟我們是短消息項目,我們可以這樣想,我給 netfocus 發(fā)了一個標(biāo)題為:“打個招呼”,內(nèi)容為:“hello netfocus”的消息,然后他給我進(jìn)行了回復(fù):“hello xishuai”,可能后面還有一些消息回復(fù)內(nèi)容,但都是針對我發(fā)的***條消息回復(fù),也就是說下面都是回復(fù)內(nèi)容,那這個在消息列表顯示的時候,標(biāo)題就顯示為“打個招呼(3)”,后面時間為***回復(fù)時間,示意圖:

上面是 netfocus 的收件箱示意圖,收件箱列表顯示的邏輯就是以發(fā)件人和標(biāo)題為一個標(biāo)識,比如 Jesse Liu 也給 netfocus 發(fā)了一個“打個招呼”的消息,雖然標(biāo)題一樣,但發(fā)件人不一樣,所以列表顯示兩條消息。

那代碼怎么實現(xiàn)這個功能呢?貼出代碼看看:

  1. public async Task<IEnumerable<MessageListDTO>> GetInbox(Contact reader, PageQuery pageQuery)  
  2. {  
  3.     var query = efContext.Context.Set<Message>()  
  4.         .Where(new InboxSpecification(reader).GetExpression()).GroupBy(m => new { m.Sender.ID, m.Title }).Select(m => m.OrderByDescending(order => order.ID).FirstOrDefault());  
  5.     int skip = (pageQuery.PageIndex - 1) * pageQuery.PageSize;  
  6.     int take = pageQuery.PageSize;  
  7.  
  8.     return await query.SortByDescending(sp => sp.ID).Skip(skip).Take(take)  
  9.         .Project().To<MessageListDTO>().ToListAsync();//MessageListDTO 為上一版本遺留問題(Select FileName),暫時沒動。  
  10. }  

GetInbox 是 MessageRepository 中的操作,其實原本收件箱的代碼不是這樣處理的,你會看到,現(xiàn)在的代碼其實就是 Linq 的代碼拼接,我當(dāng)時這樣處理就是為了可以方便查詢,現(xiàn)在看確實像“一坨XX”,代碼我就不多說了,上面列表顯示功能是可以實現(xiàn)的,除去回復(fù)數(shù)顯示,其實你會看到,這個就是對發(fā)件人和標(biāo)題進(jìn)行篩選,選取發(fā)送時間***的那一條消息。

雖然這段 Linq 代碼看起來很“簡單”,但是如果你跟蹤一下生成的 SQL 代碼,會發(fā)現(xiàn)它是非常的臃腫,沒辦法,為了實現(xiàn)功能,然后就不得不去優(yōu)化數(shù)據(jù)庫,主要是對索引的優(yōu)化,這個當(dāng)時優(yōu)化了好久,也沒有找到合適的優(yōu)化方案,***不得不重新思考這樣做是不是不合理?這完全是技術(shù)驅(qū)動啊,后來,我發(fā)現(xiàn),在領(lǐng)域驅(qū)動設(shè)計的道路上,我已經(jīng)完全“跑偏”了。

消息詳情頁實現(xiàn)

業(yè)務(wù)需求的變化,其實主要是消息詳情頁的變化,從上面那張消息詳情頁示意圖就可以看出,剛才上面說了,收件箱列表顯示是對標(biāo)題和發(fā)件人的篩選,其實詳情頁就是通過標(biāo)題和發(fā)件人找出回復(fù)消息,然后通過發(fā)送時間降序排列。具體操作是,在收件箱中點擊一條消息,然后通過這條消息和發(fā)件人去倉儲中找這條消息的回復(fù)消息,示例代碼:

  1. public async Task<IEnumerable<Message>> GetMessages(Message message, Contact reader)  
  2. {  
  3.     if (message.Recipient.ID == reader.ID)  
  4.     {  
  5.         return await GetAll(Specification<Message>.Eval(m => m.Title == message.Title  
  6.             && ((m.Sender.ID == message.Sender.ID && m.Recipient.ID == message.Recipient.ID && (m.DisplayType == MessageDisplayType.OutboxAndInbox || m.DisplayType == MessageDisplayType.Inbox))  
  7.             || (m.Recipient.ID == message.Sender.ID && m.Sender.ID == message.Recipient.ID && (m.DisplayType == MessageDisplayType.OutboxAndInbox || m.DisplayType == MessageDisplayType.Outbox)))),  
  8.             sp => sp.ID, SortOrder.Ascending).ToListAsync();  
  9.     }  
  10.     else 
  11.     {  
  12.         return await GetAll(Specification<Message>.Eval(m => m.Title == message.Title  
  13.                 && ((m.Sender.ID == message.Sender.ID && m.Recipient.ID == message.Recipient.ID && (m.DisplayType == MessageDisplayType.OutboxAndInbox || m.DisplayType == MessageDisplayType.Outbox))  
  14.                 || (m.Recipient.ID == message.Sender.ID && m.Sender.ID == message.Recipient.ID && (m.DisplayType == MessageDisplayType.OutboxAndInbox || m.DisplayType == MessageDisplayType.Inbox)))),  
  15.                 sp => sp.ID, SortOrder.Ascending).ToListAsync();  
  16.     }  
  17. }  

不知道你是否能看懂,反正我現(xiàn)在看這段代碼是需要思考一下的,呵呵。消息詳情頁基本上就是這樣實現(xiàn)的,還有一些是在應(yīng)用層獲取“點擊消息”,UI 中消息顯示判斷等一些操作。

消息發(fā)送、回復(fù)、銷毀等實現(xiàn)

其實除了上面列表和詳情頁的變化,消息發(fā)送、回復(fù)和銷毀實現(xiàn)也需要做出調(diào)整,因為消息領(lǐng)域模型沒有任何變動,發(fā)送消息還是按照之前的發(fā)送邏輯,所以發(fā)送消息是沒有變化的,回復(fù)消息也沒有大的變化,只不過回復(fù)的時候需要獲取一下消息標(biāo)題,因為除了***條發(fā)送消息需要填寫標(biāo)題,之后的消息回復(fù)是不需要填寫標(biāo)題的,需要添加的只不過是消息內(nèi)容。消息銷毀的改動相對來說大一點,因為之前都是獨立的消息發(fā)送,所以可以對每個獨立的消息進(jìn)行銷毀操作,但是從上面消息詳情頁示意圖中可以看到,獨立的消息是不能銷毀的,只能銷毀這個完整的消息,也就是詳情頁最下面的刪除按鈕,示例代碼:

  1. public async Task<OperationResponse> DeleteMessage(int messageId, string readerLoginName)  
  2. {  
  3.     IContactRepository contactRepository = new ContactRepository();  
  4.     IMessageRepository messageRepository = new MessageRepository();  
  5.  
  6.     Message message = await messageRepository.GetByKey(messageId);  
  7.     if (message == null)  
  8.     {  
  9.         return OperationResponse.Error("抱歉!獲取失?。″e誤:消息不存在");  
  10.     }  
  11.     Contact reader = await contactRepository.GetContactByLoginName(readerLoginName);  
  12.     if (reader == null)  
  13.     {  
  14.         return OperationResponse.Error("抱歉!刪除失敗!錯誤:操作人不存在");  
  15.     }  
  16.     if (!message.CanRead(reader))  
  17.     {  
  18.         throw new Exception("抱歉!獲取失?。″e誤:沒有權(quán)限刪除");  
  19.     }  
  20.     message.DisposeMessage(reader);  
  21.     var messages = await messageRepository.GetMessages(message, reader);  
  22.     foreach (Message item in messages)  
  23.     {  
  24.         item.DisposeMessage(reader);  
  25.         messageRepository.Update(item);  
  26.     }  
  27.     await messageRepository.Context.Commit();  
  28.     return OperationResponse.Success("刪除成功");  
  29. }  

這個是應(yīng)用層中消息銷毀操作,可以看到應(yīng)用層的這個操作代碼很凌亂,這就是為了實現(xiàn)而實現(xiàn)的代價,除了消息銷毀,還有一個操作就是消息狀態(tài)設(shè)置,也就是消息“未讀”和“已讀”設(shè)置,這個代碼實現(xiàn)在應(yīng)用層 ReadMessage 操作中,代碼更加凌亂,我就不貼出來了,和消息銷毀操作比較類似,消息狀態(tài)設(shè)置只不過設(shè)置一些狀態(tài)而已。

#p#

回到原點的一些思考

為什么我會詳細(xì)描述我當(dāng)時實現(xiàn)的思路?其實就是想讓你和我產(chǎn)生一些共鳴,上面的一些實現(xiàn)操作,完全是為了實現(xiàn)而實現(xiàn),不同的應(yīng)用場景下的業(yè)務(wù)需求變化是不同的,但思考的方式一般都是想通的,也就是說如果你能正確應(yīng)對這個業(yè)務(wù)需求變化,那換一個應(yīng)用場景,你照樣可以應(yīng)對,如果你不能正確應(yīng)對,那領(lǐng)域驅(qū)動設(shè)計就是“空頭白話”,為什么?因為領(lǐng)域驅(qū)動設(shè)計就是更好的應(yīng)對業(yè)務(wù)需求變化的。

其實上面的需求變化,我們已經(jīng)變相的實現(xiàn)了,只不過沒有發(fā)布出來,就像一個多月之前的發(fā)布公告中所說,“Does your code look like this?”,如果按照這種方式實現(xiàn)了,那以后的短消息代碼,就是那一坨面條,慘不忍睹。

回到原點的一些思考,其實就是回到領(lǐng)域模型去看待這次的業(yè)務(wù)需求變化,關(guān)于這部分內(nèi)容,我還沒有準(zhǔn)確的做法,這邊我說一下自己的理解:

業(yè)務(wù)需求變化,領(lǐng)域模型變化了嗎?

首先,在之前的實現(xiàn)中,消息列表顯示這部分內(nèi)容,應(yīng)該是應(yīng)用層中體現(xiàn)的,所以在領(lǐng)域模型中可以暫時不考慮,這個在倉儲中應(yīng)該著重思考下。那領(lǐng)域模型變化了什么?先說發(fā)送消息,這個變化了嗎?我覺得沒有,還是點對點的發(fā)送一個消息,這個之前是用 SendSiteMessageService 領(lǐng)域服務(wù)實現(xiàn)的,邏輯也沒有太大的變化,那回復(fù)消息呢?其實我覺得這是***的一個變化,如果你看之前的回復(fù)代碼,我是沒有在領(lǐng)域模型中實現(xiàn)回復(fù)消息操作的,為什么?因為我當(dāng)時認(rèn)為,回復(fù)消息其實也是發(fā)送消息,所以在應(yīng)用層中回復(fù)消息操作,其實就是調(diào)用的 SendSiteMessageService 領(lǐng)域服務(wù),這個現(xiàn)在看來,是不應(yīng)該這樣實現(xiàn)的。

我們先梳理一下回復(fù)消息這個操作的處理流程,這個其實上面有過分析,除了***條消息是發(fā)送以外,之后的消息都是回復(fù)操作,這就要有一個標(biāo)識,用來說明這條消息是回復(fù)的那一條發(fā)送消息,那這個怎么來設(shè)計呢?回復(fù)消息設(shè)計成實體好?還是值對象好?我個人覺得,應(yīng)該設(shè)計成實體,原因大家想想就知道了,雖然它依附于發(fā)送消息存在,但是它也是唯一的,比如一個人給另外兩個人回復(fù)同樣內(nèi)容的消息,那這兩個回復(fù)消息應(yīng)該都是獨立存在的,那這個依附關(guān)系怎么處理呢?我們可以在消息實體中添加一個標(biāo)識,用來表示它回復(fù)的是那條消息。

上面這個確定之后,那我們?nèi)绾螌崿F(xiàn)回復(fù)消息操作呢?我們可以用一個領(lǐng)域服務(wù)實現(xiàn),比如 ReplySiteMessageService,用來處理回復(fù)消息的一些操作,這個和 SendSiteMessageService 領(lǐng)域服務(wù)可能會有些不同,比如一個人 1 天只能發(fā)送 200 條消息,但是這個邏輯我們就不能放在回復(fù)消息領(lǐng)域服務(wù)中,回復(fù)只是針對一個人的回復(fù),所以這個可以不做限制,發(fā)送是針對任何人的,為了避免廣告推廣,這個我們必須要做一個發(fā)送限制,當(dāng)然具體實現(xiàn),就要看需求的要求了。

除了回復(fù)消息這個變化,說多一點,消息狀態(tài)(未讀和已讀)和消息銷毀,這個可能也會有細(xì)微的變化,比如消息狀態(tài),在消息列表中打開一個消息,其實就是把這條消息的回復(fù)內(nèi)容都設(shè)置成已讀了,我們之前的設(shè)計是針對獨立的消息狀態(tài),也就是說每個消息都有一個消息狀態(tài),按照這種方式,其實我們可以把這個狀態(tài)放在發(fā)送消息實體中,如果有人回復(fù)了,那這個消息狀態(tài)就是設(shè)置為未讀,回復(fù)消息沒有任何狀態(tài),如果這樣設(shè)計的話,有點像值對象的感覺,可以從消息實體中獨立出來一個回復(fù)消息值對象,當(dāng)然這只是我的一種思路。消息銷毀和這個消息狀態(tài)比較類似,這邊就不多說了,除了這兩個變化,其實還有一些細(xì)節(jié)需要考慮,這個只能在實現(xiàn)中進(jìn)行暴露出來了。

對象讀取的額外思考

這個其實是我看了倉儲那慘不忍睹的實現(xiàn)代碼,所引起的一些思考,你可以讀一下,這樣的一篇博文:你正在以錯誤的方式使用ORM。

倉儲在領(lǐng)域驅(qū)動設(shè)計的作用,可以看作是實體的存儲倉庫,我們獲取實體對象就要經(jīng)過倉儲,倉儲的實現(xiàn)可以是任何方式,但傳輸對象必須是聚合根對象,這個在理論中沒有什么問題,但是在實際項目中,我們從倉儲中獲取對象,一般有兩種用途:

  1. 用于領(lǐng)域模型中的一些驗證操作。
     
  2. 用于應(yīng)用層中的 DTO 對象轉(zhuǎn)化。

***種沒有什么問題,但是第二種,這個就不可避免的造成性能問題,也就是上面文中 Jimmy(AutoMapper 作者)所說的 Select N 問題,這個我之前也遇到過,***的解決方式,我是按照他在 AutoMapper 映射的一些擴(kuò)展,也就是上面代碼中的 Project().To(),但這樣就不可避免的違背了領(lǐng)域驅(qū)動設(shè)計中倉儲的一些思想。

關(guān)于這個內(nèi)容,我不想說太多,重點是上面領(lǐng)域模型的思考,倉儲的問題,我是一定要做一些改變的,因為它現(xiàn)在的實現(xiàn),讓強(qiáng)迫癥的我感覺到非常不爽,不管是 CQRS、ES、還是六邊形架構(gòu),總歸先嘗試實現(xiàn)再說,有問題不可怕,可怕的是不懂得改正。

寫在***

[[120579]]

在領(lǐng)域驅(qū)動設(shè)計的道路上,有很多你意想不到的情況發(fā)生,稍微不注意,你就會偏離的大方向,很遺憾,我沒有針對這次的業(yè)務(wù)需求變化,做出一些具體的實現(xiàn),但我覺得意識到問題很重要,這篇博文分享希望能與你產(chǎn)生一些共鳴。

本文出自:http://www.cnblogs.com/xishuai/p/3972802.html

責(zé)任編輯:林師授 來源: 田園里的蟋蟀博客
相關(guān)推薦

2021-09-08 09:22:23

領(lǐng)域驅(qū)動設(shè)計

2013-07-05 14:47:51

IoC需求

2020-09-02 08:12:05

CodeDDD代碼

2021-10-09 11:54:46

DDD微服務(wù)業(yè)務(wù)

2017-07-14 10:55:05

2022-07-17 07:37:29

微服務(wù)DDD工程化落地

2024-12-31 11:05:07

2023-01-09 09:00:00

樹服務(wù)架構(gòu)驅(qū)動決策

2024-11-27 15:33:17

軟件架構(gòu)DDD

2024-11-08 08:37:25

2023-02-15 13:50:58

DDD戰(zhàn)略設(shè)計

2019-01-02 05:55:30

領(lǐng)域驅(qū)動軟件復(fù)雜度

2014-09-11 15:05:40

驅(qū)動設(shè)計驅(qū)動開發(fā)

2023-02-08 07:51:52

DDD領(lǐng)域驅(qū)動

2009-08-07 10:12:13

博科資訊物流管理

2013-03-01 11:19:02

項目需求項目設(shè)計

2024-07-17 08:12:06

2023-09-04 15:31:54

軟件開發(fā)敏捷開發(fā)工具

2025-01-26 10:10:30

點贊
收藏

51CTO技術(shù)棧公眾號