Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

3 Mission

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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") } 呼び出し元の例

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

12 ① 簡易DI

Slide 13

Slide 13 text

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したいクラスの実装

Slide 14

Slide 14 text

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のインスタンスを持つコンテキスト型を定義

Slide 15

Slide 15 text

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") } 呼び出し元

Slide 16

Slide 16 text

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)) テストコード

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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のバケツリレーをしている ② セッションやトランザクションの引き回し

Slide 20

Slide 20 text

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を利用できる

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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コード例

Slide 23

Slide 23 text

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を返すコンストラクタ

Slide 24

Slide 24 text

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なコンストラクタ

Slide 25

Slide 25 text

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の利用とエラーハンドリング

Slide 26

Slide 26 text

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 } ) エラー時 正常時

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

No content