Slide 1

Slide 1 text

1 運⽤していくアプリケーション開発のヒント 2022/05/20 NestJS Meetup Online #2 Copyright © 2019 FOLIO Co., Ltd. All Rights Reserved.

Slide 2

Slide 2 text

2 ⾃⼰紹介 kenchan0130 Tadayuki Onishi Software Engineer @FOLIO 🔑 Tech keywords TypeScript Scala Ruby SRE AWS GCP Corporate IT https://kenchan0130.github.io 📄 Blog

Slide 3

Slide 3 text

Agenda 3 1.アプリケーション開発・運⽤の現場では何が起 きるのか 2.クリーンアーキテクチャと正当性・堅牢性 3.クリーンアーキテクチャの実現 4.正当性・堅牢性の実現

Slide 4

Slide 4 text

Agenda 4 1.アプリケーション開発・運⽤の現場では何が起 こるのか 2.クリーンアーキテクチャと正当性・堅牢性 3.クリーンアーキテクチャの実現 4.正当性・堅牢性の実現

Slide 5

Slide 5 text

アプリケーション開発・運⽤の 現場では何が起こるのか 5

Slide 6

Slide 6 text

6 アプリケーション開発は変化の連続 実際にアプリケーションが使われてからじゃないと、 わからないことがたくさんある 変更・変化に対して、より速く対応して継続的にアプリケーション をデリバリーしていくことがプロダクトの競争⼒となる

Slide 7

Slide 7 text

質を下げて速度を上げる 7 デリバリー速度をあげるアンチパターン

Slide 8

Slide 8 text

ʮ͋ͱͰΫϦʔϯʹ͢Ε͹͍͍Αɻઌʹࢢ৔ʹग़͞ͳ͚Ε͹ʂʯ ։ൃऀ͸ͦ͏΍͍ͬͯͭ΋͝·͔͢ɻ͕ͩɺ͋ͱͰΫϦʔϯʹ͢Δ͜ͱ͸ͳ ͍ɻࢢ৔͔ΒͷϓϨογϟʔ͸ࢭ·Βͳ͍͔Βͩɻʮઌʹࢢ৔ʹग़͞ͳ͚ Ε͹ʯͱ͍͏͜ͱ͸ɺޙΖʹڝ߹ଞ͕ࣾେ੎͍Δͱ͍͏͜ͱͰ͋Δɻڝ߹ ଞࣾʹ௥͍ൈ͔Εͳ͍ͨΊʹ͸ɺ͜Ε͔Β΋૸Γଓ͚Δ͔͠ͳ͍ɻ ͦͷ݁Ռɺ։ൃऀ͸ϞʔυΛ੾Γସ͑Δ͜ͱ͕Ͱ͖ͳ͍ɻ࣍ͷػೳɺ·ͨ ࣍ͷػೳɺ·ͨ·ͨ࣍ͷػೳΛ௥Ճ͢Δ͜ͱʹͳΓɺίʔυΛΫϦʔϯʹ ͢Δ͜ͱ·Ͱख͕ճΒͳ͍ɻ ͦͯ͠ɺ่յ͕࢝·Δɻੜ࢈ੑ͕θϩʹ͍͍ۙͮͯ͘ɻ ʰ$MFBO"SDIJUFDUVSFʱQ 8 質を下げてもデリバリー速度は上がらない ࣭ͱεϐʔυʢ2022य़൛ɺ࣭ٙԠ౴༻ࢿྉ෇͖ʣ / Quality and Speed 2022 Spring Edition https://speakerdeck.com/twada/quality-and-speed-2022-spring-edition

Slide 9

Slide 9 text

