Share
Embed
まずはドメインに向き合って、それからCQRSで実装する
memetics10
January 10, 2026
Transcript
37 slides
-
εςʔΫϗϧμʔͷ૾Λ͑ΔͨΊʹɺΤϯδχ ΞϓϩμΫτσβΠϯʹίϛοτ͢Δɻਖ਼͍͠ ΞʔΩςΫνϟύλʔϯͰɺؒҧͬͨϓϩμΫτΛ ࡞Δ͜ͱආ͚͍ͨɻ &SJD&WBOT %PNBJO%SJWFO%FTJHO5BDLMJOH$PNQMFYJUZJOUIF)FBSUPG4PGUXBSF 'JSTUBOEGPSFNPTU ϓϩμΫτσβΠϯ ʹ࣠Λஔ͘ ιϑτΣΞͷ֩৺ɺυϝΠϯʹؔͨ͠Λ
ϢʔβʔͷͨΊʹղܾ͢ΔೳྗͰ͋Δɻ ͦΕҎ֎ͷಛ͍ͣΕɺͲΕ΄Ͳॏཁͩͱͯ͠ɺ ͜ͷجຊతͳతΛࢧ͑Δʹ͗͢ͳ͍ɻ -
$234ΞʔΩςΫνϟύλʔϯͷҰछ͕ͩɺ ϓϩμΫτσβΠϯͷώϯτ͕͍ͬͯΔ ɾ.7$ ɾϨΠϠʔυΞʔΩςΫνϟ ɾΠϕϯτιʔγϯά ɾFUDʜ ɾϢʔβʔυϝΠϯͷੳ ɾ֓೦Ϟσϧ ɾ69ઃܭ ɾFUDʜ
ΞʔΩςΫνϟύλʔϯ ϓϩμΫτσβΠϯ #VU 8IZ$234 ιϑτΣΞͷ෦Λମܥతʹཧ͢Δํ๏ ϢʔβʔχʔζʹରԠ͢ΔΛ࡞͢Δϓϩηε -
ϢϏΩλεݴޠͷಈࢺ͕$36%ʹऩଋ͢Δ͜ͱ͋·Γͳ͍ ΦϒδΣΫτͷߏؔੑ͋Δ͕ɺৼΔ͍ͷͳ͍ʮυϝΠϯϞσϧශ݂ʯ σʔλͷ$36%͕த৺ͷΞʔΩςΫνϟͰɺυϝΠϯۦಈઃܭ͕ෆՄೳ Ϣʔβʔମݧ͕$36%ʹҾ͖ͣΒΕΔͷɺ༨ܭʹѱ͍ (SFH:PVOH $234EPDVNFOUTCZ(SFH:PVOH "TUFSFPUZQJDBMFYBNQMFPGUIJTXPVMECFXIFODIBOHJOHUIFTFYPGBOFNQMPZFF ZPVNVTUBGUFSHPFEJUUIFJSIFBMUIJOTVSBODFJOGPSNBUJPO 5IJTJTGBSXPSTFUIBOUIFDSFBUJPOPGBOBOFNJDNPEFM UIJTJTUIFDSFBUJPOPGBHMPSJ
fi FE FYDFMTQSFBETIFFU %PNBJO%SJWFO%FTJHO -
$234 $234ΦϒδΣΫτΛίϚϯυͱΫΤϦʹ͢Δ୯७ͳΞʔΩς Ϋνϟύλʔϯ͕ͩɺͦͷԸܙΛेʹड͚ΔͨΊʹ·ͣɺϢʔβʔ ͷҙਤΛΈऔΓɺମݧʹؾΛΔ (SFH:PVOH $234 5BTL#BTFE6*T &WFOU4PVSDJOHBHI $234JTTJNQMZUIFDSFBUJPOPGUXPPCKFDUTXIFSFUIFSFXBTQSFWJPVTMZPOMZPOF5IF TFQBSBUJPOPDDVSTCBTFEVQPOXIFUIFSUIFNFUIPETBSFBDPNNBOEPSBRVFSZ
UIFTBNF EF fi OJUJPOUIBUJTVTFECZ.FZFSJO$PNNBOEBOE2VFSZ4FQBSBUJPO BDPNNBOEJTBOZ NFUIPEUIBUNVUBUFTTUBUFBOEBRVFSZJTBOZNFUIPEUIBUSFUVSOTBWBMVF -
ٕज़తʹͲͪΒ୯७ͳߏମͱ͍͏Ͱಉ͡Ͱɺ໋໊ํ๏ʹҧ͍͕͋Δ ݴޠతͳҙຯʹ͠ɺ υϝΠϯϞσϧͱϢϏΩλεݴޠͷ୳ࡧʹ͏ type DeactivateInventoryItem struct { InventoryItemID string Comment
string } type InventoryItemDeactivated struct { InventoryItemID string Comment string } &%"ɺΠϕϯτʹ࣍ͷࢦؚ͕ࣔ·Εͳ͍ҙຯతͳཧ༝Ͱૄ݁߹ʹͳΔ $PNNBOE&WFOU -
ΠϕϯτετʔϛϯάΛɺσβΠϯͷख๏ͱͯ͠׆༻͢Δ ҟͳΔόοΫάϥϯυΛ࣋ͭਓͷߟ͑Λൃࢄͤ͞ɺΧϥʔύζϧͱରΛ௨ͯ͠߹ҙͤ͞Δ ·ͣΛ͘୳ΓɺͦΕ͔Βղܾऩଋ͍ͤͯ͘͞ͰɺσβΠϯࢥߟʹΑ͘ࣅ͍ͯΔ Πϕϯτ ϙϦγʔ ू ίϚϯυ ίϚϯυ Πϕϯτ ू
%PO/PSNBO 5IF%FTJHOPG&WFSZEBZ5IJOHT ൴Βɺຊͷ͕Կ͔ΛݟఆΊΔ·ͰղܾΛ୳ͦ͏ͱ͠ͳ͍ɻ ͕ఆ·ͬͨͱ͖Ͱ͑͞ɺΛղܾ͠Α͏ͱ͠ͳ͍ɻ ൴Βཱͪࢭ·͍ͬͯൣғ͔ΒՄೳͳ͑Λ୳͢ɻͦ͏ͯ͠ॳΊͯɺ࠷ऴతʹ൴ΒͷఏҊΛऩଋ͍ͤͯ͘͞ɻ ͜ͷϓϩηεσβΠϯࢥߟͱݺΕ͍ͯΔɻ &WFOU4UPSNJOH -
Πϕϯτൃੜ࣌ؒܦաͰىಈ͢ΔϦΞΫςΟϒϩδοΫɺΠϕϯτͱίϚϯυͷؒʹ͋Δࢵ৭ͷᝦ 1PMJDZ ࡏݿ͕ϚΠ φεͷ߹ ೖՙͪΛ ͢Δ ೖՙ͕ͪ ͞Εͨ ೖՙͪ ूͷෆม݅Ͱͳ͍ɺൺֱతΏΔ͔ͳϏδωε্ͷҙࢥܾఆ
ࡏݿ͕֬อ ͞Εͨ ࡏݿ ࡏݿΛ֬อ ͢Δ ࡏݿ͕֬อ ͞Εͨ ࡏݿ ࡏݿΛ֬อ ͢Δ Ұ୴ࡏݿͷϚΠφεΛڐ༰ͯ͠ɺʮͦΕ͔ΒʯೖՙͪͷରԠΛ͢Δ -
จ͞Εͨ จ͞Εͨ Βඞͣ จ จ͢Δ υϦϯΫΛ ࡞Δ υϦϯΫ υϦϯΫ͕ ࡞ΒΕͨ
υϦϯΫ͕ ͨ͠Β ඞͣ ٬ͷ໊લΛ ݺͿ ࢧ͍Λ͢ Δ ࢧ͍ ࢧ͍͕ ྃͨ͠ ࢧ͍Ͱ͖ ͳ͔ͬͨ ߹ υϦϯΫΛ ࣺͯΔ จ͢Δ ࢧ͍υ ϦϯΫ࡞ ࢧ͍υ ϦϯΫ࡞ ྃ "HHSFHBUF1PMJDZ ੍ΛकΓͳ͕ΒɺॊೈͰϊϯϒϩοΩϯάͳΠϯλϥΫγϣϯΛߟ͑Δ (SFHPS)PIQF 4UBSCVDLT%PFT/PU6TF5XP1IBTF$PNNJU XFDBOTFFUIBUUIFSFBMXPSMEJTPGUFOBTZODISPOPVT 0VSEBJMZMJWFTDPOTJTUTPGNBOZDPPSEJOBUFE CVUBTZODISPOPVTJOUFSBDUJPOT %#ϩοΫ͢Ε͍͍ɺͱٕज़ओಋͰߟ͑ͳ͍Α͏ʹ͢Δ -
ͷจ͕ ྃͨ͠ จ ͷจΛ ྃͤ͞Δ αϒεΫϦϓ γϣϯొ͢ Δ αϒεΫ ͷ߹
αϒεΫϦϓ γϣϯొ͞ Εͨ αϒεΫϦϓ γϣϯ ߘ͕ ެ։͞Εͨ ߘ ߘΛ ެ։͢Δ ސ٬ ίϛϡχςΟ ཧऀ ސ٬༻ͷ ߘϏϡʔ 'JSTU.PEFM -
ͷจ͕ ྃͨ͠ αϒεΫ ͷ߹ จ ͷจΛ ྃͤ͞Δ αϒεΫϦ ϓγϣϯొ ͢Δ
αϒεΫϦϓ γϣϯొ͞ Εͨ αϒεΫϦϓ γϣϯ ߘ͕ ެ։͞Εͨ ߘ ߘΛ ެ։͢Δ ސ٬ ίϛϡχςΟ ཧऀ ސ٬༻ͷ ߘϏϡʔ αϒεΫϦϓ γϣϯొ͞ ΕͨΒ ձһূΛड ͚औΔ ձһূ͕ड͚ औΒΕͨ ձһূ ձһূ ձһূ͕ఀࢭ ͞Εͨ ձһূΛఀ ࢭ͢Δ αϒεΫϦ ϓγϣϯΛ ແޮԽ͢Δ αϒεΫϦϓ γϣϯ͕ແޮ Խ͞Εͨ αϒεΫϦϓ γϣϯ αϒεΫϦϓ γϣϯ͕ແޮ Խ͞ΕͨΒ ސ٬ αʔϏεͷࢧ͍ͱར༻ͷݖརΛ͠ɺ ձһূΛड͚औͬͨɺͱ͍͏ࣄ࣮ΛՃ 3FDPOTJEFS%PNBJO.PEFM -
υϝΠϯΤΩεύʔταϒεΫʹΑΔ҆ఆऩӹΛୈҰʹߟ͍͑ͯͨɻυϝΠϯΤΩεύʔτݸผʹಛԽͨࣝ͠Λ ͍࣋ͬͯΔ͕ɺશମ࠷దͷղܾࡦΛ͍࣋ͬͯΔΘ͚Ͱͳ͍ υϝΠϯʹ౿ΈࠐΜͰɺղܾࡦΛ͢Δ %FTJHO%PNBJO.PEFM &SJD&WBOT %PNBJO%SJWFO%FTJHO5BDLMJOH$PNQMFYJUZJOUIF)FBSUPG4PGUXBSF զʑͷϞσϧɺ͜ͷϏδωεʹ;͞Θ͘͠ͳ͍ํ๏Ͱɺ ϑΝγϦςΟͷ୲ͱϩʔϯͷ୲Λ݁ͼ͚͍ͭͯͨͷͰ͋Δɻ தུ
ϑΝγϦςΟϩʔϯͷγΣΞʹؔͯ͠ɺಛผͳΦϒδΣΫτ͏ͳ͘ͳͬͨɻ ͲͪΒɺΑΓײతʹΘ͔ΔʮγΣΞύΠʯʹׅΓग़͞Εͨͷͩɻ ٕज़ྗͱྗͰɺࣗͨͪͳΓͷʮγΣΞύΠʯΛൃݟ͢Δ -
ίϚϯυɺΠϕϯτɺूɺϙϦγʔͱ͍ͬͨ֓೦Λ࣮͢Δ͕ɺ CQRS+ES Θͳ͍ 6EJ%BIBO IUUQTYDPN6EJ%BIBOTUBUVT %POU$234 /PUBTBUPQMFWFMBSDIJUFDUVSF /PUVOUJMZPVWFUBTLPSJFOUFEZPVS6* /PUVOUJMZPVWFTFQBSBUFQSJWBUFEBUB fl
PXTGSPNQVCMJD"OEJGUIFOFXQSPKFDUJTUSVMZ HSFFO fi FME EF fi OJUFMZEPOU$234 BTZPVQSPCBCMZXPOUCFBCMFUPBEESFTT "SDIJUFDUVSF%FDJTJPO -
"SDIJUFDUVSF$IBSBDUFSJTUJDT $234 &4ɺύϑΥʔϚϯεͱεέʔϥϏϦςΟʹد༩͢Δ ίϚϯυͱΫΤϦΛݸผʹ࠷దԽͰ͖ΔͷͰɺϋΠτϥϑΟοΫ͔ͭڝ߹͕ى͖͍͢ྖҬʹ͘ (SFH:PVOH $234EPDVNFOUTCZ(SFH:PVOH 5IFCPUUMFOFDLJOUFSNTPGTDBMJOHJTUIFEBUBTUPSBHF8IFOVTJOHB3%#.4BT DVSSFOUMZVTFUIJTCFDPNFTFWFONPSFPGBQSPCMFNNPTU3%#.4BSFBUUIJTQPJOUOPU IPSJ[POUBMMZTDBMBCMFBOEWFSUJDBMMZTDBMJOHCFDPNFTQSPIJCJUJWFMZFYQFOTJWFWFSZRVJDLMZ
Πϕϯτόʔδϣχϯάूڥքมߋͷස͕ߴ͍ෆ҆ఆͳྖҬͰมߋίετΛճऩ͢Δͷ͕͍͠ -
Collaborative Domain Independent Domain 1VCMJD%BUB Ձ֨ 1SJWBUF%BUB ໊ આ໌ ը૾
CRUD, Stereotype Architecture CQRS+ES Publish Data 6EJ%BIBO IUUQTYDPN6EJ%BIBOTUBUVT 1VCMJDEBUBXPVMEOPUCFIBOEMFEJOUIFTBNFWFSUJDBMTMJDFBTUIFQSJWBUFEBUB $234JTBNPSFBQQSPQSJBUFDIPJDFGPSQVCMJDEBUB BOEUIFSF JUDBONBLFTFOTFUPIBWFEJ ff FSFOUWFSUJDBMTMJDFTGPSUIFDPNNBOEBOERVFSZQBSUT #PVOEFE$POUFYU 5PQ-FWFM"SDIJUFDUVSF -
package subscription // ूϧʔτ type Subscription struct { ID uuid.UUID
Status Status } // ूϧʔτͷίϚϯυϝιου͕ɺෆม݅Λҡ࣋͠ͳ͕ΒΠϕϯτΛ࡞͢Δ func (s Subscription) Deactivate(deactivateAt time.Time) (Deactivated, error) { // write some invariants here if s.Status == Deactivated { return Deactivated{}, fmt.Errorf("subscription is already deactivated: %w", errors.ErrPreconditionFailed) } return Deactivated{SubscriptionID: s.ID, DeactivateAt: deactivateAt}, nil } // Πϕϯτ type Deactivated struct { SubscriptionID uuid.UUID DeactivatedAt time.Time } func (SubscriptionDeactivated) IsSubscriptionEvent() {} type Event interface { IsSubscriptionEvent() } // ϦϙδτϦ: Πϕϯτͷอଘͱूͷϩʔυ͕Մೳ type Repository interface { Store(context.Context, Event) error Load(context.Context, uuid.UUID) (Subscription, error) } *NQMFNFOUJOH"HHSFHBUF -
package command // ίϚϯυ: ૢ࡞ͷ໊લͱͦΕ࣮ߦ͢ΔͨΊʹඞཁͳೖྗσʔλΛ࣋ͭ type DeactivateSubscriptionCommand struct { SubscriptionID
uuid.UUID } // ίϚϯυϓϩηοαʔ: ूʹίϚϯυॲཧΛҕৡ͢Δ func (p DeactivateSubscriptionCommandProcessor) Exec(ctx context.Context, command DeactivateSubscriptionCommand) (subscription.Deactivated, error) { aSubscription, err := p.SubscriptionRepo.Load(ctx, command.SubscriptionID) if err != nil { return subscription.Deactivated{}, fmt.Errorf("subscription.Repository.Load: %w", err) } event, err := aSubscription.Deactivate(time.Now()) if err != nil { return subscription.Deactivated{}, fmt.Errorf("subscription.Subscription.Deactivate: %w", err) } if err := p.SubscriptionRepo.Store(ctx, event); err != nil { return subscription.Deactivated{}, fmt.Errorf("subscription.Repository.Store: %w", err) } if err := p.EventPublisher.Publish(ctx, event); err != nil { return subscription.Deactivated{}, fmt.Errorf("EventPublisher.Publish: %w", err) } return event, nil } *NQMFNFOUJOH$PNNBOE -
package redis type SubscriptionDeactivatedEventHandler struct { AsynqClient *asynq.Client } func
(h SubscriptionDeactivatedEventHandler) HandleSubscriptionDeactivated(ctx context.Context, event subscription.Deactivated) error { // ϏδωεϧʔϧΓग़͓ͯ͘͠ if policy.CanDeactivateMembershipcard(event) { payload, err := json.Marshal(task.DeactivateMembershipcardPayload{SubscriptionID: event.SubscriptionID}) if err != nil { return fmt.Errorf("marshal payload: %w", err) } // Ωϡʔʹ٧ΊͯผϓϩηεʹॲཧΛͤΔ _, err = h.AsynqClient.EnqueueContext(ctx, asynq.NewTask(task.DeactivateMembershipcard, payload)) if err != nil { return fmt.Errorf("enqueue process deactivate membershipcard task: %w", err) } } return nil } ˞࣮ߦͰ͖ͳ͍߹ͷϦτϥΠՄೳੑɺෳճ࣮ߦ͞Εͨ߹ͷႈੑɺଞͷΠϕϯτͱͷॱংੑɺΛߟྀ͓ͯ͘͠ඞཁ͋Γ *NQMFNFOUJOH1PMJDZ -
package query // ΫΤϦαʔϏε type SubscriptionService interface { QueryPage(ctx context.Context,
filter SubscriptionFilter, sorter SubscriptionSorter, limit int, cursor *string) (Page[Subscription], error) QueryByIDs(context.Context, []string) ([]Subscription, error) QueryByID(context.Context, string) (Subscription, error) } // ΫΤϦϞσϧ type Subscription struct { ID string CustomerID string ProductID string NextRenewAt time.Time LastRenewedAt time.Time } // ϑΟϧλʔ&ιʔτ type SubscriptionFilter struct { CustomerID string } type SubscriptionSorter struct { Key SubscriptionSortKey Direction SortDirection } 2VFSZ4FSWJDF.PEFM -
$36%ΛϢϏΩλεݴޠʹ͍ͯ͠Δ෦ͷ$234࣮ɺΓແବ ͱ͍͑ผͷΞʔΩςΫνϟύλʔϯͱڞଘͤ͞ΔͷɺҙࢥܾఆͷසֶशίετΛ૿ͯ͠͠·͏ ίϚϯυʹҙਤ͕ͳ͘ɺूͰڧ੍͢Δෆม͕݅ͳ͘ɺΠϕϯτ࣮࣭ूͷεφοϓγϣοτʹʜ package product func (p Product) Update(name Name,
description Description, updateAt time.Time) (ProductUpdated, error) { return ProductUpdated{ ProductID: p.ID, Name: name.value, Description: description.value, UpdatedAt: updateAt, }, nil } #VU$36%ʜʁ -
7BVHIO7FSOPO JNQMFNFOUJOHEPNBJOESJWFOEFTJHO ͋·ΓΞʔΩςΫνϟʹؾΛͱΒΕ͗ͯ͢͠·͏ͱɺ %%%Λͬͯҙਂ͘ϞσϧΛ࡞Δ͜ͱ͕ ͓Ζ͔ͦʹͳͬͯ͠·͍͕ͪͩɻ தུ υϝΠϯϞσϧͷ΄͏͕ࣄۀՁΛଟ͘ੜΈग़͢͠ɺ ͦͷӨڹଓ͖͢Δɻ %FTJHO'PS8IPNʁ
෦࣭Λॏཁࢹͭͭ͠ɺ ᘳओٛʹؕΒͳ͍Α͏ʹ͢Δ υϝΠϯͱϢʔβʔͷՁʹ ͳΔͷ͔ʁͱ͍͏͍ʹཱͪΔ