Slide 1

Slide 1 text

© 2024 Loglass Inc. 2024.10.25 株式会社ログラス 佐藤有斗 Railway Oriented Programming を オニオンアーキテクチャに適用する by kotlin-result

Slide 2

Slide 2 text

© 2024 Loglass Inc. 佐藤 有斗(ゆいと) 株式会社ログラス プロダクト開発部 エンジニア 株式会社ログラスに2020年12月に入社。 主に新規事業の立ち上げを担当。得意分野は型と自動テスト。 KotlinでたまにOSSを作ったり、他のOSSにコントリビュート したりしてます。 Yuito Sato

Slide 3

Slide 3 text

© 2024 Loglass Inc. 宣伝: n月刊ラムダノートという技術情報誌に記事を寄稿しました! ご興味があればぜひ! n月刊ラムダノート Vol.4, No.3(2024) https://www.lambdanote.com/product s/n-vol-4-no-3-2024

Slide 4

Slide 4 text

© 2024 Loglass Inc. ログラスとは ログラスについて 企業価値を向上する 
 経営管理クラウド 


Slide 5

Slide 5 text

© 2024 Loglass Inc. ログラスについて 企業価値を向上する 
 経営管理クラウド 


Slide 6

Slide 6 text

© 2024 Loglass Inc. Railway Oriented Programming とは? ● 正常ケースと異常ケースの2つのレールを型で表現しながら設計をする手法 ○ いわゆる関数型的なエラーハンドリングをする設計手法 ○ RustでいうResult。OkとErrの二つの型を持つ ○ Kotlinではkolin-resultのResult、Arrow.ktのEitherが該当する 引用: Railway Oriented Programming https://fsharpforfunandprofit.com/rop/

Slide 7

Slide 7 text

© 2024 Loglass Inc. kotlin-result ● KotlinでRailway Oriented Programmingができるライブラリ Result // 例)文字列からメモというモデルを作成する関数の型 (String) -> Result 参考: kotlin-result https://github.com/michaelbull/kotlin-result

Slide 8

Slide 8 text

© 2024 Loglass Inc. 使用例 fun createMemo(text: String): Result { return if (text.length <= 140) { Ok(Memo(text)) } else { Err(CreateMemoError()) } }

Slide 9

Slide 9 text

© 2024 Loglass Inc. 正常ケース 2つのレール 異常ケース

Slide 10

Slide 10 text

© 2024 Loglass Inc. String Memo 正常ケース String Memo

Slide 11

Slide 11 text

© 2024 Loglass Inc. String Memo 異常ケース String CreateMemoError 文字列が140字を 超えてる!

Slide 12

Slide 12 text

© 2024 Loglass Inc. Railway Oriented Programming の良いとこ ● 呼び元でエラーハンドリングを強制できる ○ 検査例外がないKotlinだと例外ハンドリングを強制できない ○ 標準のResult型はハンドリングする例外を指定できない ● 複雑な処理を制御しやすい ○ 処理の合成がしやすい(直列と並列の処理の合成)

Slide 13

Slide 13 text

© 2024 Loglass Inc. Railway Oriented Programming の良いとこ ● 呼び元でエラーハンドリングを強制できる ○ 検査例外がないKotlinだと例外ハンドリングを強制できない ○ 標準のResult型はハンドリングする例外を指定できない ● 複雑な処理を制御しやすい ○ 処理の合成がしやすい(直列と並列の処理の合成) 時間の都合上 今日はこちらのみ

Slide 14

Slide 14 text

© 2024 Loglass Inc. Railway Oriented Programming で処理を直列に合成する ● ユーザ作成(バリデーション→ユーザを作成→通知メールを送信) バリデーション ユーザ作成 通知メール送信 Request ValidateRegister UserRequestError Validated Request User CreateUserError Unit SendEmailError

Slide 15

Slide 15 text

© 2024 Loglass Inc. // バリデーション fun validateRegisterUserRequest( request: RegisterUserRequest, ): Result { ... } // ユーザー作成 fun createUser( request: ValidatedRegisterUserRequest, ): Result { ... } // メールを送信 fun sendCreateUserNotificationEmail( createdUser: User, ): Result { ... }

Slide 16

Slide 16 text

© 2024 Loglass Inc. Railway Oriented Programming で処理を直列に合成する ● andThenで処理を合成(binding, flatMapも同じことが可能) バリデーション ユーザ作成 通知メール送信 Request ValidateRegister UserRequestError Validated Request User CreateUserError Unit SendEmailError andThen andThen

Slide 17

Slide 17 text

© 2024 Loglass Inc. // ユーザー登録処理。各処理を連結 fun registerUser( request: RegisterUserRequest, ): Result { return validateCreateUserRequest(request) // バリデーション .mapError { error -> ... } .andThen { validatedRequest -> createUser(validatedRequest).mapError { error -> ... } // ユーザー作成 } .andThen { createdUser -> sendCreateUserNotificationEmail(createdUser).mapError { error -> ... } // 通知メール送信 } }

