[{"data":1,"prerenderedAt":2036},["ShallowReactive",2],{"article-\u002Farticles\u002Fddd-overview-strategic-tactical":3},{"id":4,"title":5,"body":6,"date":2021,"description":2022,"excerpt":2023,"extension":2024,"meta":2025,"navigation":726,"path":2026,"seo":2027,"stem":2028,"tags":2029,"__hash__":2035},"articles\u002Farticles\u002Fddd-overview-strategic-tactical.md","DDD 概論：戰略設計、戰術設計與 Event Storming，先搞懂它到底在解決什麼問題",{"type":7,"value":8,"toc":2003},"minimark",[9,27,30,35,42,69,75,79,86,89,145,163,165,170,177,180,241,268,271,275,281,343,350,354,360,363,383,390,397,401,404,486,493,495,502,611,615,622,659,665,1526,1529,1561,1564,1812,1818,1831,1835,1842,1845,1848,1898,1905,1909,1912,1934,1938,1957,1964,1967,1993,1996,1999],[10,11,12,13,18,19,23,24],"p",{},"在 ",[14,15,17],"a",{"href":16},"\u002Farticles\u002Fhexagonal-architecture-dotnet","六角形架構"," 那篇，我把業務規則都放進「核心」，外部技術變成可以抽換的 Adapter。但做完之後留下兩個問題：",[20,21,22],"strong",{},"核心裡面的模型要怎麼設計？"," 以及系統越長越大時，",[20,25,26],{},"一個核心要不要切成好幾個？要從哪裡切？",[10,28,29],{},"這兩個問題正好是 DDD（Domain-Driven Design，領域驅動設計）在回答的事。DDD 來自 Eric Evans 在 2003 年出版的同名書籍，二十年下來衍生出很多做法，這篇先把整體架構整理清楚，之後再針對子域劃分、Event Storming 這些主題分開深入。",[31,32,34],"h2",{"id":33},"ddd-在解決什麼問題","DDD 在解決什麼問題",[10,36,37,38,41],{},"做房仲系統這幾年，最常遇到的困難不是技術，而是",[20,39,40],{},"業務本身很複雜，而程式碼沒有反映出這份複雜","：",[43,44,45,57,63],"ul",{},[46,47,48,51,52,56],"li",{},[20,49,50],{},"翻譯成本","：業務說「這個物件已經有人斡旋了」，程式裡卻是 ",[53,54,55],"code",{},"house.status = 3","。每次需求討論都要在業務語言和程式碼之間翻譯，翻錯一次就是一個 Bug",[46,58,59,62],{},[20,60,61],{},"一個模型用到底","：「物件」一張資料表，刊登要用、搜尋要用、成交要用、業績也要用。欄位越長越多，改一個欄位要擔心十個地方",[46,64,65,68],{},[20,66,67],{},"規則散落各處","：「斡旋過期就不能接受」這條規則，前端檢查一次、API 檢查一次、排程又檢查一次，三個地方的判斷還不太一樣",[10,70,71,72],{},"DDD 的基本主張是：",[20,73,74],{},"軟體的核心複雜度來自業務領域本身，所以設計要從理解業務開始，讓程式碼的結構和用詞直接對應業務。",[31,76,78],{"id":77},"全貌戰略戰術協作探索","全貌：戰略、戰術、協作探索",[10,80,81],{},[82,83],"img",{"alt":84,"src":85},"DDD 的全貌：左邊是戰略設計，在問題空間把業務分成核心、支撐、通用子域，在解法空間把系統切成刊登、搜尋、成交等限界上下文，同一個「物件」在不同上下文有不同模型，上下文之間的關係用上下文映射描述；右邊是戰術設計，放大成交上下文，裡面由 Application Service 呼叫 Offer 聚合，聚合根 Offer 是實體，價格、斡旋金、期限是值物件，用 ListingId 參照其他聚合，狀態改變時發出 OfferAccepted 領域事件，透過 Repository 以聚合為單位存取；底部是協作探索，用 Event Storming 和 Domain Storytelling 和領域專家一起找出事件、語言與邊界","\u002Fimages\u002Farticles\u002Fddd-overview-map.svg",[10,87,88],{},"DDD 可以分成三個部分：",[90,91,92,108],"table",{},[93,94,95],"thead",{},[96,97,98,102,105],"tr",{},[99,100,101],"th",{},"部分",[99,103,104],{},"回答的問題",[99,106,107],{},"主要概念",[109,110,111,123,134],"tbody",{},[96,112,113,117,120],{},[114,115,116],"td",{},"戰略設計",[114,118,119],{},"系統要切成哪幾塊？每塊多重要？塊和塊之間怎麼合作？",[114,121,122],{},"通用語言、子域、限界上下文、上下文映射",[96,124,125,128,131],{},[114,126,127],{},"戰術設計",[114,129,130],{},"一個上下文裡面，模型要怎麼寫？",[114,132,133],{},"聚合、實體、值物件、領域事件、Repository、領域服務",[96,135,136,139,142],{},[114,137,138],{},"協作探索",[114,140,141],{},"怎麼和領域專家一起把業務弄清楚？",[114,143,144],{},"Event Storming、Domain Storytelling",[10,146,147,148,151,152,151,155,158,159,162],{},"很多團隊導入 DDD 是從戰術設計開始，專案裡多了 ",[53,149,150],{},"Entities","、",[53,153,154],{},"ValueObjects",[53,156,157],{},"Repositories"," 資料夾，但系統還是一團。這種做法常被稱為「DDD-Lite」，只拿到最表面的好處。",[20,160,161],{},"戰略設計才是 DDD 影響最大的部分","：切錯了邊界，裡面的類別寫得再漂亮也沒用。",[31,164,116],{"id":116},[166,167,169],"h3",{"id":168},"通用語言ubiquitous-language","通用語言（Ubiquitous Language）",[10,171,172,173,176],{},"團隊（包含業務、PO、開發人員）在討論、文件、程式碼中使用",[20,174,175],{},"同一套用詞","。",[10,178,179],{},"房仲業務裡就有很多容易混淆的詞：",[90,181,182,195],{},[93,183,184],{},[96,185,186,189,192],{},[99,187,188],{},"業務用詞",[99,190,191],{},"意思",[99,193,194],{},"常見的混用",[109,196,197,208,219,230],{},[96,198,199,202,205],{},[114,200,201],{},"委託",[114,203,204],{},"屋主授權經紀人銷售，有一般委託、專任委託",[114,206,207],{},"和「刊登」混用，但委託是合約，刊登是上架到網站",[96,209,210,213,216],{},[114,211,212],{},"帶看",[114,214,215],{},"經紀人帶買方實際看屋",[114,217,218],{},"「看屋」、「約看」、「預約」各講各的",[96,220,221,224,227],{},[114,222,223],{},"斡旋",[114,225,226],{},"買方付斡旋金、提出價格，請經紀人向屋主議價",[114,228,229],{},"和「出價」混用，但出價不一定有斡旋金、沒有法律效力",[96,231,232,235,238],{},[114,233,234],{},"成交",[114,236,237],{},"屋主接受價格，雙方簽約",[114,239,240],{},"斡旋被接受就算成交？還是簽約才算？各部門定義不同",[10,242,243,244,247,248,251,252,255,256,259,260,263,264,267],{},"通用語言不是一份名詞表而已，它要",[20,245,246],{},"直接出現在程式碼裡","：類別叫 ",[53,249,250],{},"Offer","（斡旋）而不是 ",[53,253,254],{},"HouseRequest","，方法叫 ",[53,257,258],{},"offer.Accept()"," 而不是 ",[53,261,262],{},"UpdateStatus(3)","。當業務說「斡旋過期不能接受」，開發人員應該能在 ",[53,265,266],{},"Offer.Accept()"," 裡找到這條規則。",[10,269,270],{},"最後一列「成交」的定義不一致，就是通用語言要解決的典型問題：只要發現同一個詞有兩種意思，就要停下來釐清，而不是各自假設。",[166,272,274],{"id":273},"子域subdomain","子域（Subdomain）",[10,276,277,280],{},[20,278,279],{},"領域","是整個業務，例如「房仲業務」。領域很大，可以再分成幾個子域，並依重要性分類：",[90,282,283,299],{},[93,284,285],{},[96,286,287,290,293,296],{},[99,288,289],{},"類型",[99,291,292],{},"說明",[99,294,295],{},"房仲的例子",[99,297,298],{},"策略",[109,300,301,315,329],{},[96,302,303,306,309,312],{},[114,304,305],{},"核心子域",[114,307,308],{},"讓公司和競爭對手不同的部分",[114,310,311],{},"物件媒合、斡旋成交流程",[114,313,314],{},"自己做、放最好的人、持續投資",[96,316,317,320,323,326],{},[114,318,319],{},"支撐子域",[114,321,322],{},"業務需要，但不是競爭優勢",[114,324,325],{},"帶看排程、經紀人業績報表",[114,327,328],{},"自己做但簡單做，或外包",[96,330,331,334,337,340],{},[114,332,333],{},"通用子域",[114,335,336],{},"大家都一樣的問題",[114,338,339],{},"會員登入、簡訊通知、金流",[114,341,342],{},"買現成的、用開源或雲端服務",[10,344,345,346,349],{},"分類的目的是",[20,347,348],{},"決定資源要放在哪裡","：核心子域值得用完整的 DDD 戰術設計慢慢打磨，通用子域則應該盡量不要自己寫。子域要怎麼找、怎麼判斷屬於哪一類，之後會另外整理一篇。",[166,351,353],{"id":352},"限界上下文bounded-context","限界上下文（Bounded Context）",[10,355,356,357,176],{},"子域是「業務長什麼樣子」（問題空間），限界上下文則是「系統怎麼切」（解法空間）：",[20,358,359],{},"一個模型和它的通用語言有效的範圍",[10,361,362],{},"最好的例子就是「物件」這個詞：",[43,364,365,371,377],{},[46,366,367,370],{},[20,368,369],{},"刊登上下文","：物件是一筆刊登內容，有標題、照片、描述、刊登期限",[46,372,373,376],{},[20,374,375],{},"搜尋上下文","：物件是一筆搜尋結果，有座標、價格區間、標籤，重點是查得快",[46,378,379,382],{},[20,380,381],{},"成交上下文","：物件是一筆交易標的，有權狀、地號、所有權人、斡旋紀錄",[10,384,385,386,389],{},"硬把三者塞成一個 ",[53,387,388],{},"House"," 類別，就是前面說的「一個模型用到底」。切成三個上下文後，每個上下文裡的「物件」只需要自己關心的欄位和規則，彼此透過明確的介面或事件溝通。",[10,391,392,393,396],{},"限界上下文通常也是",[20,394,395],{},"團隊與部署的邊界","：一個上下文由一個團隊負責，可以是一個獨立的服務，也可以是單體應用程式裡的一個模組。DDD 不要求一定要做成微服務。",[166,398,400],{"id":399},"上下文映射context-map","上下文映射（Context Map）",[10,402,403],{},"上下文之間一定會有往來，Context Map 描述它們之間的關係。常見的幾種：",[90,405,406,418],{},[93,407,408],{},[96,409,410,413,415],{},[99,411,412],{},"模式",[99,414,292],{},[99,416,417],{},"例子",[109,419,420,431,442,453,464,475],{},[96,421,422,425,428],{},[114,423,424],{},"客戶／供應商（Customer–Supplier）",[114,426,427],{},"下游提需求，上游排進開發計畫",[114,429,430],{},"搜尋上下文需要刊登上下文提供物件異動事件",[96,432,433,436,439],{},[114,434,435],{},"遵奉者（Conformist）",[114,437,438],{},"下游直接採用上游的模型，沒有議價空間",[114,440,441],{},"直接使用政府實價登錄資料的欄位格式",[96,443,444,447,450],{},[114,445,446],{},"防腐層（Anticorruption Layer）",[114,448,449],{},"下游加一層轉換，避免上游的模型汙染自己",[114,451,452],{},"串接舊的 ERP 系統時，把它的欄位轉成成交上下文自己的模型",[96,454,455,458,461],{},[114,456,457],{},"開放主機服務／公開語言（Open Host Service \u002F Published Language）",[114,459,460],{},"上游提供一套穩定、有文件的公開介面給多個下游",[114,462,463],{},"刊登上下文發布統一格式的「物件已上架」事件",[96,465,466,469,472],{},[114,467,468],{},"共享核心（Shared Kernel）",[114,470,471],{},"兩個上下文共用一小部分模型，修改要雙方同意",[114,473,474],{},"共用地址、行政區的值物件",[96,476,477,480,483],{},[114,478,479],{},"各行其道（Separate Ways）",[114,481,482],{},"整合成本太高，乾脆不整合",[114,484,485],{},"內部公告系統和成交系統完全獨立",[10,487,488,489,492],{},"畫 Context Map 最大的價值不是畫出漂亮的圖，而是",[20,490,491],{},"把團隊之間的依賴和權力關係說清楚","：如果下游是「遵奉者」，上游一改格式下游就壞，那就應該考慮加一層防腐層。",[31,494,127],{"id":127},[10,496,497,498,501],{},"戰術設計是在一個限界上下文",[20,499,500],{},"裡面","，用來表達業務模型的一組建構元素：",[90,503,504,515],{},[93,505,506],{},[96,507,508,511,513],{},[99,509,510],{},"元素",[99,512,292],{},[99,514,417],{},[109,516,517,530,547,560,574,587,598],{},[96,518,519,522,525],{},[114,520,521],{},"實體（Entity）",[114,523,524],{},"有唯一識別，狀態會隨時間改變",[114,526,527,529],{},[53,528,250],{},"：同一筆斡旋，價格或狀態變了還是同一筆",[96,531,532,535,538],{},[114,533,534],{},"值物件（Value Object）",[114,536,537],{},"沒有識別，只看值；不可變",[114,539,540,151,543,546],{},[53,541,542],{},"Money",[53,544,545],{},"Address","：兩個 1,200 萬是一樣的",[96,548,549,552,555],{},[114,550,551],{},"聚合（Aggregate）",[114,553,554],{},"一組必須保持一致的物件，對外只透過聚合根操作",[114,556,557,559],{},[53,558,250],{}," 聚合包含價格、斡旋金、期限",[96,561,562,565,568],{},[114,563,564],{},"領域事件（Domain Event）",[114,566,567],{},"業務上發生過、其他人在意的事，用過去式命名",[114,569,570,573],{},[53,571,572],{},"OfferAccepted","（斡旋已接受）",[96,575,576,579,582],{},[114,577,578],{},"Repository",[114,580,581],{},"以聚合為單位存取，隱藏資料庫細節",[114,583,584],{},[53,585,586],{},"IOfferRepository",[96,588,589,592,595],{},[114,590,591],{},"領域服務（Domain Service）",[114,593,594],{},"不屬於任何一個實體的業務規則",[114,596,597],{},"佣金計算，需要成交價、委託類型、經紀人分配比例",[96,599,600,603,606],{},[114,601,602],{},"應用服務（Application Service）",[114,604,605],{},"協調流程：讀取聚合、呼叫方法、儲存，不放業務規則",[114,607,608],{},[53,609,610],{},"AcceptOfferHandler",[166,612,614],{"id":613},"聚合最重要也最容易做錯","聚合：最重要也最容易做錯",[10,616,617,618,621],{},"聚合是一個",[20,619,620],{},"一致性邊界","：邊界內的規則（不變條件，Invariant）在每次交易結束時都必須成立。Vaughn Vernon 在《Implementing Domain-Driven Design》整理了四個設計原則：",[623,624,625,631,637,653],"ol",{},[46,626,627,630],{},[20,628,629],{},"在邊界內保護真正的不變條件","：只有「必須同時成立」的規則才放在同一個聚合",[46,632,633,636],{},[20,634,635],{},"設計小的聚合","：大聚合讀取慢、容易衝突，很多規則其實不需要即時一致",[46,638,639,41,642,644,645,648,649,652],{},[20,640,641],{},"用 Id 參照其他聚合",[53,643,250],{}," 存的是 ",[53,646,647],{},"ListingId","，不是整個 ",[53,650,651],{},"Listing"," 物件",[46,654,655,658],{},[20,656,657],{},"邊界外用最終一致","：一個交易只修改一個聚合，其他聚合透過領域事件在之後更新",[10,660,661,662,664],{},"用斡旋來看：「斡旋過期不能接受」、「已經接受的斡旋不能再撤回」是 ",[53,663,250],{}," 自己的規則，放在聚合裡；「斡旋被接受後，物件要標記為已斡旋、其他斡旋要通知失效」則牽涉其他聚合，透過事件處理。",[666,667,672],"pre",{"className":668,"code":669,"language":670,"meta":671,"style":671},"language-csharp shiki shiki-themes github-dark","public sealed record Money\n{\n    public decimal Amount { get; }\n\n    public Money(decimal amount)\n    {\n        if (amount \u003C= 0) throw new DomainException(\"金額必須大於 0\");\n        Amount = amount;\n    }\n}\n\npublic sealed class Offer : AggregateRoot\n{\n    public OfferId Id { get; }\n    public ListingId ListingId { get; }      \u002F\u002F 只用 Id 參照其他聚合\n    public BuyerId BuyerId { get; }\n    public Money Price { get; }\n    public Money EarnestMoney { get; }       \u002F\u002F 斡旋金\n    public DateTimeOffset ExpiresAt { get; }\n    public OfferStatus Status { get; private set; }\n\n    private Offer(OfferId id, ListingId listingId, BuyerId buyerId,\n        Money price, Money earnestMoney, DateTimeOffset expiresAt)\n    {\n        Id = id;\n        ListingId = listingId;\n        BuyerId = buyerId;\n        Price = price;\n        EarnestMoney = earnestMoney;\n        ExpiresAt = expiresAt;\n        Status = OfferStatus.Submitted;\n    }\n\n    public static Offer Submit(ListingId listingId, BuyerId buyerId,\n        Money price, Money earnestMoney, DateTimeOffset expiresAt, DateTimeOffset now)\n    {\n        if (expiresAt \u003C= now) throw new DomainException(\"斡旋期限必須晚於現在\");\n\n        var offer = new Offer(OfferId.New(), listingId, buyerId, price, earnestMoney, expiresAt);\n        offer.AddEvent(new OfferSubmitted(offer.Id, listingId, price.Amount));\n        return offer;\n    }\n\n    public void Accept(DateTimeOffset now)\n    {\n        if (Status != OfferStatus.Submitted)\n            throw new DomainException($\"斡旋狀態為 {Status}，不能接受\");\n        if (now > ExpiresAt)\n            throw new DomainException(\"斡旋已過期，不能接受\");\n\n        Status = OfferStatus.Accepted;\n        AddEvent(new OfferAccepted(Id, ListingId, BuyerId, Price.Amount));\n    }\n\n    public void Withdraw(DateTimeOffset now)\n    {\n        if (Status != OfferStatus.Submitted)\n            throw new DomainException($\"斡旋狀態為 {Status}，不能撤回\");\n\n        Status = OfferStatus.Withdrawn;\n        AddEvent(new OfferWithdrawn(Id, ListingId));\n    }\n}\n","csharp","",[53,673,674,693,700,721,728,748,754,791,803,809,815,820,839,844,861,882,898,914,934,951,977,982,1017,1043,1048,1059,1070,1081,1092,1103,1114,1125,1130,1135,1162,1190,1195,1221,1226,1251,1271,1280,1285,1290,1309,1314,1328,1357,1371,1387,1392,1402,1418,1423,1428,1446,1451,1462,1486,1491,1501,1516,1521],{"__ignoreMap":671},[675,676,679,683,686,689],"span",{"class":677,"line":678},"line",1,[675,680,682],{"class":681},"snl16","public",[675,684,685],{"class":681}," sealed",[675,687,688],{"class":681}," record",[675,690,692],{"class":691},"svObZ"," Money\n",[675,694,696],{"class":677,"line":695},2,[675,697,699],{"class":698},"s95oV","{\n",[675,701,703,706,709,712,715,718],{"class":677,"line":702},3,[675,704,705],{"class":681},"    public",[675,707,708],{"class":681}," decimal",[675,710,711],{"class":691}," Amount",[675,713,714],{"class":698}," { ",[675,716,717],{"class":681},"get",[675,719,720],{"class":698},"; }\n",[675,722,724],{"class":677,"line":723},4,[675,725,727],{"emptyLinePlaceholder":726},true,"\n",[675,729,731,733,736,739,742,745],{"class":677,"line":730},5,[675,732,705],{"class":681},[675,734,735],{"class":691}," Money",[675,737,738],{"class":698},"(",[675,740,741],{"class":681},"decimal",[675,743,744],{"class":691}," amount",[675,746,747],{"class":698},")\n",[675,749,751],{"class":677,"line":750},6,[675,752,753],{"class":698},"    {\n",[675,755,757,760,763,766,770,773,776,779,782,784,788],{"class":677,"line":756},7,[675,758,759],{"class":681},"        if",[675,761,762],{"class":698}," (amount ",[675,764,765],{"class":681},"\u003C=",[675,767,769],{"class":768},"sDLfK"," 0",[675,771,772],{"class":698},") ",[675,774,775],{"class":681},"throw",[675,777,778],{"class":681}," new",[675,780,781],{"class":691}," DomainException",[675,783,738],{"class":698},[675,785,787],{"class":786},"sU2Wk","\"金額必須大於 0\"",[675,789,790],{"class":698},");\n",[675,792,794,797,800],{"class":677,"line":793},8,[675,795,796],{"class":698},"        Amount ",[675,798,799],{"class":681},"=",[675,801,802],{"class":698}," amount;\n",[675,804,806],{"class":677,"line":805},9,[675,807,808],{"class":698},"    }\n",[675,810,812],{"class":677,"line":811},10,[675,813,814],{"class":698},"}\n",[675,816,818],{"class":677,"line":817},11,[675,819,727],{"emptyLinePlaceholder":726},[675,821,823,825,827,830,833,836],{"class":677,"line":822},12,[675,824,682],{"class":681},[675,826,685],{"class":681},[675,828,829],{"class":681}," class",[675,831,832],{"class":691}," Offer",[675,834,835],{"class":698}," : ",[675,837,838],{"class":691},"AggregateRoot\n",[675,840,842],{"class":677,"line":841},13,[675,843,699],{"class":698},[675,845,847,849,852,855,857,859],{"class":677,"line":846},14,[675,848,705],{"class":681},[675,850,851],{"class":691}," OfferId",[675,853,854],{"class":691}," Id",[675,856,714],{"class":698},[675,858,717],{"class":681},[675,860,720],{"class":698},[675,862,864,866,869,871,873,875,878],{"class":677,"line":863},15,[675,865,705],{"class":681},[675,867,868],{"class":691}," ListingId",[675,870,868],{"class":691},[675,872,714],{"class":698},[675,874,717],{"class":681},[675,876,877],{"class":698},"; }      ",[675,879,881],{"class":880},"sAwPA","\u002F\u002F 只用 Id 參照其他聚合\n",[675,883,885,887,890,892,894,896],{"class":677,"line":884},16,[675,886,705],{"class":681},[675,888,889],{"class":691}," BuyerId",[675,891,889],{"class":691},[675,893,714],{"class":698},[675,895,717],{"class":681},[675,897,720],{"class":698},[675,899,901,903,905,908,910,912],{"class":677,"line":900},17,[675,902,705],{"class":681},[675,904,735],{"class":691},[675,906,907],{"class":691}," Price",[675,909,714],{"class":698},[675,911,717],{"class":681},[675,913,720],{"class":698},[675,915,917,919,921,924,926,928,931],{"class":677,"line":916},18,[675,918,705],{"class":681},[675,920,735],{"class":691},[675,922,923],{"class":691}," EarnestMoney",[675,925,714],{"class":698},[675,927,717],{"class":681},[675,929,930],{"class":698},"; }       ",[675,932,933],{"class":880},"\u002F\u002F 斡旋金\n",[675,935,937,939,942,945,947,949],{"class":677,"line":936},19,[675,938,705],{"class":681},[675,940,941],{"class":691}," DateTimeOffset",[675,943,944],{"class":691}," ExpiresAt",[675,946,714],{"class":698},[675,948,717],{"class":681},[675,950,720],{"class":698},[675,952,954,956,959,962,964,966,969,972,975],{"class":677,"line":953},20,[675,955,705],{"class":681},[675,957,958],{"class":691}," OfferStatus",[675,960,961],{"class":691}," Status",[675,963,714],{"class":698},[675,965,717],{"class":681},[675,967,968],{"class":698},"; ",[675,970,971],{"class":681},"private",[675,973,974],{"class":681}," set",[675,976,720],{"class":698},[675,978,980],{"class":677,"line":979},21,[675,981,727],{"emptyLinePlaceholder":726},[675,983,985,988,990,992,995,998,1001,1003,1006,1008,1011,1014],{"class":677,"line":984},22,[675,986,987],{"class":681},"    private",[675,989,832],{"class":691},[675,991,738],{"class":698},[675,993,994],{"class":691},"OfferId",[675,996,997],{"class":691}," id",[675,999,1000],{"class":698},", ",[675,1002,647],{"class":691},[675,1004,1005],{"class":691}," listingId",[675,1007,1000],{"class":698},[675,1009,1010],{"class":691},"BuyerId",[675,1012,1013],{"class":691}," buyerId",[675,1015,1016],{"class":698},",\n",[675,1018,1020,1023,1026,1028,1030,1033,1035,1038,1041],{"class":677,"line":1019},23,[675,1021,1022],{"class":691},"        Money",[675,1024,1025],{"class":691}," price",[675,1027,1000],{"class":698},[675,1029,542],{"class":691},[675,1031,1032],{"class":691}," earnestMoney",[675,1034,1000],{"class":698},[675,1036,1037],{"class":691},"DateTimeOffset",[675,1039,1040],{"class":691}," expiresAt",[675,1042,747],{"class":698},[675,1044,1046],{"class":677,"line":1045},24,[675,1047,753],{"class":698},[675,1049,1051,1054,1056],{"class":677,"line":1050},25,[675,1052,1053],{"class":698},"        Id ",[675,1055,799],{"class":681},[675,1057,1058],{"class":698}," id;\n",[675,1060,1062,1065,1067],{"class":677,"line":1061},26,[675,1063,1064],{"class":698},"        ListingId ",[675,1066,799],{"class":681},[675,1068,1069],{"class":698}," listingId;\n",[675,1071,1073,1076,1078],{"class":677,"line":1072},27,[675,1074,1075],{"class":698},"        BuyerId ",[675,1077,799],{"class":681},[675,1079,1080],{"class":698}," buyerId;\n",[675,1082,1084,1087,1089],{"class":677,"line":1083},28,[675,1085,1086],{"class":698},"        Price ",[675,1088,799],{"class":681},[675,1090,1091],{"class":698}," price;\n",[675,1093,1095,1098,1100],{"class":677,"line":1094},29,[675,1096,1097],{"class":698},"        EarnestMoney ",[675,1099,799],{"class":681},[675,1101,1102],{"class":698}," earnestMoney;\n",[675,1104,1106,1109,1111],{"class":677,"line":1105},30,[675,1107,1108],{"class":698},"        ExpiresAt ",[675,1110,799],{"class":681},[675,1112,1113],{"class":698}," expiresAt;\n",[675,1115,1117,1120,1122],{"class":677,"line":1116},31,[675,1118,1119],{"class":698},"        Status ",[675,1121,799],{"class":681},[675,1123,1124],{"class":698}," OfferStatus.Submitted;\n",[675,1126,1128],{"class":677,"line":1127},32,[675,1129,808],{"class":698},[675,1131,1133],{"class":677,"line":1132},33,[675,1134,727],{"emptyLinePlaceholder":726},[675,1136,1138,1140,1143,1145,1148,1150,1152,1154,1156,1158,1160],{"class":677,"line":1137},34,[675,1139,705],{"class":681},[675,1141,1142],{"class":681}," static",[675,1144,832],{"class":691},[675,1146,1147],{"class":691}," Submit",[675,1149,738],{"class":698},[675,1151,647],{"class":691},[675,1153,1005],{"class":691},[675,1155,1000],{"class":698},[675,1157,1010],{"class":691},[675,1159,1013],{"class":691},[675,1161,1016],{"class":698},[675,1163,1165,1167,1169,1171,1173,1175,1177,1179,1181,1183,1185,1188],{"class":677,"line":1164},35,[675,1166,1022],{"class":691},[675,1168,1025],{"class":691},[675,1170,1000],{"class":698},[675,1172,542],{"class":691},[675,1174,1032],{"class":691},[675,1176,1000],{"class":698},[675,1178,1037],{"class":691},[675,1180,1040],{"class":691},[675,1182,1000],{"class":698},[675,1184,1037],{"class":691},[675,1186,1187],{"class":691}," now",[675,1189,747],{"class":698},[675,1191,1193],{"class":677,"line":1192},36,[675,1194,753],{"class":698},[675,1196,1198,1200,1203,1205,1208,1210,1212,1214,1216,1219],{"class":677,"line":1197},37,[675,1199,759],{"class":681},[675,1201,1202],{"class":698}," (expiresAt ",[675,1204,765],{"class":681},[675,1206,1207],{"class":698}," now) ",[675,1209,775],{"class":681},[675,1211,778],{"class":681},[675,1213,781],{"class":691},[675,1215,738],{"class":698},[675,1217,1218],{"class":786},"\"斡旋期限必須晚於現在\"",[675,1220,790],{"class":698},[675,1222,1224],{"class":677,"line":1223},38,[675,1225,727],{"emptyLinePlaceholder":726},[675,1227,1229,1232,1235,1238,1240,1242,1245,1248],{"class":677,"line":1228},39,[675,1230,1231],{"class":681},"        var",[675,1233,1234],{"class":691}," offer",[675,1236,1237],{"class":681}," =",[675,1239,778],{"class":681},[675,1241,832],{"class":691},[675,1243,1244],{"class":698},"(OfferId.",[675,1246,1247],{"class":691},"New",[675,1249,1250],{"class":698},"(), listingId, buyerId, price, earnestMoney, expiresAt);\n",[675,1252,1254,1257,1260,1262,1265,1268],{"class":677,"line":1253},40,[675,1255,1256],{"class":698},"        offer.",[675,1258,1259],{"class":691},"AddEvent",[675,1261,738],{"class":698},[675,1263,1264],{"class":681},"new",[675,1266,1267],{"class":691}," OfferSubmitted",[675,1269,1270],{"class":698},"(offer.Id, listingId, price.Amount));\n",[675,1272,1274,1277],{"class":677,"line":1273},41,[675,1275,1276],{"class":681},"        return",[675,1278,1279],{"class":698}," offer;\n",[675,1281,1283],{"class":677,"line":1282},42,[675,1284,808],{"class":698},[675,1286,1288],{"class":677,"line":1287},43,[675,1289,727],{"emptyLinePlaceholder":726},[675,1291,1293,1295,1298,1301,1303,1305,1307],{"class":677,"line":1292},44,[675,1294,705],{"class":681},[675,1296,1297],{"class":681}," void",[675,1299,1300],{"class":691}," Accept",[675,1302,738],{"class":698},[675,1304,1037],{"class":691},[675,1306,1187],{"class":691},[675,1308,747],{"class":698},[675,1310,1312],{"class":677,"line":1311},45,[675,1313,753],{"class":698},[675,1315,1317,1319,1322,1325],{"class":677,"line":1316},46,[675,1318,759],{"class":681},[675,1320,1321],{"class":698}," (Status ",[675,1323,1324],{"class":681},"!=",[675,1326,1327],{"class":698}," OfferStatus.Submitted)\n",[675,1329,1331,1334,1336,1338,1340,1343,1346,1349,1352,1355],{"class":677,"line":1330},47,[675,1332,1333],{"class":681},"            throw",[675,1335,778],{"class":681},[675,1337,781],{"class":691},[675,1339,738],{"class":698},[675,1341,1342],{"class":786},"$\"斡旋狀態為 ",[675,1344,1345],{"class":786},"{",[675,1347,1348],{"class":698},"Status",[675,1350,1351],{"class":786},"}",[675,1353,1354],{"class":786},"，不能接受\"",[675,1356,790],{"class":698},[675,1358,1360,1362,1365,1368],{"class":677,"line":1359},48,[675,1361,759],{"class":681},[675,1363,1364],{"class":698}," (now ",[675,1366,1367],{"class":681},">",[675,1369,1370],{"class":698}," ExpiresAt)\n",[675,1372,1374,1376,1378,1380,1382,1385],{"class":677,"line":1373},49,[675,1375,1333],{"class":681},[675,1377,778],{"class":681},[675,1379,781],{"class":691},[675,1381,738],{"class":698},[675,1383,1384],{"class":786},"\"斡旋已過期，不能接受\"",[675,1386,790],{"class":698},[675,1388,1390],{"class":677,"line":1389},50,[675,1391,727],{"emptyLinePlaceholder":726},[675,1393,1395,1397,1399],{"class":677,"line":1394},51,[675,1396,1119],{"class":698},[675,1398,799],{"class":681},[675,1400,1401],{"class":698}," OfferStatus.Accepted;\n",[675,1403,1405,1408,1410,1412,1415],{"class":677,"line":1404},52,[675,1406,1407],{"class":691},"        AddEvent",[675,1409,738],{"class":698},[675,1411,1264],{"class":681},[675,1413,1414],{"class":691}," OfferAccepted",[675,1416,1417],{"class":698},"(Id, ListingId, BuyerId, Price.Amount));\n",[675,1419,1421],{"class":677,"line":1420},53,[675,1422,808],{"class":698},[675,1424,1426],{"class":677,"line":1425},54,[675,1427,727],{"emptyLinePlaceholder":726},[675,1429,1431,1433,1435,1438,1440,1442,1444],{"class":677,"line":1430},55,[675,1432,705],{"class":681},[675,1434,1297],{"class":681},[675,1436,1437],{"class":691}," Withdraw",[675,1439,738],{"class":698},[675,1441,1037],{"class":691},[675,1443,1187],{"class":691},[675,1445,747],{"class":698},[675,1447,1449],{"class":677,"line":1448},56,[675,1450,753],{"class":698},[675,1452,1454,1456,1458,1460],{"class":677,"line":1453},57,[675,1455,759],{"class":681},[675,1457,1321],{"class":698},[675,1459,1324],{"class":681},[675,1461,1327],{"class":698},[675,1463,1465,1467,1469,1471,1473,1475,1477,1479,1481,1484],{"class":677,"line":1464},58,[675,1466,1333],{"class":681},[675,1468,778],{"class":681},[675,1470,781],{"class":691},[675,1472,738],{"class":698},[675,1474,1342],{"class":786},[675,1476,1345],{"class":786},[675,1478,1348],{"class":698},[675,1480,1351],{"class":786},[675,1482,1483],{"class":786},"，不能撤回\"",[675,1485,790],{"class":698},[675,1487,1489],{"class":677,"line":1488},59,[675,1490,727],{"emptyLinePlaceholder":726},[675,1492,1494,1496,1498],{"class":677,"line":1493},60,[675,1495,1119],{"class":698},[675,1497,799],{"class":681},[675,1499,1500],{"class":698}," OfferStatus.Withdrawn;\n",[675,1502,1504,1506,1508,1510,1513],{"class":677,"line":1503},61,[675,1505,1407],{"class":691},[675,1507,738],{"class":698},[675,1509,1264],{"class":681},[675,1511,1512],{"class":691}," OfferWithdrawn",[675,1514,1515],{"class":698},"(Id, ListingId));\n",[675,1517,1519],{"class":677,"line":1518},62,[675,1520,808],{"class":698},[675,1522,1524],{"class":677,"line":1523},63,[675,1525,814],{"class":698},[10,1527,1528],{},"幾個重點：",[43,1530,1531,1544,1553],{},[46,1532,1533,1536,1537,151,1540,1543],{},[20,1534,1535],{},"沒有公開的 setter","：狀態只能透過 ",[53,1538,1539],{},"Accept()",[53,1541,1542],{},"Withdraw()"," 這些有業務意義的方法改變，規則不會被繞過",[46,1545,1546,41,1549,1552],{},[20,1547,1548],{},"時間從外面傳入",[53,1550,1551],{},"now"," 由呼叫端提供，單元測試時可以指定任意時間",[46,1554,1555,41,1558,1560],{},[20,1556,1557],{},"事件先記在聚合上",[53,1559,1259],{}," 只是暫存，儲存時才和聚合一起寫入（例如寫進同一個資料庫的 Outbox 表），避免「資料存了、事件沒送」",[10,1562,1563],{},"應用服務只負責協調：",[666,1565,1567],{"className":668,"code":1566,"language":670,"meta":671,"style":671},"public sealed class AcceptOfferHandler\n{\n    private readonly IOfferRepository _offers;\n    private readonly TimeProvider _clock;\n\n    public AcceptOfferHandler(IOfferRepository offers, TimeProvider clock)\n    {\n        _offers = offers;\n        _clock = clock;\n    }\n\n    public async Task Handle(AcceptOfferCommand command, CancellationToken ct)\n    {\n        var offer = await _offers.GetAsync(command.OfferId, ct)\n            ?? throw new NotFoundException($\"找不到斡旋 {command.OfferId}\");\n\n        offer.Accept(_clock.GetUtcNow());\n\n        await _offers.SaveAsync(offer, ct);   \u002F\u002F 同一個交易：更新聚合 + 寫入 Outbox\n    }\n}\n",[53,1568,1569,1580,1584,1600,1614,1618,1642,1646,1656,1666,1670,1674,1705,1709,1729,1764,1768,1784,1788,1804,1808],{"__ignoreMap":671},[675,1570,1571,1573,1575,1577],{"class":677,"line":678},[675,1572,682],{"class":681},[675,1574,685],{"class":681},[675,1576,829],{"class":681},[675,1578,1579],{"class":691}," AcceptOfferHandler\n",[675,1581,1582],{"class":677,"line":695},[675,1583,699],{"class":698},[675,1585,1586,1588,1591,1594,1597],{"class":677,"line":702},[675,1587,987],{"class":681},[675,1589,1590],{"class":681}," readonly",[675,1592,1593],{"class":691}," IOfferRepository",[675,1595,1596],{"class":691}," _offers",[675,1598,1599],{"class":698},";\n",[675,1601,1602,1604,1606,1609,1612],{"class":677,"line":723},[675,1603,987],{"class":681},[675,1605,1590],{"class":681},[675,1607,1608],{"class":691}," TimeProvider",[675,1610,1611],{"class":691}," _clock",[675,1613,1599],{"class":698},[675,1615,1616],{"class":677,"line":730},[675,1617,727],{"emptyLinePlaceholder":726},[675,1619,1620,1622,1625,1627,1629,1632,1634,1637,1640],{"class":677,"line":750},[675,1621,705],{"class":681},[675,1623,1624],{"class":691}," AcceptOfferHandler",[675,1626,738],{"class":698},[675,1628,586],{"class":691},[675,1630,1631],{"class":691}," offers",[675,1633,1000],{"class":698},[675,1635,1636],{"class":691},"TimeProvider",[675,1638,1639],{"class":691}," clock",[675,1641,747],{"class":698},[675,1643,1644],{"class":677,"line":756},[675,1645,753],{"class":698},[675,1647,1648,1651,1653],{"class":677,"line":793},[675,1649,1650],{"class":698},"        _offers ",[675,1652,799],{"class":681},[675,1654,1655],{"class":698}," offers;\n",[675,1657,1658,1661,1663],{"class":677,"line":805},[675,1659,1660],{"class":698},"        _clock ",[675,1662,799],{"class":681},[675,1664,1665],{"class":698}," clock;\n",[675,1667,1668],{"class":677,"line":811},[675,1669,808],{"class":698},[675,1671,1672],{"class":677,"line":817},[675,1673,727],{"emptyLinePlaceholder":726},[675,1675,1676,1678,1681,1684,1687,1689,1692,1695,1697,1700,1703],{"class":677,"line":822},[675,1677,705],{"class":681},[675,1679,1680],{"class":681}," async",[675,1682,1683],{"class":691}," Task",[675,1685,1686],{"class":691}," Handle",[675,1688,738],{"class":698},[675,1690,1691],{"class":691},"AcceptOfferCommand",[675,1693,1694],{"class":691}," command",[675,1696,1000],{"class":698},[675,1698,1699],{"class":691},"CancellationToken",[675,1701,1702],{"class":691}," ct",[675,1704,747],{"class":698},[675,1706,1707],{"class":677,"line":841},[675,1708,753],{"class":698},[675,1710,1711,1713,1715,1717,1720,1723,1726],{"class":677,"line":846},[675,1712,1231],{"class":681},[675,1714,1234],{"class":691},[675,1716,1237],{"class":681},[675,1718,1719],{"class":681}," await",[675,1721,1722],{"class":698}," _offers.",[675,1724,1725],{"class":691},"GetAsync",[675,1727,1728],{"class":698},"(command.OfferId, ct)\n",[675,1730,1731,1734,1737,1739,1742,1744,1747,1749,1752,1755,1757,1759,1762],{"class":677,"line":863},[675,1732,1733],{"class":681},"            ??",[675,1735,1736],{"class":681}," throw",[675,1738,778],{"class":681},[675,1740,1741],{"class":691}," NotFoundException",[675,1743,738],{"class":698},[675,1745,1746],{"class":786},"$\"找不到斡旋 ",[675,1748,1345],{"class":786},[675,1750,1751],{"class":698},"command",[675,1753,1754],{"class":786},".",[675,1756,994],{"class":698},[675,1758,1351],{"class":786},[675,1760,1761],{"class":786},"\"",[675,1763,790],{"class":698},[675,1765,1766],{"class":677,"line":884},[675,1767,727],{"emptyLinePlaceholder":726},[675,1769,1770,1772,1775,1778,1781],{"class":677,"line":900},[675,1771,1256],{"class":698},[675,1773,1774],{"class":691},"Accept",[675,1776,1777],{"class":698},"(_clock.",[675,1779,1780],{"class":691},"GetUtcNow",[675,1782,1783],{"class":698},"());\n",[675,1785,1786],{"class":677,"line":916},[675,1787,727],{"emptyLinePlaceholder":726},[675,1789,1790,1793,1795,1798,1801],{"class":677,"line":936},[675,1791,1792],{"class":681},"        await",[675,1794,1722],{"class":698},[675,1796,1797],{"class":691},"SaveAsync",[675,1799,1800],{"class":698},"(offer, ct);   ",[675,1802,1803],{"class":880},"\u002F\u002F 同一個交易：更新聚合 + 寫入 Outbox\n",[675,1805,1806],{"class":677,"line":953},[675,1807,808],{"class":698},[675,1809,1810],{"class":677,"line":979},[675,1811,814],{"class":698},[10,1813,1814],{},[82,1815],{"alt":1816,"src":1817},"一次「接受斡旋」的處理流程：屋主發出接受斡旋的命令，Offer 聚合檢查狀態為已提出且未過期，通過後狀態改為已接受並發出 OfferAccepted 事件，這三步在交易 1 內完成，只修改 Offer 一個聚合；事件送達後，政策「斡旋被接受時，物件改為已斡旋」觸發，Listing 聚合在交易 2 中把狀態改為已斡旋並發出 ListingUnderOffer 事件，這是最終一致。聚合之間不共用交易，用 Id 參照、用領域事件串接","\u002Fimages\u002Farticles\u002Fddd-aggregate-offer.svg",[10,1819,1820,1821,1823,1824,1826,1827,1830],{},"如果把 ",[53,1822,651],{}," 和所有 ",[53,1825,250],{}," 放進同一個大聚合，「接受斡旋」和「物件改狀態」就能在一個交易裡完成，看起來比較簡單。但代價是每次有人提出斡旋、經紀人改刊登文案，都要鎖住同一個大聚合，熱門物件很容易出現併發衝突。",[20,1828,1829],{},"先問業務：物件狀態晚幾秒更新，會不會有問題？"," 大多數時候答案是不會，那就應該拆開。",[31,1832,1834],{"id":1833},"協作探索event-storming","協作探索：Event Storming",[10,1836,1837,1838,1841],{},"前面的子域、上下文、聚合都需要一個前提：",[20,1839,1840],{},"真的理解業務","。這份理解在領域專家（業務、PO、資深經紀人）的腦袋裡，不在需求文件裡。",[10,1843,1844],{},"Event Storming 是 Alberto Brandolini 提出的工作坊形式：把領域專家和開發人員找到同一面牆前，用不同顏色的便利貼，沿著時間軸貼出業務上發生的所有事件（橘色），再補上觸發事件的命令（藍色）、角色、政策、外部系統，以及有疑問的熱點。",[10,1846,1847],{},"它有三種層次：",[90,1849,1850,1863],{},[93,1851,1852],{},[96,1853,1854,1857,1860],{},[99,1855,1856],{},"層次",[99,1858,1859],{},"目的",[99,1861,1862],{},"產出",[109,1864,1865,1876,1887],{},[96,1866,1867,1870,1873],{},[114,1868,1869],{},"Big Picture",[114,1871,1872],{},"看清楚整個業務的全貌",[114,1874,1875],{},"完整的事件時間軸、熱點、子域與上下文的候選邊界",[96,1877,1878,1881,1884],{},[114,1879,1880],{},"Process Modeling",[114,1882,1883],{},"拆解單一流程",[114,1885,1886],{},"命令、政策、讀取模型、角色之間的完整流程",[96,1888,1889,1892,1895],{},[114,1890,1891],{},"Software Design",[114,1893,1894],{},"設計程式",[114,1896,1897],{},"聚合的候選、每個聚合負責的命令與不變條件",[10,1899,1900,1901,1904],{},"一場 Big Picture 下來，最有價值的常常不是便利貼本身，而是",[20,1902,1903],{},"大家發現彼此的理解不一樣","：業務部門以為「斡旋被接受」就是成交，財務部門以為「簽約完成」才是成交，這就是一個熱點，也是一個可能的上下文邊界。實際怎麼準備、怎麼引導，之後會另外整理一篇。",[31,1906,1908],{"id":1907},"和六角形架構cqrs-的關係","和六角形架構、CQRS 的關係",[10,1910,1911],{},"DDD 不是一種架構，而是一種設計方法，它和其他架構模式是互補的：",[43,1913,1914,1919,1928],{},[46,1915,1916,1918],{},[20,1917,17],{},"決定「依賴方向」：戰術設計的聚合、值物件、領域事件放在核心，Repository 是核心定義的 Port，EF Core 實作是 Adapter",[46,1920,1921,1927],{},[20,1922,1923],{},[14,1924,1926],{"href":1925},"\u002Farticles\u002Fcqrs-elasticsearch-read-model","CQRS"," 決定「讀寫分開」：聚合只負責寫入時的規則，查詢需求（例如搜尋列表）不需要經過聚合，直接讀取讀取模型",[46,1929,1930,1933],{},[20,1931,1932],{},"領域事件","是兩者之間的橋梁：聚合發出事件，讀取模型根據事件更新",[31,1935,1937],{"id":1936},"什麼時候不需要-ddd","什麼時候不需要 DDD",[43,1939,1940,1946,1952],{},[46,1941,1942,1945],{},[20,1943,1944],{},"以 CRUD 為主的系統","：後台設定頁、簡單的資料維護，沒有什麼業務規則，用聚合和領域事件只會增加複雜度",[46,1947,1948,1951],{},[20,1949,1950],{},"找不到領域專家","：DDD 的前提是和懂業務的人持續對話。如果需求只能從文件和截圖猜，通用語言就建立不起來",[46,1953,1954,1956],{},[20,1955,333],{},"：會員登入、檔案上傳這類問題，用現成方案就好",[10,1958,1959,1960,1963],{},"同一個系統裡，",[20,1961,1962],{},"核心子域用完整的 DDD，其他部分用簡單的做法","，這才是 DDD 建議的投資方式。",[31,1965,1966],{"id":1966},"常見誤解",[43,1968,1969,1975,1981,1987],{},[46,1970,1971,1974],{},[20,1972,1973],{},"DDD 等於微服務","：限界上下文可以是單體裡的一個模組。先在單體裡把邊界切清楚，之後要拆服務才拆得動；邊界沒切清楚就拆成微服務，只會得到分散式的大泥球",[46,1976,1977,1980],{},[20,1978,1979],{},"有 Entity、Repository 資料夾就是 DDD","：如果業務規則還是寫在 Service 裡、Entity 只有 getter\u002Fsetter（貧血模型），那只是換了資料夾名稱",[46,1982,1983,1986],{},[20,1984,1985],{},"一個聚合對應一張資料表","：聚合是一致性邊界，不是資料表。一個聚合可以存成多張表，也可以存成一份 JSON",[46,1988,1989,1992],{},[20,1990,1991],{},"一開始就要設計出正確的模型","：模型會隨著對業務的理解而演進。Evans 稱之為「突破」：某次討論後發現一個更好的概念，模型就要跟著重構",[31,1994,1995],{"id":1995},"一句話總結",[10,1997,1998],{},"DDD 的重點不是 Entity 和 Repository，而是先和領域專家一起把業務攤開（Event Storming），建立團隊共用的語言，依照業務的重要性和語言的差異切出限界上下文（戰略設計），最後才在每個上下文裡用聚合守住真正需要即時一致的規則、用領域事件串起其他部分（戰術設計）。",[2000,2001,2002],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":671,"searchDepth":695,"depth":695,"links":2004},[2005,2006,2007,2013,2016,2017,2018,2019,2020],{"id":33,"depth":695,"text":34},{"id":77,"depth":695,"text":78},{"id":116,"depth":695,"text":116,"children":2008},[2009,2010,2011,2012],{"id":168,"depth":702,"text":169},{"id":273,"depth":702,"text":274},{"id":352,"depth":702,"text":353},{"id":399,"depth":702,"text":400},{"id":127,"depth":695,"text":127,"children":2014},[2015],{"id":613,"depth":702,"text":614},{"id":1833,"depth":695,"text":1834},{"id":1907,"depth":695,"text":1908},{"id":1936,"depth":695,"text":1937},{"id":1966,"depth":695,"text":1966},{"id":1995,"depth":695,"text":1995},"2024-01-16","在 六角形架構 那篇，我把業務規則都放進「核心」，外部技術變成可以抽換的 Adapter。但做完之後留下兩個問題：核心裡面的模型要怎麼設計？ 以及系統越長越大時，一個核心要不要切成好幾個？要從哪裡切？",null,"md",{},"\u002Farticles\u002Fddd-overview-strategic-tactical",{"title":5,"description":2022},"articles\u002Fddd-overview-strategic-tactical",[2030,2031,2032,2033,2034],"DDD","領域驅動設計","架構設計","系統設計",".NET","VYjDNRSWnEnRljX3oFgbnHcfI-IrL76GTJe31FYU63o",1792959150436]