IUUQTPTBIBUFOBCMPHDPNFOUSZ Ͱ͸ɺ඼࣭ͱ଎౓ʹ͍ͭͯͷτϨʔυΦϑ͕ҙࣝ͞ΕΔ ͱ͖ɺ࣮ࡍʹ͸ԿͱԿ͕ṝʹ͔͚ΒΕ͍ͯΔͷ͔ɻ ͦΕ͸֤ݸਓͰ͸ͳ͘ϓϩμΫτશମͷ඼࣭ͱ଎౓͕ṝ ʹ͔͚ΒΕ͍ͯΔͷͰ͸ͳ͍͔ɻݴ͍׵͑Ε͹ɺϓϩμ Ϋτͷ඼࣭Λࢧ͑ΔͨΊʹඞཁͳϝϯόʔͷ੒௕ͱͦͷ ੒௕ͷͨΊʹඞཁͳϑΟʔυόοΫ΍ֶशͷ͕࣌ؒṝʹ ͔͚ΒΕ͍ͯΔͷͰ͸ͳ͍͔ͱࢥ͏ɻ ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ w ʮ඼࣭ͱεϐʔυ͸τϨʔυΦϑͷؔ܎ʹ͋Δʯ͸େ͖ͳޡղ w ʮ඼࣭ʯͷ໊ͷ΋ͱʹ٘ਜ਼ʹ͞ΕΔͷ͸಺෦඼࣭ͷಛʹอकੑ ʢςετ༰қੑɺཧղ༰қੑɺมߋ༰қੑʣ w ࣮ࡍʹ͸อकੑΛߴΊΕ͹εϐʔυ͸্͕Δ͠ɺอकੑΛམͱͤ͹ εϐʔυ͸Լ͕Δ w εϐʔυΛམͱͯ͠΋อकੑ͸্͕Βͳ͍͠ɺεϐʔυΛམͱ͢ͱԾઆݕ ূϓϩηε͕ճΒͳ͍ w ಺෦඼࣭΁ͷ౤ࢿͷଛӹ෼ذ఺͸ҙ֎ͱૣ͘ ϲ݄Ҏ಺ ΍ͬͯ͘Δ w ϲ݄Ҏ಺ͱ͍͏͜ͱ͸डӹऀ͸ࣗ෼ͨͪࣗ਎Ͱ͋Γɺͭ·Γಓಙ΍ᛗዟ ͷ࿩Ͱ͸ͳ͘ଛಘͷ࿩Ͱ͋Δ w εϐʔυ͓Αͼ࣭ͱτϨʔυΦϑͳͷ͸ڭҭɺ੒௕ɺଟ༷ੑ΁ͷ౤ࢿ 9 質およびスピードは何とトレードオフなのか ࣭ͱεϐʔυʢ2022य़൛ɺ࣭ٙԠ౴༻ࢿྉ෇͖ʣ / Quality and Speed 2022 Spring Edition https://speakerdeck.com/twada/quality-and-speed-2022-spring-edition

Slide 10

Slide 10 text

仮説検証を繰り返して学びを得るしくみと、 組織的に技術⼒を⾼めるしくみを作ること 10 より良いプロダクトを作るには

Slide 11

Slide 11 text

シンプル、つまり変更しやすい設計で備える 11 アプリケーション開発の⽂脈では

Slide 12

Slide 12 text

学んだ設計⼿法を使って、 ⽬の前にある複雑な課題に応⽤できないかを 常に考えて設計する 12 アプリケーション開発の⽂脈では

Slide 13

Slide 13 text

Agenda 13 1.アプリケーション開発・運⽤の現場では何が起 こるのか 2.クリーンアーキテクチャと正当性・堅牢性 3.クリーンアーキテクチャの実現 4.正当性・堅牢性の実現

Slide 14

Slide 14 text

クリーンアーキテクチャと 正当性・堅牢性 14

Slide 15

Slide 15 text

ਖ਼౰ੑ ݎ࿚ੑ ৗʹਖ਼֬͞ɺਖ਼͠͞ΛॏΜ͡Δ ֎ք͔Βͷ༷ʑͳೖྗ΍ग़ྗΛɺίϯςΫετ ʹԠͯ͡ద੾ʹϋϯυϦϯά͠ɺม׵͢Δ • 円の外側(外界に近いレイヤー)ではより堅牢性、 円の内側(アプリケーションのコアロジックに近い レイヤー)ではより正当性を表現できる • 関⼼事が分離できる • テストおよびリファクタリングがしやすくなる • 変更しやすいアプリケーションに近づく 15 - https://speakerdeck.com/twada/growing-reliable-code-phperkaigi-2022 クリーンアーキテクチャと堅牢性・正当性 ੺ͱ੨ͷք໘ɺ૚ͷ๷ޚϥΠϯ͕͋Δ

Slide 16

Slide 16 text