Slide 18

Slide 18 text

© 2024 Loglass Inc. Railway Oriented Programming で処理を並列に合成する ● zipで処理を合成 zip CreateUserError UseName CreateUserError String UserName, Email Email CreateUserError String 一つ目のエラーを返す User

Slide 19

Slide 19 text

© 2024 Loglass Inc. fun validateAndCreate(userName: String, email: String): Result { return zip( { UserName.validateAndCreate(userName) }, { Email.validateAndCreate(email) }, ) { validatedUserName, validatedEmail -> User( id = UserId.generate(), userName = validatedUserName, email = validatedEmail, ) } }

Slide 20

Slide 20 text

© 2024 Loglass Inc. ● zipOrAccumulateで処理を合成 zipOrAccumulate List UseName CreateUserError String Email CreateUserError String zipOrAccumulateでエラーをまとめて返す UserName, Email User

Slide 21

Slide 21 text

© 2024 Loglass Inc. fun validateAndCreate(userName: String, email: String): Result> { return zipOrAccumulate( { UserName.validateAndCreate(userName) }, { Email.validateAndCreate(email) }, ) { validatedUserName, validatedEmail -> User( id = UserId.generate(), userName = validatedUserName, email = validatedEmail, ) } }

Slide 22

Slide 22 text

© 2024 Loglass Inc. List>を並列に処理する ● いわゆる一括処理はcombineで処理を並列に合成できる combine CreateUserError User CreateUserError Request List User CreateUserError Request User CreateUserError Request User User User 一つ目のエラーを返す

Slide 23

Slide 23 text

© 2024 Loglass Inc. // ユーザー作成処理を並列に合成 fun createUsers( request: ValidatedRegisterUserRequest, ): Result, CreateUserError> { val resultUserList: List> = requests.map { createUser(it) } val userListResult: Result, CreateUserError> = resultUserList.combine() return userListResult }

Slide 24

Slide 24 text

© 2024 Loglass Inc. エラーをまとめて返すなどもカスタムすればできる ● combineOrAccumlateはデフォルトでないので自作が必要 combineOr Accumulate List User CreateUserError Request User CreateUserError Request User CreateUserError Request List User User User CreateUserError CreateUserError 3つの内2つ失敗した

Slide 25

Slide 25 text

© 2024 Loglass Inc. fun bulkCreateUser( requests: List, ): Result, List> { val resultUserList: List> = requests.map { createUser(it) } val userListResult: Result, List> = resultUserList.combineOrAccumulate() return userListResult }

Slide 26

Slide 26 text

© 2024 Loglass Inc. import com.github.michaelbull.result.partition // partitionメソッドを使って意外と簡単に自作が可能 fun List>.combineOrAccumulate(): Result, List> { val (values, errors) = this.partition() return if (errors.isEmpty()) { Ok(values) } else { Err(errors) } }

Slide 27

Slide 27 text

© 2024 Loglass Inc. kotlin-resultの処理の合成まとめ ● 処理を「直列」に合成したい(成功したら次へ) ○ andThen、binding、flatMapを使う ● 処理を「並列」に合成したい(お互いの結果に依存しない) ○ zip, combineを使う ● 処理を「並列」に合成したいかつエラーをまとめたい ○ zipOrAccumulateを使う。(combineOrAccumulateは拡張が必要)

Slide 28

Slide 28 text

© 2024 Loglass Inc. ちなみに初コントリビュートはzipOrAccumulateです

Slide 29

Slide 29 text

© 2024 Loglass Inc. Railway Oriented Programming & オニオンアーキテクチャ - どの層でResultを使うの? Domain Application Presentation Infrastructure DB

Slide 30

Slide 30 text

© 2024 Loglass Inc. Railway Oriented Programming & オニオンアーキテクチャ - どの層でResultを使うの? Domain Presentation Infrastructure DB Application

Slide 31

Slide 31 text

© 2024 Loglass Inc. Railway Oriented Programming & オニオンアーキテクチャ - どの層でResultを使うの?→Application層とDomain層で使う Domain Presentation Infrastructure Application 異常値は 基本例外を投げて ハンドリング しない App層からの エラーを 例外に変換 メソッドは基本 Resultを返す Domain、 Repositoryの 処理を呼び出し、 処理を合成

Slide 32

Slide 32 text

© 2024 Loglass Inc. Domain層 - 基本Resultを返す class User(...) { companion object { fun validateAndCreate( userName: String, email: String, ): Result {...} } }

Slide 33

Slide 33 text

