文章出處

寫在前面

上一篇:DDD 領域驅動設計-看我如何應對業務需求變化,愚蠢的應對?

“愚蠢的應對”,這個標題是我后來補充上的,博文中除了描述需求變化、愚蠢應對和一些思考,確實沒有實質性的應對,文不對題,實在慚愧。

這次應對,我們從領域模型開始。

領域模型思考

業務需求變化,關于領域模型的調整,上一篇我只給出了一些思考,但這段內容,我覺得是那篇博文最重要的地方,不知道你仔細看了沒,我一直在強調“回復的概念”,以及之前領域模型沒有“回復”所造成的一些問題,在上一個版本的領域模型中,對消息的操作,除去本身狀態的改變,就只有發送消息操作,也就是 ISendMessageService 領域服務接口,在短消息應用場景中,是有回復和轉發操作的,但之前的設計把回復和轉發的消息也看作是“消息”,一個完整的“消息”,它和第一次發送的消息是一樣的,說是完整,其實也是獨立的,我們可以對它進行獨立操作,但這也就造成了回復概念的模糊,比如應用層中的 SendMessage/ReplyMessage/ForwardMessage 操作,大部分都是重復代碼,最后調用同一個 ISendMessageService 領域服務實現,從應用層的這部分代碼,就可以看出,現在領域模型所暴露出來的一些問題。當然不止這些,后來在“愚蠢的應對”中,最后所出現的性能問題,追根究底也是這個原因,因為現在消息的“獨立性”,致使消息之間沒有任何關聯,所以我們在消息倉儲進行取出消息對象操作,這個就像是進行所有消息對象的過濾,我所規定的過濾條件是標題和收發件人,然后再進行發送時間降序排序,取出最新發送的那一條消息,當然條件和轉換還有很多,最后使用 ORM 生成了一大串的 SQL 代碼,然后就優化,再優化,最后就陷入泥潭了。。。

所有的一些問題出現,追根究底都是領域模型設計不合理所導致的,從這一方面就可以體現出領域模型的重要性

回復是現在領域模型調整的核心,在上一篇博文評論中,針對現有領域模型的調整,我和 ntefocus 探討了兩種方式,還有后來徐少俠、翱翔提出的第三種方式,這邊我再大致總結一下,詳細內容可以看上一篇的評論。

領域模型調整的三種方式

  1. 消息和回復設計成兩個實體(消息為聚合根),發送消息就是針對第一個實體而言,之后回復就是針對第二個實體,可以很好進行區分,在倉儲獲取的時候也更方便。以前,每個消息都是獨立的消息,回復也是一個消息,也就是說,回復也有對應的收件人;現在不同了,只有第一個才是消息,后面的都是該消息的回復,回復不需要指定收件人,只需要指定被回復的消 ID即可;所以,如果是要這樣的需求,那這個消息系統的領域模型和論壇就很像了,但不完全是一個論壇系統,還是有一定的差別,比如論壇里的帖子是沒有收件人的概念的,而這里的消息有收件人的概念。
    然后,針對類似論壇的領域模型,消息和回復是兩個實體了;
    消息:id, subject, body, senderId, receiverId
    回復:id, messageId, body, replierId
  2. 基本上不動現在的消息領域模型,發送和回復在實體中用標識進行區分,那發送消息和回復消息進行關聯呢?可以加一個自關聯消息實體對象,表示它回復的是哪條消息,這種方式雖然很。。。但是以后的擴展會比較好應對,比如后面有轉發功能,就是消息增加一種轉發標識就行了,而且這種不會感覺到很怪,回復和發送都是一種消息,同屬于消息實體。
    只要擴從一下消息實體,增加一個parentId,表示當前消息的父消息是什么,這個parentId可以為空;頂層消息的parentId為空,回復消息的parentId是其被回復的消息的id。然后replierId就填到senderId里,收件人ID你在回復的時候肯定也能得到。然后對于回復,就不要填寫subject了;
    通過這樣的實體變動,那消息實體就變為:id, subject, body, parentId, senderId, receiverId
  3. session模式(session看作是一個概念,描述可能不是很準確),發送消息時,會有一個消息,同時生成一個會話,該消息自動關聯到該會話,也就是消息上會有一個sessionId;然后消息的標題不屬于消息本身,而是屬于會話;當回復時,你也把回復理解為一個消息,然后這個消息的sessionId也是當前的session的id;這種方式和第一種方式的最大差別,就是是否把消息標題獨立到一個獨立的會話對象中,大致實體:
    Message: ID, Title, Body, Attach, Creator, DateTime...
    Session: ID, Owner, Title, State...