❌ クリーンアーキテクチャの図が意味するところは、 図の通りのレイヤーにすること ⭕ 常に内側にしか依存しないようにする、つまり依存 の⽅向を⼀⽅向にすること 16 クリーンアーキテクチャの図の意味

Slide 17

Slide 17 text

Agenda 17 1.アプリケーション開発・運⽤の現場では何が起 こるのか 2.クリーンアーキテクチャと正当性・堅牢性 3.クリーンアーキテクチャの実現 4.正当性・堅牢性の実現

Slide 18

Slide 18 text

クリーンアーキテクチャの実現 18

Slide 19

Slide 19 text

19 依存の⽅向を考えずに愚直に構成

Slide 20

Slide 20 text

• Application層が、本来外側に位置する Infrastructure層に依存している • 依存性逆転の原則に違反している 20 依存の⽅向を考えずに愚直に構成

Slide 21

Slide 21 text

1. 上位のモジュールは下位のモジュールに依存しては ならない。どちらのモジュールも「抽象」に依存す べきである。 2. 「抽象」は実装の詳細に依存してはならない。実装 の詳細が「抽象」に依存すべきである。 21 依存性逆転の原則

Slide 22

Slide 22 text

• Application層が外側のInfrastructure層に依存しな くなった • 業務ロジックが技術選択の関⼼事に依存しにくくな り、正当性が⾼いアプリケーションになる • 関⼼事が分離できた 22 抽象に依存するように変更

Slide 23

Slide 23 text

NestJSのDIコンテナを使って実装してみる

Slide 24

Slide 24 text

抽象を定義 24 // application/repository/PostRepository.ts export type CreatePostProps = Readonly<{ postId: string; title: string; contents: Readonly<{ order: number; type: number; body: string; }>[]; }>; export abstract class PostRepository { abstract save(props: CreatePostProps): Promise; }

Slide 25

Slide 25 text

抽象に関する詳細な実装 25 // infrastructure/repository/PostRepository.ts @Injectable() export class PostRepositoryImpl implements PostRepository { constructor(private readonly prismaClient: MyPrismaClient) {} async save(props: CreatePostProps): Promise { await this.prismaClient.post.create({ data: { postId: props.postId, title: props.title, contents: { create: props.contents.map((v) => ({ contentOrder: v.order, contentType: v.type, body: v.body, })), }, }, }); } }

Slide 26

Slide 26 text

DIで依存を逆転 26 // infrastructure/repository/Repository.module.ts const providers = [ { provide: PostRepository, useClass: PostRepositoryImpl, }, ]; @Module({ imports: [PrismaModule], providers, exports: providers.map((v) => v.provide), }) export class RepositoryModule {}

Slide 27

Slide 27 text

DIで依存を逆転 27 // infrastructure/repository/Repository.module.ts const providers = [ { provide: PostRepository, useClass: PostRepositoryImpl, }, ]; @Module({ imports: [PrismaModule], providers, exports: providers.map((v) => v.provide), }) export class RepositoryModule {}

Slide 28

Slide 28 text

UseCaseでは抽象を使⽤ 28 // application/usecase/CreatePost.usecase.ts @Injectable() export class CreatePostUseCase { constructor(private readonly postRepository: PostRepository) {} async execute( dto: CreatePostUseCaseInputDto ): Promise { const postId = ulid(); await this.postRepository.save({ postId, title: dto.title, contents: dto.contents, }); return { postId }; } }

Slide 29

Slide 29 text

UseCaseでは抽象を使⽤ 29 // application/usecase/CreatePost.usecase.ts @Injectable() export class CreatePostUseCase { constructor(private readonly postRepository: PostRepository) {} async execute( dto: CreatePostUseCaseInputDto ): Promise { const postId = ulid(); await this.postRepository.save({ postId, title: dto.title, contents: dto.contents, }); return { postId }; } } UseCase

Slide 30

Slide 30 text

UseCaseの依存をInfrastructure層で設定 30 // infrastructure/ioc/usecase/CreatePostUseCase.module.ts @Module({ imports: [RepositoryModule], providers: [CreatePostUseCase], exports: [CreatePostUseCase], }) export class CreatePostUseCaseModule {}

Slide 31

Slide 31 text

常に内側に依存するように、 依存の⽅向を⼀⽅向にできた

Slide 32

Slide 32 text

そういえば、抽象をinterfaceじゃなくて abstract classで定義していたけど...