© 2024 Loglass Inc. Application層 - Resultを返す処理を結合 class RegisterUserUseCase( private val userRepository: UserRepository, private val createUserNotificationEmailSender: CreateUserNotificationEmailSender, ) { fun execute( param: RegisterUserDto, ): Result { return User.validateAndCreate(param.userName, param.email) .mapError { error -> ValidateAndCreateUserUseCaseError(error) }.andThen { createdUser -> userRepository.insert(createdUser) createUserNotificationEmailSender .send(createdUser) .mapError { error -> SendCreateUserNotificationEmailUseCaseError(error) } } } }

Slide 34

Slide 34 text

© 2024 Loglass Inc. Application層 - Resultを返す処理を結合 Domain層処理① Domain層処理② 保存処理 エラー値 成功値 成功値 エラー値 Unit andThen andThen データ取得 データ リクエスト

Slide 35

Slide 35 text

© 2024 Loglass Inc. Presentation層 - Resultのエラーを 例外に変換 class UserController( private val registerUserUseCase: RegisterUserUseCase, ) { fun registerUser( request: RegisterUserRequest ): String { val dto = request.toDto() val result = registerUserUseCase.execute(dto) return result .map { "OK" } .getOrThrow { error -> error.toException() } } } fun RegisterUserUseCaseError.toException(): BadRequestException { return when (this) { is RegisterUserUseCaseError.ValidateAndCreateUserUseCaseError -> … is RegisterUserUseCaseError.SendCreateUserNotificationEmailUseCaseError -> … // ... } }

Slide 36

Slide 36 text

© 2024 Loglass Inc. Infrastructure層 - 基本Resultを返さない - 基本自力復帰できないエラーが多いため - 復帰できるエラーがある場合は返す - 例)メールの送信エラーなど // Domain層 interface UserRepository { fun insert(user: User) } // Infrastructure層(Domain層のInterfaceを実装) class UserJDBCRepository(): UserRepository { override fun insert(user: User) { // ユーザー登録処理 … } }

Slide 37

Slide 37 text

© 2024 Loglass Inc. 補足)ユーザーが自力復帰できないエラーはResultで管理しない - 500になるようなエラーは基本ハンドリングしない Domain層処理① Domain層処理② 保存処理 エラー値 成功値 成功値 エラー値 Unit データ取得 データ リクエスト 500エラー (データ不整合) 500エラー (DB落ちてた)

Slide 38

Slide 38 text

© 2024 Loglass Inc. まとめ: Railway Oriented Programming & オニオンアーキテクチャ - どの層でResultを使うの?→Application層とDomain層で使う Domain Presentation Infrastructure Application 異常値は 基本例外を投げて ハンドリング しない App層からの エラーを 例外に変換 メソッドは基本 Resultを返す Domain、 Repositoryの 処理を呼び出し、 処理を合成

Slide 39

Slide 39 text

© 2024 Loglass Inc. しかし一つ問題があります

Slide 40

Slide 40 text

© 2024 Loglass Inc. エラー時に自動でロールバックされない問題 - Application層がResultを返すと、例外を投げないためRDBが自動でロールバック しない - そのため現状ログラスではApp層でエラー→例外への変換をしている - しかし処理を統合する責務を持つApp層がResultを返せないと価値が十分に引き出せない

Slide 41

Slide 41 text

© 2024 Loglass Inc. エラー時に自動でロールバックされない問題 class RegisterUserUseCase( private val userRepository: UserRepository, private val createUserNotificationEmailSender: CreateUserNotificationEmailSender, ) { // SpringBootのアノテーションで例外投げられたら自動ロールバック @Transactional fun execute( param: RegisterUserDto, ): Unit { … } }

Slide 42

Slide 42 text

© 2024 Loglass Inc. 実験: カスタムアノテーション @Retention(AnnotationRetention.RUNTIME) @Transactional(propagation = Propagation.REQUIRED) annotation class ResultTransactional @Component @Aspect class ResultTransactionalAspect { @Around("@annotation(ResultTransactional)") fun handleResultTransaction(joinPoint: ProceedingJoinPoint): Any? { val proceeded = joinPoint.proceed() val isErr = proceeded.javaClass.name == "com.github.michaelbull.result.Failure" if (isErr) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() } return proceeded } } - @ResultTransactional というアノテーションを自作 ⚠ 動くことは確認しましたが、 ログラスではまだ実運用していません

Slide 43

Slide 43 text

© 2024 Loglass Inc. 使ってみる class RegisterUserUseCase( private val userRepository: UserRepository, private val createUserNotificationEmailSender: CreateUserNotificationEmailSender, ) { // 返り値がErrなら自動でロールバック @ResultTransactional fun execute( param: RegisterUserDto, ): Unit { … } }

Slide 44

Slide 44 text

© 2024 Loglass Inc. なんかできそう!(続報をお待ちください)

Slide 45

Slide 45 text

© 2024 Loglass Inc. 今日のコード https://github.com/YuitoSato/kotlin-spring-boot-jooq

Slide 46

Slide 46 text

© 2024 Loglass Inc.