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

実践Context Receiver 〜現場で使えるかもしれないパターン3選〜

実践Context Receiver 〜現場で使えるかもしれないパターン3選〜

More Decks by コドモン開発チーム

Transcript

  1. 実践Context Receiver
    〜現場で使えるかもしれないパターン3選〜
    2023年06月23日
    @Server-Side Kotlin Meetup vol.9
    関根 純

    View full-size slide

  2. 2
    経歴
    プロダクト開発チームにて、バックエンド、フロントエンド問わず開
    発に携わる。息子とディズニーとサッカーとArrow-ktが好き。
    自己紹介
    関根 純 せきね じゅん
    2023.01 コドモンに開発エンジニアとして入社
    2023.06 Kotlin & Reactでプロダクト開発

    View full-size slide

  3. 4
    すべての先生に 子どもと向き合う
    時間と心のゆとりを
    こんなプロダクトを開発しています
    メインプロダクトは、保育・教育施設向けWebアプリケーション。
    保護者と施設のやり取りを支えるモバイルアプリケーションや、施設職員向けモバイル版
    アプリケーション、外部サービスと連携するAPIなども開発しています。

    View full-size slide

  4. 5
    コドモンではKotlinコードの保守性を高めるために、
    リファクタなどを通して日々より良いコードの書き方を模索しています。
    きょうは、Kotlin ConfのKeynoteで話題に上がったり、
    前回のこのMeetupで機能紹介のLTがあるなど
    一部で熱い Context Receiver について
    本番コードでの利用を検討している内容をお伝えします。
    はじめに

    View full-size slide

  5. 6
    今日話すこと
    Context Receiver機能についておさらい
    Context Receiverの用法3パターン
    コドモンではどう使っているか
    1
    2
    3

    View full-size slide

  6. 7
    今日話すこと
    Context Receiver機能についておさらい
    Context Receiverの用法3パターン
    コドモンではどう使っているか
    1
    2
    3

    View full-size slide

  7. 8
    CONFIDENTIAL - © 2022 CoDMON Inc. 8
    Context Receiver機能について
    ● 2023年6月現在、Preview機能
    ○ 詳細はKEEPを参照してください
    ● 2022年2月のリリースで、Previewとはいえ既に1年経過
    ○ 標準搭載待ち状態といって良さそう
    ○ 機能的に大幅な変更が加わることもおそらくない
    ● 先日のKotlin Conf ‘23にて、Kotlin 2.0より正式な機能に昇格と発表 🎉

    View full-size slide

  8. 9
    CONFIDENTIAL - © 2022 CoDMON Inc. 9
    Context Receiver機能について
    引き受けたコンテキスト型の内部で
    関数を実行していることになる。
    = 複数の型に対し拡張できるイメージ
    class UserUseCase {
    context(Logger, Storage)
    fun userInfo(name: String): Storage.Info {
    log("Retrieving User info about $name")
    return info(name)
    }
    }
    Logger
    Storage
    fun info(name: String): Info
    fun log(message: String): Unit
    関数定義の例
    UserUseCase
    fun userInfo(name: String): Storage.Info

    View full-size slide

  9. 10
    CONFIDENTIAL - © 2022 CoDMON Inc. 10
    Context Receiver機能について
    withやrunなどのスコープ関数  の引数 block: T.() -> R における
    コンテキスト型Tが、呼び出し先の関数において暗黙的に受け渡される
    ※ スコープ関数は元々ある機能で、渡されるところの機能なのでReceiverなのだと理解
    -/ コンテキストを定義
    class LoggerAndStorageContext (loggerName: String) : Logger, Storage
    -/ コンテキストのインスタンスを生成
    val userStorageContext = LoggerAndStorageContext("user storage")
    -/ コンテキスト内でuserInfoを実行
    -/ = 暗黙的にuserInfoにコンテキストが渡される
    userStorageContext.run { UserUseCase.userInfo("Alice") }
    呼び出し元の例

    View full-size slide

  10. 11
    今日話すこと
    Context Receiver機能についておさらい
    Context Receiverの用法3パターン
    コドモンではどう使っているか
    1
    2
    3

    View full-size slide

  11. 12
    ① 簡易DI

    View full-size slide

  12. 13
    CONFIDENTIAL - © 2022 CoDMON Inc. 13
    ① 簡易DI
    ● ユーザの新規登録のドメインを実装するようなケースを考える
    ● ユニットテストにより期待通りにnewできるか検証したい
      DIでClockを注入することを検討するが、この程度のことのために
      DIコンテナを使うのはtoo muchな気もする...
    class User(val id: UUID, val name: String, val created: LocalDateTime) {
    constructor(name: String):
    this(UUID.randomUUID(), name, LocalDateTime.now())
    init {
    require(name.isNotEmpty()) { "name is empty" }
    }
    } DIしたいクラスの実装

    View full-size slide

  13. 14
    CONFIDENTIAL - © 2022 CoDMON Inc. 14
    ① 簡易DI
    companion objectからコンストラクトするようにしつつ、
    ClockContextを引き受けられるようにすると...
    interface ClockContext {
    val clock: Clock
    }
    class User (val id: UUID, val name: String, val created: LocalDateTime) {
    companion object {
    context(ClockContext)
    fun of(name: String): User {
    require(name.isNotEmpty()) { "name is empty" }
    return User(UUID.randomUUID(), name, LocalDateTime.now(clock))
    }
    }
    }
    引数をそのままに依存しているコンテキストを宣言できる -
    (仕様が分かりづらくなるのでclockは引数には入れたくない) -
    User生成関数の宣言
    Clockのインスタンスを持つコンテキスト型を定義

    View full-size slide

  14. 15
    CONFIDENTIAL - © 2022 CoDMON Inc. 15
    ① 簡易DI
    mainでは呼び出しはこのようにシステムの時計を利用し、
    本来の挙動通りシステムコールを行う時計となる。
    object UTCClockContext: ClockContext {
    override val clock: Clock = Clock.systemUTC()
    }
    val user1 = UTCClockContext.run { User.of("Alice") }
    val user2 = with(UTCClockContext) { User.of("Bob") } 呼び出し元

    View full-size slide

  15. 16
    CONFIDENTIAL - © 2022 CoDMON Inc. 16
    ① 簡易DI
    testでは時計の時刻を固定してnowが特定の値になるようにすることで、
    期待通りの初期化が出来ているかassertできる。
    ※ そもそもモックすべきかについては様々意見や好みがあるので触れない
    class FixedClockContext(time: String): ClockContext {
    override val clock: Clock =
    Clock.fixed(Instant.parse(time), ZoneId.of("UTC"))
    }
    val fixedTime = "2023-12-31T12:34:56.123Z"
    val user = with(FixedClockContext(fixedTime)) { User("Alice") }
    assert(user.created -= LocalDateTime.parse(fixedTime))
    テストコード

    View full-size slide

  16. 17
    ② セッションやトランザクションの引き回し

    View full-size slide

  17. 18
    CONFIDENTIAL - © 2022 CoDMON Inc. 18
    ② セッションやトランザクションの引き回し
    例としてこのようなユースケースを考える。
    ● マイクロサービスアーキテクチャでECサイトを開発している
    ● 商品APIと商品レコメンドAPIが存在している
    ● 商品APIのふるまい
    ○ 任意の商品とユーザ&商品に対するレコメンド商品を返す
    ○ ユーザ情報を商品レコメンドAPIに対して送る必要あり

    View full-size slide

  18. 19
    CONFIDENTIAL - © 2022 CoDMON Inc. 19
    User情報の引き回しを行う例
    // Controller
    fun getItems(req: Request): Response {
    val result = usecase.getItems(req.user, req.itemId)
    return ItemsResponse.from(result)
    }
    // Usecase
    fun getItems(user: User): Result {
    val item = repository.getItem(itemId)
    val recommended = getRecommendedItems(user, itemId)
    return Result(item, recommended)
    }
    // API Client
    fun getRecommendedItems(user: User, itemId: String): List
    レイヤーを跨いでuserのバケツリレーをしている
    ② セッションやトランザクションの引き回し

    View full-size slide

  19. 20
    CONFIDENTIAL - © 2022 CoDMON Inc. 20
    ② セッションやトランザクションの引き回し
    User情報の引き回しにContext Receiverを利用した例
    -/ Controller
    fun someEndpoint(req: Request): Response {
    UserContext(req.user).run { usecase.getItems(req.itemId) }
    }
    -/ Usecase
    context(UserContext)
    fun getItems(user: User): Result {
    val item = repository.getItem(itemId)
    val recommended = getRecommendedItems(itemId)
    return Result(item, recommended)
    }
    -/ API Client
    context(UserContext)
    fun getRecommendedItems(itemId: String): List
    getItemsはUserContext内にあるため、
    apiClient.getItemsに対して暗黙的にUserContextを渡している -
    以降は暗黙的にコンテキストが引き回されるので、
    user情報を扱うコンテキストの宣言を一度だけ行えば良い -
    getRecommendedItemsは
    Controllerで宣言したUserContextを利用できる

    View full-size slide

  20. 21
    ③ チェック例外(Arrow-kt必須)

    View full-size slide

  21. 22
    CONFIDENTIAL - © 2022 CoDMON Inc. 22
    ③ チェック例外(Arrow-kt必須)
    Javaにはあるチェック例外の機構がKotlinでは意図的に省かれている。
    一方で、エラー型を定義して返却するのは良いプラクティスとされている  
    ゆえに極力エラー時の型はシグネチャから分かるようにしておきたい。
    class PositiveInt {
    private int value;
    public PositiveInt(int value) throws InvalidNumberException {
    if (value < 0) {
    throw new InvalidNumberException("Number cannot be less than 0");
    }
    this.value = value;
    }
    } チェック例外を使うJavaコード例

    View full-size slide

  22. 23
    CONFIDENTIAL - © 2022 CoDMON Inc. 23
    ③ チェック例外(Arrow-kt必須)
    Resutを利用することもできるが、
    Result.failureは引数がThrowableなので型情報が失われてしまう(惜しい)。
    companion object {
    fun of(value: Int): Result =
    if (value < 0) {
    Result.failure(
    InvalidNumberException("Number cannot be less than 0")
    )
    } else {
    Result.success(PositiveInt(value))
    }
    } Resultを返すコンストラクタ

    View full-size slide

  23. 24
    CONFIDENTIAL - © 2022 CoDMON Inc. 24
    ③ チェック例外(Arrow-kt必須)
    Arrow-ktのRaiseをコンテキストとして受け取るパターン
    Raiseのコンテキスト内でensureやraise等Raiseの関数を呼び出すと、
    その時点で処理が中断する(≒ throwに近い)かたちとなる。
    context(Raise)
    fun of(value: Int): PositiveInt {
    if(value -= 0) {
    raise(InvalidNumberException("Number cannot be less than 0"))
    }
    return PositiveInt(value)
    }
    raiseがthrow相当で、コンテキストの宣言を行った箇所まで飛ぶ -
    コンテキストがRaiseなコンストラクタ

    View full-size slide

  24. 25
    CONFIDENTIAL - © 2022 CoDMON Inc. 25
    ③ チェック例外(Arrow-kt必須)
    Raiseをコンテキストとして関数を呼ぶために、
    Arrow-ktが提供するRaise用fold関数を利用する。
    fold(
    { -/ 第一引数: Raiseをコンテキストとした処理
    PositiveInt.of(-1) },
    { -/ 第二引数: エラーがraiseされたときの処理
    it -> println(“Failed… ${it.message}“) },
    { -/ 第三引数: 正常時の処理
    it -> println(“Success!!!”)
    }
    )
    PositiveIntに対してマイナス値を与えているため、
    raiseされて第二引数のブロックが呼び出される。
    Raiseの利用とエラーハンドリング

    View full-size slide

  25. 26
    CONFIDENTIAL - © 2022 CoDMON Inc. 26
    main()
    ③ チェック例外(Arrow-kt必須)
    Raise
    fun: raise(r: Error): Nothing
    PositiveInt
    fun: of(int: Int): PositiveInt {
    if ( int < 0 ) raise(Error())
    PositiveInt(int)
    }
    fold({
    },
    { e: Error => T },
    { pi: PositiveInt => T } )
    エラー時
    正常時

    View full-size slide

  26. 27
    今日話すこと
    Context Receiver機能についておさらい
    Context Receiverの用法3パターン
    コドモンではどう使っているか
    1
    2
    3

    View full-size slide

  27. 28
    CONFIDENTIAL - © 2022 CoDMON Inc. 28
    コドモンでのContext Receiver
    ① 簡易DIのパターン => 利用中 🥰
    ● ドメインロジックにおける副作用を含む部分を外部から注入したい
    ○ [背景] webアプリ(Spring WebFlux)をクリーンアーキテクチャにより実装
    ○ ドメインロジックを可能な限りピュアに保ちFWへの依存をなくしたくて、
    このパターンでClockをDIすることにした
    採用してみての社内からのフィードバック -
    テスタビリティが上がることを実感している。
    関数のシグネチャ + Context宣言でニコイチである
    ことを覚えておく必要があるのは慣れなければ。

    View full-size slide

  28. 29
    CONFIDENTIAL - © 2022 CoDMON Inc. 29
    コドモンでのContext Receiver
    ② セッションやトランザクションの引き回し = 継続検討 🤔
    1. 見送 DBトランザクション情報をクラスを跨いで受け渡す
    ○ 現行のクラス設計上かならずしも必要ではないという話になった
    ○ やるにしてもSpring Frameworkを利用しているのでその流儀に則る
    2. 将来 ログインユーザー情報の引き回し
    ○ マイクロサービスアーキテクチャを採用しており、publicなapiで処理した
       認証ユーザ情報をprivateなapiに対して渡す必要性がいずれ出てくる

    View full-size slide

  29. 30
    CONFIDENTIAL - © 2022 CoDMON Inc. 30
    ③ チェック例外(Arrow) => 未検討だが見送りの可能性高し 😇
    ● Arrow-kt自体はEither等のデータ型のために導入している
    ● それでもRaiseへ置き換えるにはハードルがやや高いと感じる
    ○ Kotlin + Eitherに加えてRaiseまで覚えるのはさすがに学習コスト高い
    ○ && 他言語(PHP)を書いていたチームから異動したメンバーもいる
    ※ もしかしたらEitherを捨ててRaiseだけでいくぞ!と判断できれば良いかもしれない。
      Eitherは社内で他チーム含めある程度浸透しており今更Either使わないとは言いづらい
    コドモンでのContext Receiver

    View full-size slide

  30. 31
    CONFIDENTIAL - © 2022 CoDMON Inc. 31
    さいごに: Context Receiver自体への感想
    ● 便利ではあるが、現実的に環境で利用するかは悩ましい
    ○ 😁 テスト容易性やコードの型安全性UP、ボイラープレート削減
    ○ 🥺 単純に機能自体への理解ハードルがありeasyさを欠く
    ■ 不用意に難易度をあげて保守性がむしろ下がるリスク
    ■ “暗黙的に” というところがそう感じさせる
    ● 検討結果としては ”良し悪しあるので使い方の理解が必要” という形に
    ○ 用法用量を守って使うためにはガイドラインがあると良さそう
    = どういう時に使えてどういう時に避けるほうが良いか

    View full-size slide

  31. 32
    コドモン採用ページ 開発ブログ
    コドモンでは一緒に働きたい仲間を募集しています!

    View full-size slide

  32. 33
    ご清聴ありがとうございました!

    View full-size slide