Upgrade to Pro — share decks privately, control downloads, hide ads and more …

運用していくアプリケーション開発のヒント

 運用していくアプリケーション開発のヒント

NestJS meetup Online #2 (https://nest-jp.connpass.com/event/244015/) で発表した資料です。

kenchan0130

May 20, 2022
Tweet

More Decks by kenchan0130

Other Decks in Technology

Transcript

  1. 2 ⾃⼰紹介 kenchan0130 Tadayuki Onishi Software Engineer @FOLIO 🔑 Tech

    keywords TypeScript Scala Ruby SRE AWS GCP Corporate IT https://kenchan0130.github.io 📄 Blog
  2. 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
  3. ਖ਼౰ੑ ݎ࿚ੑ ৗʹਖ਼֬͞ɺਖ਼͠͞ΛॏΜ͡Δ ֎ք͔Βͷ༷ʑͳೖྗ΍ग़ྗΛɺίϯςΫετ ʹԠͯ͡ద੾ʹϋϯυϦϯά͠ɺม׵͢Δ • 円の外側(外界に近いレイヤー)ではより堅牢性、 円の内側(アプリケーションのコアロジックに近い レイヤー)ではより正当性を表現できる •

    関⼼事が分離できる • テストおよびリファクタリングがしやすくなる • 変更しやすいアプリケーションに近づく 15 - https://speakerdeck.com/twada/growing-reliable-code-phperkaigi-2022 クリーンアーキテクチャと堅牢性・正当性 ੺ͱ੨ͷք໘ɺ૚ͷ๷ޚϥΠϯ͕͋Δ
  4. 抽象を定義 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<void>; }
  5. 抽象に関する詳細な実装 25 // infrastructure/repository/PostRepository.ts @Injectable() export class PostRepositoryImpl implements PostRepository

    { constructor(private readonly prismaClient: MyPrismaClient) {} async save(props: CreatePostProps): Promise<void> { 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, })), }, }, }); } }
  6. 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 {}
  7. 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 {}
  8. UseCaseでは抽象を使⽤ 28 // application/usecase/CreatePost.usecase.ts @Injectable() export class CreatePostUseCase { constructor(private

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

    readonly postRepository: PostRepository) {} async execute( dto: CreatePostUseCaseInputDto ): Promise<CreatePostUseCaseOutputDto> { const postId = ulid(); await this.postRepository.save({ postId, title: dto.title, contents: dto.contents, }); return { postId }; } } UseCase
  10. interfaceはDIのトークンにできない 33 // application/repository/PostRepository.ts export interface PostRepository { createPost(props: CreatePostProps):

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

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

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

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

    { constructor(@Inject("PostRepositoryToken") private readonly postRepository: PostRepository) {}
  15. 42 外側が内側に滲み出てしまう // application/usecase/CreatePost.usecase.ts @Injectable() export class CreatePostUseCase { constructor(

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

    postRepository: PostRepository, private readonly prismaClient: MyPrismaClient ) {} async execute( dto: CreatePostUseCaseInputDto ): Promise<CreatePostUseCaseOutputDto> { const postId = ulid(); await this.prismaClient.$transaction(async (prismaTransaction) => { await this.postRepository.createPost( { postId, title: dto.title, contents: dto.contents, }, prismaTransaction ); }); return { postId }; } } Infrastructure
  17. 49 clsを使ってContextを定義 // infrastructure/volatility/ClsContext.ts import * as cls from "cls-hooked";

    export class ClsContext<T> { private context: cls.Namespace; constructor(private readonly tokenKey: string) { this.context = cls.createNamespace(this.constructor.name); } async run<T>(fn: () => Promise<T>): Promise<T> { return this.context.runPromise(fn); } set(v: T): void { this.context.set(this.tokenKey, v); } get(): T | undefined { return this.context.get(this.tokenKey); } }
  18. 50 clsを使ってContextを定義 // infrastructure/volatility/PrismaTransactionContext.ts @Injectable() export class PrismaTransactionContext extends ClsContext<Prisma.TransactionClient>

    { constructor() { super("prismaTransactionClient"); } } // infrastructure/volatility/PrismaTransactionContext.module.ts @Module({ providers: [PrismaTransactionContext], exports: [PrismaTransactionContext], }) export class PrismaTransactionContextModule {}
  19. 52 抽象に関する詳細な実装 // infrastructure/repository/PrismaTransactionRepository.ts export class PrismaTransactionRepositoryImpl implements DatabaseTransactionRepository {

    constructor( private readonly client: MyPrismaClient, private readonly prismaTransactionContext: PrismaTransactionContext ) {} async runTransaction<T>(fn: () => Promise<T>): Promise<T> { return this.client.$transaction((transactionClient) => { return this.prismaTransactionContext.run(() => { this.prismaTransactionContext.set(transactionClient); return fn(); }); }); } }
  20. 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 {}
  21. 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
  22. 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<void> { 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, }, }) ) ); } }
  23. 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<void> { 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
  24. 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<CreatePostUseCaseOutputDto> { const postId = ulid(); await this.databaseTransactionRepository.runTransaction(async () => { await this.postRepository.createPost({ postId, title: dto.title, contents: dto.contents, }); }); return { postId }; } }
  25. 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<CreatePostUseCaseOutputDto> { const postId = ulid(); await this.databaseTransactionRepository.runTransaction(async () => { await this.postRepository.createPost({ postId, title: dto.title, contents: dto.contents, }); }); return { postId }; } }
  26. ਖ਼౰ੑ ݎ࿚ੑ ৗʹਖ਼֬͞ɺਖ਼͠͞ΛॏΜ͡Δ ֎ք͔Βͷ༷ʑͳೖྗ΍ग़ྗΛɺίϯςΫετ ʹԠͯ͡ద੾ʹϋϯυϦϯά͠ɺม׵͢Δ • 正当性 • 常に正確さ、正しさを重んじる •

    堅牢性 • 外界から様々な⼊⼒や出⼒を、コンテクストに応 じて適切にハンドリングして変換する 62 [再掲]正当性・堅牢性 - https://speakerdeck.com/twada/growing-reliable-code-phperkaigi-2022
  27. 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
  28. ⽅法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<PostTitle>((s) => s !== ""), } as const; PostTitle.prism.getOption("タイトル") ⽅法2)オブジェクトで表現し、ファクトリで完全な値にする export class PostTitle { private constructor(private readonly value: string) {} asString(): string { return this.value; } // Either<NonEmptyArray<Error>, PostTitlte> を返すのもアリ static from(s: string): PostTitle { if (s !== "") { throw new Error("PostTitleに空文字は許可されません") } return new PostTitle(s) } } PostTitle.from("タイトル")
  29. 69 UseCaseに渡す前に値に変換する // presentation/posts/Posts.controller.ts @Controller("posts") export class PostsController { constructor(private

    readonly createPostUseCase: CreatePostUseCase) {} @Post() async createPost(@Body() dto: CreatePostDto): Promise<GetPostResponse> { const title = (() => { try { return PostTitle.from(dto.title) } catch (e) { throw new BadRequestException(e, "titleが不正な値でした"); } })(); return this.createPostUseCase.execute({ title }); } }
  30. 74 [Appendix] 正当性と堅牢性 - https://speakerdeck.com/twada/growing-reliable-code-phperkaigi-2022 ਖ਼౰ੑͱݎ࿚ੑ w ࠷దͳΤϥʔॲཧ͸Τϥʔ͕ൃੜͨ͠ιϑτ΢ΣΞͷछྨʹΑΓҟͳΔ w ਖ਼౰ੑͱ͸ɺෆਖ਼֬ͳ݁ՌΛܾͯ͠ฦ͞ͳ͍͜ͱΛҙຯ͢Δɻෆਖ਼֬ͳ

    ݁ՌΛฦ͘͢Β͍ͳΒɺԿ΋ฦ͞ͳ͍ํ͕·͠Ͱ͋Δ w ݎ࿚ੑͱ͸ɺιϑτ΢ΣΞͷ࣮ߦΛܧଓͰ͖ΔΑ͏ʹखΛਚ͘͢͜ͱͰ ͋ΔɻͦΕʹΑͬͯෆਖ਼֬ͳ݁Ռ͕΋ͨΒ͞ΕΔ͜ͱ͕͋ͬͯ΋͔·Θ ͳ͍ w ҆શੑ ΍ਖ਼֬ੑ Λॏࢹ͢ΔΞϓϦέʔγϣϯͰ͸ɺݎ࿚ੑΑΓ΋ਖ਼౰ ੑ͕༏ઌ͞ΕΔ܏޲ʹ͋Δ w ίϯγϡʔϚΞϓϦέʔγϣϯͰ͸ɺਖ਼౰ੑΑΓ΋ݎ࿚ੑ͕༏ઌ͞ΕΔ ܏޲ʹ͋Δ IUUQTXXXBNB[PODPKQEQ9
  31. • Keep it simple, stupid. • ケリー・ジョンソン⽒によって提唱された原則 • 不必要な複雑性は避けるべきである、設計の単純 性・簡潔性は成功への鍵

    • アプリケーションの変更は複雑性が⾼くなり、保守 コスト・拡張コストが⾼くなる • シンプルなコードを保つことが好ましい 75 [Appendix] KISSの原則 https://en.wikipedia.org/wiki/ Kelly_Johnson_(engineer)