Slide 33

Slide 33 text

interfaceはDIのトークンにできない 33 // application/repository/PostRepository.ts export interface PostRepository { createPost(props: CreatePostProps): Promise; } // infrastructure/repository/Repository.module.ts @Module({ imports: [PrismaModule], providers: [ { provide: "PostRepositoryToken", useClass: PostRepositoryImpl, }, ], exports: ["PostRepositoryToken"], }) export class RepositoryModule {}

Slide 34

Slide 34 text

interfaceはDIのトークンにできない 34 // application/repository/PostRepository.ts export interface PostRepository { createPost(props: CreatePostProps): Promise; } // infrastructure/repository/Repository.module.ts @Module({ imports: [PrismaModule], providers: [ { provide: "PostRepositoryToken", useClass: PostRepositoryImpl, }, ], exports: ["PostRepositoryToken"], }) export class RepositoryModule {} interface interface

Slide 35

Slide 35 text

35 // application/usecase/CreatePost.usecase.ts @Injectable() export class CreatePostUseCase { constructor(@Inject("PostRepositoryToken") private readonly postRepository: PostRepository) {} 毎回挿⼊するのは⾯倒 Inject

Slide 36

Slide 36 text

abstract classをDIのトークンにするTips 36 // application/repository/PostRepository.ts export abstract class PostRepository { abstract createPost(props: CreatePostProps): Promise; } // infrastructure/repository/Repository.module.ts @Module({ imports: [PrismaModule], providers: [ { provide: PostRepository, useClass: PostRepositoryImpl, }, ] exports: PostRepository, }) export class RepositoryModule {}

Slide 37

Slide 37 text

abstract classをDIのトークンにするTips 37 // application/repository/PostRepository.ts export abstract class PostRepository { abstract createPost(props: CreatePostProps): Promise; } // infrastructure/repository/Repository.module.ts @Module({ imports: [PrismaModule], providers: [ { provide: PostRepository, useClass: PostRepositoryImpl, }, ] exports: PostRepository, }) export class RepositoryModule {} abstract class abstract class

Slide 38

Slide 38 text

38 Inject abstract classをDIのトークンにするTips // application/usecase/CreatePost.usecase.ts @Injectable() export class CreatePostUseCase { constructor(@Inject("PostRepositoryToken") private readonly postRepository: PostRepository) {}

Slide 39

Slide 39 text

DI(Dependency Injection) を使うことで、関⼼事が分離できた

Slide 40

Slide 40 text

DI(Dependency Injection) を使うことで、関⼼事が分離できたか?

Slide 41

Slide 41 text

UseCaseで複数のテーブル操作を同⼀の DBトランザクションにしたいこともある

Slide 42

Slide 42 text

42 外側が内側に滲み出てしまう // application/usecase/CreatePost.usecase.ts @Injectable() export class CreatePostUseCase { constructor( private readonly postRepository: PostRepository, private readonly prismaClient: MyPrismaClient ) {} async execute( dto: CreatePostUseCaseInputDto ): Promise { const postId = ulid(); await this.prismaClient.$transaction(async (prismaTransaction) => { await this.postRepository.createPost( { postId, title: dto.title, contents: dto.contents, }, prismaTransaction ); }); return { postId }; } }

Slide 43

Slide 43 text

43 外側が内側に滲み出てしまう @Injectable() export class CreatePostUseCase { constructor( private readonly postRepository: PostRepository, private readonly prismaClient: MyPrismaClient ) {} async execute( dto: CreatePostUseCaseInputDto ): Promise { const postId = ulid(); await this.prismaClient.$transaction(async (prismaTransaction) => { await this.postRepository.createPost( { postId, title: dto.title, contents: dto.contents, }, prismaTransaction ); }); return { postId }; } } Infrastructure

Slide 44

Slide 44 text

Async Hooksで 外側(Infrastructure)への依存を回避する

Slide 45

Slide 45 text

• ⾮同期リソースをトラッキングできる • Node.js 8.2.1からExperimentalなAPIとして導⼊さ れた • 現時点(2022年5⽉20⽇現在)の最新版である 18.2.0においても、Experimentalである 45 Async Hooksとは

Slide 46

Slide 46 text

• スレッドローカルストレージのように使える • Async Hooksを使⽤して実現している 46 Continuation-Local Storage ( Hooked )

