10/14 の Tech Play での発表資料です https://techplay.jp/event/873259
5ZQF4DSJQUʹΑΔ (SBQI2-όοΫΤϯυ։ൃ5ZQF4DSJQUͷܕγεςϜͱσʔλϑϩʔʹணͨ͠એݴతϓϩάϥϛϯάגࣜձࣾ Ұٳҏ౻
View Slide
Ϟνϕʔγϣϯ
ࡢࠓɺϑϩϯτΤϯυ όοΫΤϯυͷٕज़తؔ৺ࣄʹΪϟοϓ• ΞϓϦέʔγϣϯͷঢ়ଶཧϞσϧ• σβΠϯγεςϜ• ϓϦϨϯμϦϯά• ŋŋŋϑϩϯτΤϯυόοΫΤϯυ• υϝΠϯϞσϧ• ϨΠϠʔυɾΞʔΩςΫνϟ• $234• ŋŋŋ৫ͷٕज़࿅্͕͕Ε্͕Δ΄Ͳɺؔ৺ࣄͷΪϟοϓ͕͕͍ͬͯͬͨ
3FBDUͰϑϩϯτΤϯυΛ։ൃ͔ͯ͠Βɺ όοΫΤϯυΛॻ͘ͱŋŋŋ• 3FBDUŋŋŋ খ͞ͳؔΛΈ߹Θͤͯએݴతʹॻ͍͍ͯ͘• όοΫΤϯυ ŋŋŋ ΫϥεΛͨ͘͞Μॻ͍ͯɺϨΠϠʔΛލ͙ͱ %50Ͱͷ٧Ίସ͑ΛߦͬͯɺJOUFSGBDFͰґଘੑͷٯసΛߦͬͯŋŋŋ– ʮŋŋŋϑϩϯτΤϯυͩͱ͜͏͍͏͜ͱɺ͋Μ·ΓΒͳ͍ΑͶʯ։ൃ࣌ͷϝϯλϧϞσϧͷΪϟοϓ͕େ͖͍ίϯςΩετεΠονͷෛ୲େ͖͍
όοΫΤϯυ։ൃͷΓํΛ࠶ߟͯ͠Έ͍ͨ• 3FBDUΛ͍ͬͯΔͱϑϩϯτΤϯυബ͘ॻ͘͜ͱ͕Ͱ͖Δ• ؔ৺ࣄ͕ҧ͏ͷવɻ͔ͱ͍ͬͯɺΓํ͕ҧ͏ͷΛશٙ͘Θͳ͍ͷͲ͏ͩΖ͏ϑϩϯτΤϯυͷঢ়ଶཧෳࡶͦͷෳࡶͳͷΛͲ͏ѻ͏͔ɺݱ࣌Ͱ࠷ྑͷϞσϧͷͻͱ͕ͭ 3FBDUͷͣʮෳࡶͳঢ়ଶΛͲ͏ѻ͏͔ʯͱ͍͏؍ͰɺαʔόʔαΠυಉ͡Α͏ʹߟ͑ΒΕͳ͍ͷ͔Α͠ɺ(SBQI2-όοΫΤϯυ 5ZQF4DSJQUͰॻ͍ͯΈΑ͏
ຊʹೖΔલʹɺ(SBQI2-όοΫΤϯυͱ $234
$234• ίϚϯυΫΤϦݪଇ $PNNBOE2VFSZ3FTQPOTJCJMJUZ4FHSFHBUJPO$234– ΞϓϦέʔγϣϯ࣮ͷจ຺Ͱࢀরܥ 2VFSZͱߋ৽ܥ $PNNBOEͰҟͳΔϞσϧΛ͏Ξϓϩʔν
(SBQI2-2VFSZͱυϝΠϯϞσϧͷʮूʯɺטΈ߹Θ͕ͤѱ͍• (SBQI2-2VFSZ ˞ .VUBUJPOͰͳ͍– ΫϥΠΞϯτ͕ϢʔεέʔεΛߏ͢Δ– ू୯ҐͰͳ͘ɺϦιʔε୯ҐͰૢ࡞͞ΕΔ• (SBQI2-2VFSZʹݶΒͣूΛશ෦Ҿ͘Θ͚ʹ͍͔ͳ͍໘ͰͲ͏͢Δ͔ŋŋŋ '"2– ྫݕࡧҰཡϖʔδ– $234Λద༻ͯ͠ 2VFSZ4FSWJDFΛ࣮͢Δͱ͍͏ͷ͕ɺͻͱͭͷղܾࡦ
(SBQI2-2VFSZͰෳࡶͳۀϩδοΫ͕͋·Γඞཁͳ͍ ˞զʑͷ߹• ܦݧతʹ͔͖ͬͯͨ– ଟ͘σʔλϕʔεͷΛͦͷ·· (SBQI2-0CKFDUʹ͢Εྑ͍͚ͩ– ϦονͳʮυϝΠϯϞσϧʯࢀরܥʹɺඞཁͳ͍͜ͱ͕ଟ͍• ͦͦ %%%ͷूࢀরͱ͍͏ΑΓɺूશମͷ߹ੑΛอͭ͜ͱʹॏ͖͕ஔ͔Ε͍ͯΔŋŋŋͣ
https://speakerdeck.com/qsona/architecture-decision-for-the-next-n-years-at-studysapuri?slide=35
1SJTNB
1SJTNB• ʮ03.ʯͱ͋Δ͕࣮ࡍΫΤϦϏϧμʔ ϓϨʔϯͳΦϒδΣΫτΛฦ͢σʔλΞΫηεϥΠϒϥϦ• ࠓͷͱ͋·Γؔͳ͍ϓϩάϥϛϯάݴޠʹґଘ͠ͳ͍εΩʔϚఆٛɺϚΠάϨʔγϣϯػߏΛ͍࣋ͬͯΔͷ͕ॴɻ։ൃऀମݧ͕ͱͯྑ͍
Domain ModelGraphQL MutationRepositoryߋ৽ܥGraphQL QueryPrismaPrisma࣌ʑബ͍%PNBJOࢀরܥ(SBQI2-1SJTNB$234
Domain ModelGraphQL MutationRepositoryߋ৽ܥͷυϝΠϯϞσϧΛͲ͏ॻ͔͘ɻ͔͜͜Β͕ຊߋ৽ܥGraphQL QueryPrismaPrisma薄いDomain Layerࢀরܥ
8FCΞϓϦέʔγϣϯόοΫΤϯυͷ࣮ํ๏Λ࠶ߟ͢Δ
ࡢࠓͷϑϩϯτΤϯυͷϓϩάϥϛϯάύϥμΠϜΛߟ͑ͯΈΔhttps://zenn.dev/mizchi/articles/oop-think-modern
&MNΞʔΩςΫνϟhttps://guide.elm-lang.jp/architecture/
update : Msg -> Model -> ( Model, Cmd Msg )update msg model =case msg ofToggleLike ->( { model | photo = Maybe.map toggleLike model.photo }, Cmd.none )UpdateComment comment ->( { model | photo = Maybe.map (updateComment comment) model.photo }, Cmd.none )SaveComment ->( { model | photo = Maybe.map saveNewComment model.photo }, Cmd.none )LoadFeed (Ok photo) ->( { model | photo = Just photo }, Cmd.none )LoadFeed (Err _) ->( model, Cmd.none )viewLikeButton : Photo -> Html MsgviewLikeButton model =let buttonClass = if model.liked then ...div [ class "like-button" ][ i [ class "fa fa-2x", class buttonClass, onClick ToggleLike ] [] ]&MN7JFX .PEFMΛඳըɻϢʔβʔૢ࡞ʹԠͯ͡ΠϕϯτΛૹΔͱŋŋŋ&MNϥϯλΠϜ͕ VQEBUFؔΛݺͿɻؔʹΠϕϯτͷछྨʹԠͨ͡Ϟσϧͷঢ়ଶભҠΛهड़͓ͯ͘͠
ঢ়ଶભҠͷؔ֎քͱΓͱΓ *0イベントコマンド
model -> model'model -> model' model -> model'ΠϕϯτΛܖػʹঢ়ଶ͕ભҠ͢Δ ŋŋŋ ࣌ܥྻʹج͍ͮͨঢ়ଶ
ঢ়ଶભҠͷؔ ϥϯλΠϜϑϨʔϜϫʔΫイベントコマンドΠϕϯτʹ͍ঢ়ଶΛભҠͤͯ͞ɺ͋ͱϑϨʔϜϫʔΫϥϯλΠϜʹͤΔ
3FEVY"QQMJDBUJPO%BUB'MPXhttps://redux.js.org/tutorials/essentials/part-1-overview-concepts
όοΫΤϯυͰಉ͡Α͏ʹʮ࣌ܥྻʹجͮ͘ঢ়ଶભҠʯͷࢹͰߟ͑ΒΕͳ͍͔• όοΫΤϯυͷੈքͷओͳʮঢ়ଶʯ ŋŋŋ υϝΠϯϞσϧͷঢ়ଶ• υϝΠϯϞσϧͷঢ়ଶΛભҠͤ͞ΔΠϕϯτ ŋŋŋ υϝΠϯΠϕϯτ
ͨͱ͑ʮ॓ധ༧ʯΛྫʹυϝΠϯϞσϧΛվΊͯߟ͑ͯΈΔ• ͲΜͳ؍ʹͯ͠ߟ͑ͯΈΔ͖͔– σʔλߏ– &3ਤ– Ϋϥεͷ࣮– ը໘• ͍ͣΕ੩తͳߏʹযΛ͍ͯͯΔɻࢹΛม͑ͯΈ͍ͨ– ಈతͳͷŋŋŋυϝΠϯΠϕϯτঢ়ଶʹযΛͯͯΈΔͱ
ʮ༧ʯͷঢ়ଶભҠʹணͯ͠ΈΔ༧ྃΧʔυܾࡁࡁΈΩϟϯηϧ॓ധࡁΈ
৽ن༧͕ྃ͢Δલ͔ΒυϝΠϯϞσϧଘࡏ͍ͯ͠Δ༧ྃΩϟϯηϧ॓ധࡁΈೖྗݕূࡁΈೖྗະݕূࡏݿ֬อࡁΈ
ঢ়ଶԿ͔͠ΒͷΠϕϯτΛܖػʹભҠ͢Δ༧ྃΩϟϯηϧ॓ധࡁΈೖྗݕূࡁΈೖྗະݕূࡏݿ֬อࡁΈ༧Λ։࢝ͨ͠ ݕূ͕ྃͨ͠ ࡏݿΛ֬อͨ͠ ༧ΛߦͬͨΩϟϯηϧ͞Εͨ॓ധͨ͠
model -> model'model -> model' model -> model'͓
྆֎෦ͱͷΠϯλϑΣʔεmodel -> modelmodel -> model&WFOU)BOEMFS8FC"QQͳΒSPVUFS%#ʹอଘ͠Ϩεϙϯε6*
model -> modelmodel -> modelevent&WFOU)BOEMFS8FC"QQͳΒSPVUFS%#ʹอଘ͠Ϩεϙϯε6*eventevent֎ͷੈք֎ͷੈք֎ͷੈքΠϕϯτ ˠϞσϧͷঢ়ଶભҠ🤔 Ͳ͔͜Ͱݟͨͳŋŋŋ
ঢ়ଶભҠͷؔ ϥϯλΠϜϑϨʔϜϫʔΫイベントコマンドಉ͡
*0ঢ়ଶભҠ *0Pure functionModel -> Model*0JOQVUMPBE*0PVUQVU
https://www.slideshare.net/ScottWlaschin/reinventing-the-transaction-script-ndc-london-2020
ϑϩϯτΤϯυͱόοΫΤϯυͷঢ়ଶཧ• ϑϩϯτΤϯυͷঢ়ଶཧ ŋŋŋ ओͳؔ৺ࣄʮΞϓϦέʔγϣϯͷঢ়ଶʯ• όοΫΤϯυͷঢ়ଶཧ ŋŋŋ ओͳؔ৺ࣄʮυϝΠϯϞσϧɺυϝΠϯΦϒδΣΫτͷঢ়ଶʯཧ͍ͯ͠Δঢ়ଶͷίϯςΩετҧ͏ͷͷঢ়ଶཧͷϞσϧࣅͨΑ͏ʹߟ͑ΒΕΔͷͰͳ͍͔
όοΫΤϯυ 5ZQF4DSJQUͰએݴతϓϩάϥϛϯά
ʮυϝΠϯΦϒδΣΫτͷঢ়ଶભҠΛએݴతʹهड़ͭͭ͠ *0͔Β͢Δʯ• ͜ͷίϯηϓτͰ࣮• &MNΞʔΩςΫνϟॻ੶ %PNBJO.PEFM.BEF'VODUJPOBMΛࢀߟʹ
͜ͷελΠϧͰΑ͘͏ͷ• type / interface• λά͖ϢχΦϯ ܕ• 3FTVMUܕ• ΧϦʔԽ• ܕͷϒϥϯυԽ ίϯύχΦϯΦϒδΣΫτ
͋·ΓΘͳ͍ͷ• class• ྫ֎ͷ throw– Error Ϋϥε͍·͢
؆୯ͳϢʔεέʔεྫ
5BHΤϯςΟςΟ ूϧʔτͷঢ়ଶભҠʹண͢Δ7BMJEBUFE6OWBMJEBUFE $SFBUFEೖྗ͕͋ͬͨ ݕূͨ͠ ࡞ͨ͠˞͜͜Ͱͷʮ$SFBUFEʯ͋͘·ͰυϝΠϯΦϒδΣΫτ͕l࡞ࡁΈzʹͳͬͨঢ়ଶͰ͋ͬͯɺσʔλϕʔεʹϨίʔυΛՃͨ͠ɺͱ͍͏ঢ়ଶͰͳ͍
5BHΫϥεΛ࡞Δexport class Tag {state: 'Unvalidated' | 'Validated' | 'Created',id: TagId | undefined,groupId: RestaurantGroupId,label: string,icon: TagIcon | undefined,sortOrder: number | undefined,builtin: boolean | undefined}
export class Tag {state: 'Unvalidated' | 'Validated' | 'Created',id: TagId | undefined,groupId: RestaurantGroupId,label: string,icon: TagIcon | undefined,sortOrder: number | undefined,builtin: boolean | undefined}ঢ়ଶભҠલʹ֬ఆ͠ͳ͍ϓϩύςΟ͕ VOEFGJOFE ʹͳͬͯ͠·͏ŋŋŋ
interface UnvalidatedTag {kind: 'Unvalidated'groupId: stringlabel: stringicon?: { symbol: string; type: TagIconType; color?: string | null | undefined } | null | undefined}interface ValidatedTag {kind: 'Validated'groupId: RestaurantGroupIdlabel: stringicon: TagIcon}export interface CreatedTag {kind: 'Created'id: TagIdgroupId: RestaurantGroupIdlabel: TagLabelicon: TagIconsortOrder: numberbuiltin: boolean}//※この型は実際には出番がないので使っていないexport type Tag = UnvalidatedTag | ValidatedTag | CreatedTagͦ͜Ͱঢ়ଶ͝ͱʹܕΛఆٛ͢Δঢ়ଶ͕ભҠ͢Δ υϝΠϯΠϕϯτ͕ൃੜ͢Δ͝ͱʹϞσϧͷ͕֬ఆ͍ͯ͘͠ͷ͕એݴͰ͖͍ͯΔ
l.BLF*MMFHBM4UBUFT6OSFQSFTFOUBCMFzinterface User {memberId: MemberId | undefinedguestId: GuestId | undefined}interface Member {userId: MemberId}interface Guest {guestId: GuestId}type User = Member | GuestऔΓಘΔͷछྨ֤ଐੑͷੵʹͳΔ ੵYɾ྆ํ VOEFGJOFEɾ྆ํͷ͕ຒ·Δͱ͍͏্༷͋Γಘͳ͍ঢ়ଶ͕ੜ·ΕΔऔΓಘΔछྨ֤ଐੑͷ ্༷͋Γಘͳ͍ঢ়ଶදݱ͠ͳ͍Ϩίʔυʮ͔ͭ "/%ʯ ϢχΦϯʮ·ͨ 03ʯ
ͪ͜ΒΑΓŋŋŋexport class Tag {state: 'Unvalidated' | 'Validated' | 'Created',id: TagId | undefined,groupId: RestaurantGroupId,label: string,icon: TagIcon | undefined,sortOrder: number | undefined,builtin: boolean | undefined}
interface UnvalidatedTag {kind: 'Unvalidated'groupId: stringlabel: stringicon?: { symbol: string; type: TagIconType; color?: string | null | undefined } | null | undefined}interface ValidatedTag {kind: 'Validated'groupId: RestaurantGroupIdlabel: stringicon: TagIcon}export interface CreatedTag {kind: 'Created'id: TagIdgroupId: RestaurantGroupIdlabel: TagLabelicon: TagIconsortOrder: numberbuiltin: boolean}export type Tag = UnvalidatedTag | ValidatedTag | CreatedTagͪ͜Βͷํ͕ɺͷΈ߹Θͤύλʔϯ͕গͳ͘ݫີ
type validateTag = (model: UnvalidatedTag) => ValidatedTagconst validateTag: validateTag = (model) => {// (省略: 値の validation ...)return {...model,kind: 'Validated',groupId: RestaurantGroupId(model.groupId),icon: model.icon ? TagIcon(model.icon) : NoIcon(),}}ঢ়ଶΛભҠͤ͞Δεςοϓ ؔ
ঢ়ଶΛભҠͤ͞Δεςοϓ ؔtype createTag = (model: ValidatedTag) => CreatedTagconst createTag: CreatedTag = (model) => {return {...model,kind: 'Created',id: generateTagId(),sortOrder: getTagSortOrder({ groupId: model.groupId }),builtin: false,}}४උ͕ͬͯॳΊ͕ͯ֬ఆ͢ΔͷΛࣗવʹهड़Ͱ͖Δͳ͓ getTagSortOrder *0͕͋ΔͨΊ %*͢Δɻޙड़
ϞσϧͷܕɺؔͷܕʹΑͬͯঢ়ଶભҠΛએݴతʹهड़͢Δ7BMJEBUFE6OWBMJEBUFE $SFBUFE(model: UnvalidatedTag) => ValidatedTag (model: ValidatedTag) => CreatedTag
ݸผʹఆٛͨ͠ঢ়ଶભҠͷؔΛܨ͍͛ͨ• Ͱɺܭࢉʮ్தͰࣦഊ͢ΔʯՄೳੑ͕͋Δ– ͨͱ͑υϝΠϯϞσϧͷࣄલ݅Λຬͨ͞ͳ͍Τϥʔ– 7BMJEBUJPO&SSPSŋŋŋ– .BY5BH-JNJU&YDFFEFEʜ• ʮ్தͰࣦഊ͢Δʯ͜ͱΛܕͰએݴͰ͖ͳ͍͔ ˠ 3FTVMUܕ
3FTVMUܕͰࣦഊͷՄೳੑͷ͋ΔܭࢉΛҰຊಓʹ߹͢Δimport { Result, ok, err } from 'neverthrow'function itsUnder100(n: number): Result {return n <= 100 ? ok(n) : err(new Error('100より大きい数字です'))}function itsEven(n: number): Result {return n % 2 == 0 ? ok(n) : err(new Error('奇数です'))}function itsPositive(n: number): Result {return n > 0 ? ok(n) : err(new Error('負数です'))}const result = ok(96).andThen(itsUnder100).andThen(itsEven).andThen(itsPositive)result.match((n) => console.log(n),(error) => { throw error })
3FTVMUܕͰঢ়ଶભҠؔΛͭͳ͛ͯɺҰͭͷʮϫʔΫϑϩʔʯΛ࡞Δ7BMJEBUFE5BH$SFBUFE5BH6OWBMJEBUFE5BH7BMJEBUFE5BH8PSL'MPX
type validateTag = (model: UnvalidatedTag) => Resultconst validateTag: validateTag = (model) => {const groupId = RestaurantGroupId(model.groupId)const label = TagLabel(model.label)const icon = model.icon ? TagIcon(model.icon) : ok(NoIcon())const values = Result.combine(tuple(groupId, label, icon))return values.map(([groupId, label, icon]) => ({...model,kind: 'Validated',groupId,label,icon,}))}ঢ়ଶભҠͷޭ ࣦഊΛ 3FTVMUܕͰฦ͢Α͏ʹ͢Δͷੜʹࣦഊ͢ΔՄೳੑ͕͋ΔͷͰɺ͜ΕΒ 3FTVMUܕΛฦ͢
3FTVMUܕͰঢ়ଶભҠؔΛܨ͛ͯɺϫʔΫϑϩʔ υϝΠϯϩδοΫΛ࡞Δtype WorkFlow = (model: UnvalidatedTag) => Resultexport const createTagWorkFlow: WorkFlow = (model) =>ok(model).andThen(validateTag).andThen(createTag)
ϫʔΫϑϩʔͷ࢝·ΓͱऴΘΓ͕ɺ֎քͱͷ JOPVU7BMJEBUFE5BH$SFBUFE5BH6OWBMJEBUFE5BH7BMJEBUFE5BH8PSL'MPXೖྗͷ%50ྫ(SBQI2-*OQVU5ZQFΛ6OWBMJEBUFE5BHʹมUBH3FQPTJUPSZͰ$SFBUFE5BHΛอଘ
(SBQI2-SFTPMWFS3FQPTJUPSZ %#ͱϫʔΫϑϩʔΛଓ͢Δimport { saveCreatedTag } from '../../../customers/repos/tagRepository'export const createTagMutation = mutationField('createTag', {...resolve(_root, { input }, context) {const workflow = createTagWorkFlow()// GraphQL 入力をワークフローの入力に変換const unvalidatedTag = toUnvalidatedTag({...input,groupId: context.operator.groupId,})// ワークフローを実行し Repository パターンの関数で DB に保存 (ここも Result で繋ぐ)const result = ok(unvalidatedTag).andThen(workflow).andThen(saveCreatedTag(context))return result.match((tag) => ({tag: {...tag,id: toGlobalId('Tag', tag.id),},}),(error) => {// ここで初めて例外をスロー (単に Nexus にエラーを伝える手段としてスローする)throw error})},})
ok(model).andThen(workflow).andThen(saveCreatedTag(context))(SBQI2-*OQVUσʔλϕʔεPure functionModel -> Model*0JOQVUMPBE*0PVUQVU
ঢ়ଶભҠͷؔ ϥϯλΠϜϑϨʔϜϫʔΫイベントコマンド
σʔλϑϩʔϓϩάϥϛϯά• 3FTVMUܕͰࣦഊͷ͋ΔܭࢉΛ߹͠ɺσʔλͷ௨Γಓͱͯ͠ͷܭࢉաఔΛ࡞Δ– ͦ͜ʹσʔλΛ์ΓࠐΉͱɺͦͷதΛ௨ͬͯঢ়ଶભҠͨ͠σʔλ͕ಘΒΕΔ– σʔλΛσʔλͷ··ɺͦͷՄൖੑΛԼ͛ͣʹѻ͍͍ͨɻ݁Ռ class ͷొػձ͕ͳ͍• ܭࢉΛҰຊಓʹ͢Δ– େҬग़͠ͳ͍ɻେҬग़͢Δͱܭࢉ͕ҰຊಓʹͳΒͳ͍ ˠྫ֎ΛΘͳ͍– ࣦഊͷذ 3FTVMUͰ߹ ˞3FTVMUܕϞφυ– ܭࢉ͕ҰຊಓʹͳΔ σʔλෆมɻೝෛՙ͕͘ͳΔ
υϝΠϯϞσϧͷอଘैདྷ௨Γ 3FQPTJUPSZexport const saveCraetedTag =({ prisma }: applicationContext) =>(model: CreatedTag): ResultAsync => {const { kind: _, ...tag } = modelconst icon = toIconData(tag.icon)return ResultAsync.fromPromise(prisma.tag.create({data: {...tag,...icon,},}),PrismaClientError)}3FTVMU"TZODGSPN1SPNJTFΛ͏͜ͱͰ 1SPNJTFΛ 3FTVMUʹแΈɺଞͷ3FTVMUܕͱ߹Ͱ͖Δྫ֎෧͡ࠐΊΔ͜ͱ͕Ͱ͖Δ
3FQPTJUPSZύλʔϯʹ͍ͭͯ• %PNBJO.PEFM.BEF'VODUJPOBMͰʮ͜ͷख๏ͳΒ 3FQPTJUPSZඞཁͳ͍ʯͱ͍͏هड़͕͋Δ– ᐌ͘ 3FQPTJUPSZ .VUBCMFͳυϝΠϯϞσϧʹجͮ͘ͷ͔ͩΒɺͱͷ͜ͱ– ղઆ͕͍͜ͱ͋Γจҙ͕Α͘௫Ίͳ͔ͬͨŋŋŋ• ैདྷ௨Γ 3FQPTJUPSZΛར༻– ͨͩ͠3FQPTJUPSZΫϥεͷΦϒδΣΫτͰͳ͘ɺؔ– ूηΦϦʔ௨Γʹߏங͍ͯ͠Δɻ͑ͯݸผͷߋ৽༻ؔʹׂ͢Δඞཁͳ͍ͱߟ͑ͨ
XPSLGMPXʹॾʑॻ͍͍ͯ͘ͱɺτϥϯβΫγϣϯεΫϦϓτʹͳΔ• %PNBJO.PEFMJOH.BEF'VODUJPOBMτϥϯβΫγϣϯεΫϦϓτ• ŋŋŋ͕ɺڽूੑʹ՝͕ग़ͦ͏ͳͷͰɺ߲࣍ͷͱ͓ΓϞδϡʔϧԽͨ͠
ΤϯςΟςΟͷܕΛجʹͨ͠ϞδϡʔϧΛ࡞ΓɺίΞυϝΠϯϩδοΫؔͦ͜ʹूΊΔ// customers/objects/tag.tsexport interface Tag {id: TagIdgroupId: RestaurantGroupIdlabel: TagLabelicon: TagIconsortOrder: numberbuiltin: boolean}export const updateLabel = (label: TagLabel) => (tag: Tag) => ({ ...tag, label })export const updateIcon = (icon: TagIcon) => (tag: Tag) => ({ ...tag, icon })
τϥϯβΫγϣϯεΫϦϓτͰͳ͘ͳΔ• ίΞυϝΠϯΦϒδΣΫτͷपΓʹɺίΞυϝΠϯϩδοΫ͕ؔू·Δ• XPSLGMPXͦͷίΞυϝΠϯϩδοΫؔΛݺͼग़ͯ͠ɺۀϑϩʔΛΈཱͯΔׂʹ– ΦχΦϯΞʔΩςΫνϟͳͲͷ 6TF$BTFͱಉׂ͡ΞϓϦέʔγϣϯΞʔΩςΫνϟͷશମ૾ɺैདྷͷΞʔΩςΫνϟͱͦΕ΄Ͳେ͖͘มΘ͍ͬͯͳ͍
έʔεͦͷطଘͷΤϯςΟςΟͷߋ৽• ͖͞΄Ͳ·ͰͷΤϯςΟςΟͷ৽ن࡞• طଘͷΤϯςΟςΟΛߋ৽͢ΔΑ͏ͳϢʔεέʔεͷ߹ŋŋŋ– σʔλϕʔε͔ΒΤϯςΟςΟΛ෮ݩ͢Δ– ͦͷΤϯςΟςΟͱผʹɺೖྗ͕͋Δ
7BMJEBUFE*OQVU6OWBMJEBUFE*OQVU6QEBUFE5BH5BH(SBQI2-%#3FQPTJUPSZͳΜ͔ͩͪ͝Όͭ͘ŋŋŋ
(SBQI2-%#3FQPTJUPSZ*OQVU5BH6OWBMJEBUFE$PNNBOE7BMJEBUFE$PNNBOEŋŋŋ 6QEBUFE5BH8PSL'MPXHFU5BH#Z*E ೖྗͱυϝΠϯΦϒδΣΫτΛͻͱͭʹ·ͱΊͨʮίϚϯυʯΛϫʔΫϑϩʔͷೖྗʹ͢Δͦͷ্Ͱɺઌ΄Ͳಉ༷ɺ୯ํσʔλϑϩʔͷதͰঢ়ଶભҠͤͯ͞తͷग़ྗʹ͚͍ۙͮͯ͘
// customers/workflows/updateTag.tsinterface UnvalidatedInput {kind: 'Unvalidated'label: stringicon?: { symbol: string; type: TagIconType; color?: string | null | undefined } | null | undefined}interface UnvalidatedCommand {input: UnvalidatedInputtag: Tag}interface ValidatedInput {kind: 'Validated'label: TagLabelicon: TagIcon}interface ValidatedCommand {input: ValidatedInputtag: Tag}// Outputtype UpdatedTag = Tag & { kind: 'Updated' }
// customers/workflows/updateTag.ts// substep1: validateCommandtype validateCommand = (command: UnvalidatedCommand) => Result// substep2: updateTagtype updateTag = (command: ValidatedCommand) => Result// workflow: validateCommand -> updateTagtype WorkFlow = (command: UnvalidatedCommand) => Resultexport const updateTagWorkFlow = (): WorkFlow => (command) =>ok(command).andThen(validateCommand).andThen(updateTag)
(SBQI2-%#3FQPTJUPSZ*OQVU5BH6OWBMJEBUFE$PNNBOE7BMJEBUFE$PNNBOEŋŋŋ 6QEBUFE5BH8PSL'MPXHFU5BH#Z*E
// customers/repos/tagRepository.tsexport const findTagById =({ prisma }: applicationContext) =>(id: TagId): ResultAsync =>ResultAsync.fromPromise(prisma.tag.findUnique({ where: { id } }),PrismaClientError).andThen((tag) => (tag ? Tag(tag) : ok(null)))export const getTagById =(context: applicationContext) =>(id: TagId): ResultAsync =>findTagById(context)(id).andThen((tag) =>tag ? ok(tag) : err(new EntityNotFound(`タグがみつかりません: ${id}`)))
// graphql/mutation/updateTag.tsexport const updateTagMutation = mutationField('updateTag', {type: UpdateTagPayload,args: {tagId: idArg({ description: 'タグID' }),input: TagInput,},resolve(_root, { tagId, input }, context) {const workflow = updateTagWorkFlow()const preprocess = TagId(tagId).asyncAndThen(getTagById(context)).map((tag) => toUnvalidatedCommand({ input, tag }))const result = preprocess.andThen(workflow).andThen(saveTag(context))return result.match( ... )},})
ΑΓෳࡶͳϑϩʔ• جຊతʹߏಉ͡ͰɺߏࣗମෳࡶʹͳΒͳ͍• ঢ়ଶભҠͷαϒεςοϓ͕૿͍͑ͯ͘
έʔεͦͷυϝΠϯϩδοΫͷ్தͰ *0͕ൃੜ͢Δ߹• ඞͣ͠υϝΠϯϩδοΫ࣮ߦલʹಡΈࠐΈΛࡁ·ͤΒΕΔέʔε͔ΓͰͳ͍• ͨͱ͑ŋŋŋ– ࡞ͨ͠ΤϯςΟςΟͷฒͼҐஔΛɺσʔλϕʔεʹ͍߹Θܾͤͯఆ͢Δ– υϝΠϯϞσϧͷߋ৽ʹɺԿΒ͔֎෦ͷ 8FC "1*Λίʔϧͯ͠औಘͨ͠Λར༻͢Δ– ŋŋŋ
%FQFOEFODZ*OKFDUJPOΛ͏• *0ͦͷͷΛۀॲཧͷ్த͔ΒऔΓআ͚ͳ͍ͳΒɺ%*Ͱ XPSLGMPX͕ *0 ʹ·ͭΘΔܕ ྫ σʔλϕʔεଓΦϒδΣΫτ ʹґଘ͠ͳ͍Α͏ʹ͢Δ– XPSLGMPX *0ʹґଘ͠ͳ͍ؔͰ͋Δ͜ͱΛҡ࣋͢Δ• %*ΧϦʔԽʹΑΔ෦ద༻Ͱ࣮ݱ͢Δ– %PNBJO.PEFM .BEF'VODUJPOBMͰఏҊ͞Ε͍ͯͨख๏
// customer/services/tag.tsexport type getTagSortOrder = ({ groupId }: { groupId: RestaurantGroupId })=> ResultAsyncexport const getTagSortOrder =({ prisma }: applicationContext): getTagSortOrder =>({ groupId }) =>ResultAsync.fromPromise(prisma.tag.aggregate({_max: {sortOrder: true,},where: { groupId },}).then((x) => (x._max.sortOrder ? x._max.sortOrder + 1 : 1)),PrismaClientError)ΧϦʔԽʹΑΓɺ෦ద༻Ͱ͖ΔΑ͏ʹ͢Δ
// graphql/mutation/createTag.tsexport const createTagMutation = mutationField('createTag', {type: CreateTagPayload,args: {input: TagInput,},resolve(_root, { input }, context) {const workflow = createTagWorkFlow(checkTagExists(context), getTagSortOrder(context)) // DIconst unvalidatedTag = toUnvalidatedTag({...input,groupId: context.operator.groupId,})const result = workflow(unvalidatedTag).andThen(saveCreatedTag(context))return result.match(...)},})ϫʔΫϑϩʔʹίϯςΩετ͕෦ద༻͞ΕͨؔΛ͢
// customer/workflows/createTag.tstype createTag = (model: ValidatedTag) => ResultAsyncconst createTag =(getTagSortOrder: getTagSortOrder): createTag =>(model) => {const values = getTagSortOrder({ groupId: model.groupId })return values.map((n) => ({...model,kind: 'Created',id: generateTagId(),sortOrder: n,builtin: false,}))}// workflow: validateTag -> createTagtype WorkFlow = (model: UnvalidatedTag) => ResultAsyncexport const createTagWorkFlow =(checkTagExists: checkTagExists, // dependencygetTagSortOrder: getTagSortOrder // dependency): WorkFlow =>(model) =>okAsync(model).andThen(validateTag(checkTagExists)).andThen(createTag(getTagSortOrder))ϫʔΫϑϩʔ͔ΒσʔλϕʔείϯςΩετ͕ཁΒͳ͍७ਮؔʹݟ͑Δɻͭ·Γɺςετσόοά࣌ɺ७ਮؔʹࠩ͑͠Δ͜ͱͰ͖Δ%*͞ΕͨؔΛೖͭͭ͠ɺσʔλϑϩʔ͜Ε·Ͱ௨ΓͷߏHFU5BH4PSU0SEFS 3FTVMUΛฦ͢ͷͰɺσʔλϑϩʔͷதͰ݁Ռ߹͞ΕΔ
%*Ͱղܾ͢ΔΑ͏ͳ *0Ͱͳ͍߹ϫʔΫϑϩʔͷαϯυΠονʹ͢Δͱྑ͍ɺͱͷ͜ͱhttps://www.slideshare.net/ScottWlaschin/reinventing-the-transaction-script-ndc-london-2020
ߟ
ϑϩϯτΤϯυͱͷൺֱ• ࣌ܥྻʹجͮ͘ঢ়ଶભҠ σʔλϑϩʔΛએݴతʹهड़͢Δɺͱ͍͏ߟ͑ํಉ͡ʹͳͬͨ– ܕͱখ͞ͳؔͷએݴతͳهड़ͰɺϑϩʔΛΈ্͛Δ• ҰํɺϫʔΫϑϩʔͷ࣮Λ͍ͯ͠Δͱ͖ͷײ֮ʹ·ͩڑ͕͋Δ– υϝΠϯΠϕϯτͰঢ়ଶભҠɺͱ͍ͬͯଟ͘ͷ߹ʮ7BMJEBUFͯ͠ɺೖྗͰυϝΠϯϞσϧΛߋ৽͢Δʯ͚ͩ• ݁ՌɺϫʔΫϑϩʔఆܕతͳهड़͕ଟ͘ͳΔ ŋŋŋ ϑϨʔϜϫʔΫԽͰ͖Δ͔• ϑϩϯτΤϯυͦ͜Λ 3FBDU &MNͳͲͷϑϨʔϜϫʔΫ͕͍ͬͯΔ ͔ͩΒɺΠϕϯτʹର͢ΔϞσϧͷঢ়ଶભҠͱɺͦͷঢ়ଶΛදݱ͢ΔϓϨθϯςʔγϣϯͷهड़ʹूதͰ͖Δ• ΠϕϯτͱΠϕϯτͷͭͳ͗߹Θͤ ྫ3FTVMUܕʹΑΔ߹ΛࣗͰهड़͍ͯ͠Δͷ͕ݱঢ়
ैདྷͷΞʔΩςΫνϟͱͷࠩҟʹ͍ͭͯ• ్தͰ৮Εͨͱ͓ΓɺେͷΞʔΩςΫνϟ͋·ΓมΘ͍ͬͯͳ͍– 6TF$BTF૬ͷ XPSLGMPX– 3FQPTJUPSZ– ίΞυϝΠϯϞσϧ– ूɺΤϯςΟςΟ• ίϯϙʔωϯτͷதͷ࣮ͷύϥμΠϜ͕ҟͳΔ– ܕͰۀͷঢ়ଶɺϑϩʔΛએݴ͢Δ– σʔλϑϩʔϓϩάϥϛϯάʹΑΔɺ୯ํߴσʔλϑϩʔ
ैདྷख๏ʹൺֱ͠هड़ྔগͳ͍• σʔλΛσʔλͷ··ӡΜͰ͍ΔͷͰʮ٧Ίସׂ͑ͯͷҟͳΔผछͷΦϒδΣΫτʯʹ͢Δŋŋŋͱ͍͏ඞཁ͕ͳ͍– ͷίϐʔ࣮ࡍʹͱ͜ΖͲ͜Ζ͍ͬͯΔ͕ ͨͩͷσʔλΛׂೖͰهड़Ͱ͖ΔͷͰɺهड़ྔ࠷খ
ݱ࣌Ͱͷײ• ্༷ͳ͍ঢ়ଶΛ࡞ΒͣʹࡁΉͨΊɺݎ࿚• ΑΓෳࡶͳϫʔΫϑϩʔΛ࣮ͨ͠߹ಉ͡ߏʹऩ·Δɻೝෛՙ͕͍• 3FTVMUܕʹΑΓܭࢉΛܨ͛ΒΕΔΑ͏هड़͢Δྑ͍ڧ੍ྗ͕ಇ͘– ͨͩ͠ andThen().andThen().asyncAndThen().map() ͕͢͞ʹಡΈͮΒ͍– 3FTVMU͕ೖΕࢠʹͳͬͯ͘Δͱɺ3FTVMUܕύζϧʹΉ࣌ŋŋŋ• )BTLFMMͷ EPه๏ɺ'ͷίϯϐϡςʔγϣϯࣜʹ૬͢Δͷ͕ཉ͍͠ŋŋŋ• ͷ٧Ίସ͑ͷهड़͕ͳ͍ͷ ͱͯخ͍͠
;Γ͔͑Γhttps://zenn.dev/mizchi/articles/oop-think-modern
·ͱΊ• ࣌ܥྻʹجͮ͘ঢ়ଶભҠͷએݴͱϑϨʔϜϫʔΫଆʹΑΔঢ়ଶભҠɺௐఀ– ϑϩϯτΤϯυɺͦͷͨΊͷϑϨʔϜϫʔΫͷ࣮͕ॆ࣮͍ͯ͠Δ– ؔܕ͔ΒӨڹΛड͚ͨྑ͍࡞๏• ʮએݴతϓϩάϥϛϯάʯͷϓϥΫςΟεɺݱ࣌Ͱܦݧతʹྑ͍ͷ– όοΫΤϯυ։ൃ͜ͷߟ͑ํʹऩᏑ͍ͤͯ͘͞ͷѱ͘ͳ͍ͷͰ ˠͬͯΈͨΒײ৮– ྲྀߦΔ͔Ͳ͏͔Θ͔Γ·ͤΜ• ϑϩϯτΤϯυ όοΫΤϯυͷύϥμΠϜΪϟοϓΛগͳ͍͖͍ͯͨ͘͠
ัଊ• 5ZQF4DSJQUʹΈࠐΈͷ 3FTVMUܕͳ͍ͷͰɺOFWFSUISPXΛͬͨɻଞʹ GQUTͳͲͷީิ͕͋Δ• 3FTVMUܕ XPSLGMPXͷߏ͚ͩͰͳ͋͘ΒΏΔॴͰ͏• 1SPNJTF 3FTVMU"TZODʹΑͬͯ 3FTVMUԽͯ͠߹Ͱ͖Δ• UZQFͱ JOUFSGBDFͷ͍͚ ŋŋŋ GQUTʹ฿͍ͬͯΔɻಛʹͦ͏͠ͳ͚ΕͳΒ͍ཧ༝ͳ͍ͱࢥ͏• ͕ؔσϑΥϧτͰΧϦʔԽ͞Εͳ͍ͷํ͕ͳ͍ɻ࣌ંΧϦʔԽͨ͠ͷΛΕͯ·Δ• SFBEPOMZԣணͯ͠ɺͬͯͳ͍ɻ ͪΌΜͱݕ౼͍ͯ͠ͳ͍
͓·͚ ŋŋŋ OFXUZQFશίϯετϥΫλdeclare const __newtype: unique symbolexport type newtype = Type & {readonly [__newtype]: Constructor}export type TagId = newtype<'TagId', string>export function TagId(value: string): Result {return validate(value)? ok(value as TagId): err(new ValidationError('IDの形式が不正です'))}υϝΠϯϞσϧͷߏʹ/PNJOBMͳܕ͕ཉ͍͠ͱ͖ɺ͜͏͍͏࣮Ͱͬͯ·͢
ࢀߟจݙ• 4DPUU8MBTDIJO ʮ%PNBJO.PEFMJOH.BEF'VODUJPOBMᴷ5BDLMF4PGUXBSF$PNQMFYJUZXJUI%PNBJO%SJWFO%FTJHOBOE'ʯ • +FSFNZ'BJSCBOL ஶ ϠΪͷ͘͞ΒͪΌΜ ༁ ʮϓϩάϥϛϯά&MNᴷ҆શͰϝϯςφϯε͍͢͠ϑϩϯτΤϯυΞϓϦέʔγϣϯ։ൃೖʯ • ླ ྅ଠ ʮϓϩΛࢦ͢ਓͷͨΊͷ5ZQF4DSJQUೖ ᴷ ҆શͳίʔυͷॻ͖ํ͔Βߴͳܕͷ͍ํ·Ͱʯ • #PSJT$IFSOZ ஶ ࠓଜ ݠ࢜ म ݪ ོจ ༁ ʮϓϩάϥϛϯά5ZQF4DSJQUʕεέʔϧ͢Δ+BWB4DSJQUΞϓϦέʔγϣϯ։ൃʯ • ੵܕͱܕʹ͍ͭͯ– ʮू߹ͱͯ͠ͷܕ u"O*OUSPEVDUJPOUP&MNʯ IUUQTHVJEFFMNMBOHKQBQQFOEJYUZQFT@BT@TFUTIUNM– ʮͳͥ࣍ʹֶͿݴޠؔܕͰ͋Δ͖͔ʯ IUUQTZNPUPOHQPPIBUFOBCMPHDPNFOUSZ