針對上面每一種所出現的細節問題,我們探討了很久,可能我描述的不是很準確,這邊只需要明白每一種所表達的意思,針對其實現,沒誰對誰錯,只有適不適合,每一種改變,都會對應一個新的領域模型產生。

最后,我所采用的是第二種方式,原因在評論中也有詳細的說明,這邊我再簡單敘述下:

  1. 堅守回復也是消息,沒有偏離最初的設計。
  2. 可以和現有領域模型很好兼容。
  3. 可以很好應對以后消息模型的調整。
  4. 相對而言改動較小。
  5. ...

當然這種方式最大的缺點就是回復屬性的冗余,有利有弊,沒有什么完美的,我們來看一下領域模型的具體調整。

領域模型調整

我直接貼一下領域模型的調整代碼,首先是 Message 實體中,增加 ParentMessage 屬性,用來表示回復的概念:

public Message ParentMessage { get; set; }

這邊需要注意的是,ParentMessage 屬性類型為 Message,而不是之前描述的 parentId,Id 標識的概念具體會體現在 ORM 映射中,領域模型中應該是 Message 對象。
回復操作,我設計為 ReplySiteMessageService 領域服務,示例代碼為:

using CNBlogs.Msg.Domain.Entity;
using CNBlogs.Msg.Domain.ValueObject;
using CNBlogs.Msg.Infrastructure;

namespace CNBlogs.Msg.Domain.DomainService
{
    /// <summary>
    /// ReplySiteMessageService 領域服務-回復消息
    /// </summary>
    public class ReplySiteMessageService
    {
        public bool ReplySiteMessage(Message parentMessage, Message replyMessage)
        {
            if (parentMessage.Sender == replyMessage.Sender && parentMessage.Recipient == replyMessage.Recipient)
            {
                if (parentMessage.DisplayType == MessageDisplayType.Outbox)
                {
                    parentMessage.DisplayType = MessageDisplayType.OutboxAndInbox;
                }
                else
                {
                    throw new CustomMessageException("消息已被您刪除,無法回復");
                }
            }
            else if (parentMessage.Sender == replyMessage.Recipient && parentMessage.Recipient == replyMessage.Sender)
            {
                if (parentMessage.DisplayType == MessageDisplayType.Inbox)
                {
                    parentMessage.DisplayType = MessageDisplayType.OutboxAndInbox;
                }
                else
                {
                    throw new CustomMessageException("消息已被您刪除,無法回復");
                }
            }
            else
            {
                throw new CustomMessageException("您不是收發件人,沒有權限回復");
            }
            replyMessage.ParentMessage = parentMessage;
            return true;
        }
    }
}

上面 ReplySiteMessageService 領域服務中,我只寫了一個權限判斷的代碼,可能以后會有所拓展,如果你熟悉之前 SendSiteMessageService 領域服務的代碼,就會發現它們是有所不同的,這也就是發送和回復要進行隔離開,但它們本質都是消息,也就是同一個實體概念。

為了方便大家查看短消息領域模型的代碼,我把它上傳到 GitHub 了,感興趣的話,可以參考下。

寫在最后

領域模型是領域驅動設計的核心!

在領域驅動設計的過程中,上面那句話常常掛在嘴邊,但當實際操作的時候,卻往往會把它忽略,這次領域模型調整的代碼雖然很少,但是卻思考了很久,核心內容確定下來,后面的一些操作才能夠圍繞它展開。


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()