Slide 47

Slide 47 text

47 DBトランザクションも抽象に依存できる

Slide 48

Slide 48 text

実装してみる

Slide 49

Slide 49 text

49 clsを使ってContextを定義 // infrastructure/volatility/ClsContext.ts import * as cls from "cls-hooked"; export class ClsContext { private context: cls.Namespace; constructor(private readonly tokenKey: string) { this.context = cls.createNamespace(this.constructor.name); } async run(fn: () => Promise): Promise { return this.context.runPromise(fn); } set(v: T): void { this.context.set(this.tokenKey, v); } get(): T | undefined { return this.context.get(this.tokenKey); } }

Slide 50

Slide 50 text

50 clsを使ってContextを定義 // infrastructure/volatility/PrismaTransactionContext.ts @Injectable() export class PrismaTransactionContext extends ClsContext { constructor() { super("prismaTransactionClient"); } } // infrastructure/volatility/PrismaTransactionContext.module.ts @Module({ providers: [PrismaTransactionContext], exports: [PrismaTransactionContext], }) export class PrismaTransactionContextModule {}

Slide 51

Slide 51 text

51 トランザクションを表現する抽象を定義 // application/repository/DatabaseTransactionRepository.ts export abstract class DatabaseTransactionRepository { abstract runTransaction(fn: () => Promise): Promise; }

Slide 52

Slide 52 text

52 抽象に関する詳細な実装 // infrastructure/repository/PrismaTransactionRepository.ts export class PrismaTransactionRepositoryImpl implements DatabaseTransactionRepository { constructor( private readonly client: MyPrismaClient, private readonly prismaTransactionContext: PrismaTransactionContext ) {} async runTransaction(fn: () => Promise): Promise { return this.client.$transaction((transactionClient) => { return this.prismaTransactionContext.run(() => { this.prismaTransactionContext.set(transactionClient); return fn(); }); }); } }

Slide 53

Slide 53 text

53 依存を逆転させる // infrastructure/Repository.module.ts const providers = [ { provide: PostRepository, useClass: PostRepositoryImpl, }, { provide: DatabaseTransactionRepository, useClass: PrismaTransactionRepositoryImpl, }, ]; @Module({ imports: [ PrismaTestClientModule, PrismaTransactionContextModule ], providers, exports: providers.map((v) => v.provide), }) export class RepositoryModule {}

Slide 54

Slide 54 text

54 トランザクションのContextをDI // infrastructure/Repository.module.ts const providers = [ { provide: PostRepository, useClass: PostRepositoryImpl, }, { provide: DatabaseTransactionRepository, useClass: PrismaTransactionRepositoryImpl, }, ]; @Module({ imports: [ PrismaTestClientModule, PrismaTransactionContextModule ], providers, exports: providers.map((v) => v.provide), }) export class RepositoryModule {} Context

Slide 55

Slide 55 text

55 トランザクションのクライアントを使⽤ // infrastructure/repository/PostRepository.ts @Injectable() export class PostRepositoryImpl implements PostRepository { constructor( private readonly prismaClient: MyPrismaClient, private readonly prismaTransactionContext: PrismaTransactionContext ) {} async createPost(props: CreatePostProps): Promise { const client = this.prismaTransactionContext.get() ?? this.prismaClient; await client.post.create({ data: { postId: props.postId, title: props.title, }, }); await Promise.all( props.contents.map((content) => client.postContent.create({ data: { postId: props.postId, contentOrder: content.order, contentType: content.type, body: content.body, }, }) ) ); } }

Slide 56

Slide 56 text

56 トランザクションのクライアントを使⽤ // infrastructure/repository/PostRepository.ts @Injectable() export class PostRepositoryImpl implements PostRepository { constructor( private readonly prismaClient: MyPrismaClient, private readonly prismaTransactionContext: PrismaTransactionContext ) {} async createPost(props: CreatePostProps): Promise { const client = this.prismaTransactionContext.get() ?? this.prismaClient; await client.post.create({ data: { postId: props.postId, title: props.title, }, }); await Promise.all( props.contents.map((content) => client.postContent.create({ data: { postId: props.postId, contentOrder: content.order, contentType: content.type, body: content.body, }, }) ) ); } } Client

Slide 57

Slide 57 text

57 UseCaseでDBトランザクションを張る // application/repository/CreatePost.usecase.ts @Injectable() export class CreatePostUseCase { constructor( private readonly postRepository: PostRepository, private readonly databaseTransactionRepository: DatabaseTransactionRepository ) {} async execute( dto: CreatePostUseCaseInputDto ): Promise { const postId = ulid(); await this.databaseTransactionRepository.runTransaction(async () => { await this.postRepository.createPost({ postId, title: dto.title, contents: dto.contents, }); }); return { postId }; } }

Slide 58

Slide 58 text

58 UseCaseでDBトランザクションを張る // application/repository/CreatePost.usecase.ts @Injectable() export class CreatePostUseCase { constructor( private readonly postRepository: PostRepository, private readonly databaseTransactionRepository: DatabaseTransactionRepository ) {} async execute( dto: CreatePostUseCaseInputDto ): Promise { const postId = ulid(); await this.databaseTransactionRepository.runTransaction(async () => { await this.postRepository.createPost({ postId, title: dto.title, contents: dto.contents, }); }); return { postId }; } }

Slide 59

Slide 59 text

Async Hooksを使うことで、 UseCaseで外側(Infrastructure)に依存せずに DBトランザクションを表現できた

Slide 60

Slide 60 text

Agenda 60 1.アプリケーション開発・運⽤の現場では何が起 こるのか 2.クリーンアーキテクチャと正当性・堅牢性 3.クリーンアーキテクチャの実現 4.正当性・堅牢性の実現

Slide 61

Slide 61 text

正当性・堅牢性の実現 61

Slide 62

Slide 62 text

ਖ਼౰ੑ ݎ࿚ੑ ৗʹਖ਼֬͞ɺਖ਼͠͞ΛॏΜ͡Δ ֎ք͔Βͷ༷ʑͳೖྗ΍ग़ྗΛɺίϯςΫετ ʹԠͯ͡ద੾ʹϋϯυϦϯά͠ɺม׵͢Δ • 正当性 • 常に正確さ、正しさを重んじる • 堅牢性 • 外界から様々な⼊⼒や出⼒を、コンテクストに応 じて適切にハンドリングして変換する 62 [再掲]正当性・堅牢性 - https://speakerdeck.com/twada/growing-reliable-code-phperkaigi-2022

Slide 63

Slide 63 text

1. 堅牢性を担保したいがために、class-validatorを 使ったDtoで⾊々やりすぎてしまう • ドメイン知識と重複しがち 2. Validationで使うDtoは外界とのアダプターの役割 を持っている • つまり外側の持ち物なので、内部に持ち込めない • 持ち込むと正当性が脅かされる • 変更がしにくくなる 63 NestJSのValidationの問題点 (async () => { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); })(); export class CreateUserDto { @IsEmail() email: string; @IsNotEmpty() password: string; } @Controller() export class SampleController { @Post() create(@Body() createUserDto: CreateUserDto) { return 'This action adds a new user'; } } https://docs.nestjs.com/techniques/validation

Slide 64

Slide 64 text

使わない、または アダプターに関⼼があることだけやる 64 NestJSのValidationはどうすればいいのか

Slide 65

Slide 65 text

じゃあどうすればいいのか

Slide 66

Slide 66 text

あるモデル要素について、その属性しか関⼼の対象とな らないのであれば、その要素を値オブジェクトとして 分類すること。 値オブジェクトに、⾃分が伝える属性の意味を表現さ せ、関係した機能を与えること。 値オブジェクトを不変なものとして扱うこと。 同⼀性を与えず、エンティティを維持するために必要と なる複雑な設計を避けること。 66 DDDの値オブジェクトパターン

Slide 67

Slide 67 text

密接に関係し合うロジックが⼀箇所にギュッと集まっ ている構造を⾼凝集と呼びます。 ⼤事なのはデータとデータを操作するロジックを⼀箇 所にまとめて⾼凝集にすること、そして必要な操作だ けを外に公開するようカプセル化する考えです。 ⾼凝集な構造は、変更に強い、望ましい構造です。逆に 低凝集な構造は、壊れやすく変更が困難です。 67 ⾼凝集な構造

Slide 68

Slide 68 text

⽅法1)NewTypeで表現し、Prismで完全な値にする 68 ⾼凝集な構造で表現 import { prism } from "newtype-ts"; import type { Newtype } from "newtype-ts"; export type PostTitle = Newtype< { readonly POST_TITLE: unique symbol }, string >; export const PostTitle = { prism: prism((s) => s !== ""), } as const; PostTitle.prism.getOption("タイトル") ⽅法2)オブジェクトで表現し、ファクトリで完全な値にする export class PostTitle { private constructor(private readonly value: string) {} asString(): string { return this.value; } // Either, PostTitlte> を返すのもアリ static from(s: string): PostTitle { if (s !== "") { throw new Error("PostTitleに空文字は許可されません") } return new PostTitle(s) } } PostTitle.from("タイトル")

Slide 69

Slide 69 text

69 UseCaseに渡す前に値に変換する // presentation/posts/Posts.controller.ts @Controller("posts") export class PostsController { constructor(private readonly createPostUseCase: CreatePostUseCase) {} @Post() async createPost(@Body() dto: CreatePostDto): Promise { const title = (() => { try { return PostTitle.from(dto.title) } catch (e) { throw new BadRequestException(e, "titleが不正な値でした"); } })(); return this.createPostUseCase.execute({ title }); } }

Slide 70

Slide 70 text

アダプターの関⼼事と ドメインの関⼼事を分離できた ⾼凝集な値でロジックが散らばりにくくなった

Slide 71

Slide 71 text

71 まとめ • NestJSのDIにより依存性逆転の原則を適⽤ • 業務ロジックが技術選択の関⼼事に依存しにくくなり、正当性が⾼いアプリケーションになる • 正当性を重んじるレイヤーと堅牢性を重んじるレイヤーを分けることで関⼼事が分離できる • ⾼凝集な表現により、制約や意図を型として表現可能になり、堅牢なコードが書ける

Slide 72

Slide 72 text

採⽤情報について 詳細はこちらから フロントエンドエンジニアほか ポジション採⽤を⾏っています

Slide 73

Slide 73 text

THANK YOU

Slide 74

Slide 74 text

74 [Appendix] 正当性と堅牢性 - https://speakerdeck.com/twada/growing-reliable-code-phperkaigi-2022 ਖ਼౰ੑͱݎ࿚ੑ w ࠷దͳΤϥʔॲཧ͸Τϥʔ͕ൃੜͨ͠ιϑτ΢ΣΞͷछྨʹΑΓҟͳΔ w ਖ਼౰ੑͱ͸ɺෆਖ਼֬ͳ݁ՌΛܾͯ͠ฦ͞ͳ͍͜ͱΛҙຯ͢Δɻෆਖ਼֬ͳ ݁ՌΛฦ͘͢Β͍ͳΒɺԿ΋ฦ͞ͳ͍ํ͕·͠Ͱ͋Δ w ݎ࿚ੑͱ͸ɺιϑτ΢ΣΞͷ࣮ߦΛܧଓͰ͖ΔΑ͏ʹखΛਚ͘͢͜ͱͰ ͋ΔɻͦΕʹΑͬͯෆਖ਼֬ͳ݁Ռ͕΋ͨΒ͞ΕΔ͜ͱ͕͋ͬͯ΋͔·Θ ͳ͍ w ҆શੑ ΍ਖ਼֬ੑ Λॏࢹ͢ΔΞϓϦέʔγϣϯͰ͸ɺݎ࿚ੑΑΓ΋ਖ਼౰ ੑ͕༏ઌ͞ΕΔ܏޲ʹ͋Δ w ίϯγϡʔϚΞϓϦέʔγϣϯͰ͸ɺਖ਼౰ੑΑΓ΋ݎ࿚ੑ͕༏ઌ͞ΕΔ ܏޲ʹ͋Δ IUUQTXXXBNB[PODPKQEQ9

Slide 75

Slide 75 text

• Keep it simple, stupid. • ケリー・ジョンソン⽒によって提唱された原則 • 不必要な複雑性は避けるべきである、設計の単純 性・簡潔性は成功への鍵 • アプリケーションの変更は複雑性が⾼くなり、保守 コスト・拡張コストが⾼くなる • シンプルなコードを保つことが好ましい 75 [Appendix] KISSの原則 https://en.wikipedia.org/wiki/ Kelly_Johnson_(engineer)