Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Railway Oriented Programming を オニオンアーキテクチャに適用する...
Search
YuitoSato
October 25, 2024
Technology
1.6k
5
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Railway Oriented Programming を オニオンアーキテクチャに適用する by kotlin-result / Railway Oriented Programming in Onion Architecture by kotlin-result
YuitoSato
October 25, 2024
More Decks by YuitoSato
See All by YuitoSato
「規約、知識、オペレーション」から考える中規模以上の開発組織のCursorルールの 考え方・育て方 / Cursor Rules for Coding Styles, Domain Knowledges and Operations
yuitosato
9
9.3k
大AI時代で輝くために今こそドメインにディープダイブしよう / Deep Dive into Domain in AI-Agent-Era
yuitosato
2
5k
50人の組織でAIエージェントを使う文化を作るためには / How to Create a Culture of Using AI Agents in a 50-Person Organization
yuitosato
6
9.3k
リファクタリングへの耐性が高いモデルベースの統合テストの紹介 / Model-Base Integration Test for Refactoring
yuitosato
7
4k
Expressing Business Logic with Types: Functional DDD for OOP
yuitosato
1
220
ビジネスロジックを「型」で表現するOOPのための関数型DDD / Functional And Type-Safe DDD for OOP
yuitosato
44
32k
Java21とKotlinの代数的データ型 & パターンマッチの紹介と本当に嬉しい使い方 / Algebraic Data Type in Java and Kotlin: Happy Use of Pattern Match
yuitosato
14
5.8k
ログラスの継続的ライブラリアップデートのWhyとHow / Why and How to Update Libraries Continuously in Loglass
yuitosato
0
550
リプレイス「後」が大事!Reactフルリプレイスから2年で良かったこと・その後大事なこと / The Important Point After The Framework Replacement
yuitosato
3
1.2k
Other Decks in Technology
See All in Technology
生成 AI × MCP で切り拓く次世代 SRE!自律型運用への挑戦と開発者体験の進化
_awache
0
170
Databricks における 生成AIガバナンスの実践
taka_aki
1
350
EventBridge Connection
_kensh
5
650
AI フレンドリーなエラー監視を TypeScript で実現する
shinyaigeek
2
270
Agentic ERPをどう設計するか ー 受発注エージェントを動かす、現場の知見と設計思想ー
recerqainc
1
1.9k
noUncheckedIndexedAccess、3時間、1万円。 / noUncheckedIndexedAccess, 3 Hours, 10,000 JPY.
kaonavi
1
330
チームで実践する AI-DLC 思考の軌跡を残すチェックポイント設計
belongadmin
0
3k
もりもり新機能を一挙紹介! AgentCoreに入門して、AWS上にAIエージェントを構築しよう
minorun365
PRO
6
850
社内 AI エージェント Synapse と セマンティックレイヤーの育て方
hiroakis
0
390
運用を見据えたAIエージェント設計実践
amacbee
1
3.2k
作って終わりにしない タイミーのセマンティックレイヤー育成の現在地
chanyou0311
0
490
protovalidate-es を導入してみた
bengo4com
0
160
Featured
See All Featured
Building Experiences: Design Systems, User Experience, and Full Site Editing
marktimemedia
0
520
Mobile First: as difficult as doing things right
swwweet
225
10k
Statistics for Hackers
jakevdp
799
230k
Darren the Foodie - Storyboard
khoart
PRO
3
3.4k
Odyssey Design
rkendrick25
PRO
2
690
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.2k
A better future with KSS
kneath
240
18k
16th Malabo Montpellier Forum Presentation
akademiya2063
PRO
0
140
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
1.9k
Agile that works and the tools we love
rasmusluckow
331
21k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
10k
Product Roadmaps are Hard
iamctodd
PRO
55
12k
Transcript
© 2024 Loglass Inc. 2024.10.25 株式会社ログラス 佐藤有斗 Railway Oriented Programming
を オニオンアーキテクチャに適用する by kotlin-result
© 2024 Loglass Inc. 佐藤 有斗(ゆいと) 株式会社ログラス プロダクト開発部 エンジニア 株式会社ログラスに2020年12月に入社。
主に新規事業の立ち上げを担当。得意分野は型と自動テスト。 KotlinでたまにOSSを作ったり、他のOSSにコントリビュート したりしてます。 Yuito Sato
© 2024 Loglass Inc. 宣伝: n月刊ラムダノートという技術情報誌に記事を寄稿しました! ご興味があればぜひ! n月刊ラムダノート Vol.4, No.3(2024)
https://www.lambdanote.com/product s/n-vol-4-no-3-2024
© 2024 Loglass Inc. ログラスとは ログラスについて 企業価値を向上する 経営管理クラウド
© 2024 Loglass Inc. ログラスについて 企業価値を向上する 経営管理クラウド
© 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/
© 2024 Loglass Inc. kotlin-result • KotlinでRailway Oriented Programmingができるライブラリ Result<out
V, out E> // 例)文字列からメモというモデルを作成する関数の型 (String) -> Result<Memo, CreateMemoError> 参考: kotlin-result https://github.com/michaelbull/kotlin-result
© 2024 Loglass Inc. 使用例 fun createMemo(text: String): Result<Memo, CreateMemoError>
{ return if (text.length <= 140) { Ok(Memo(text)) } else { Err(CreateMemoError()) } }
© 2024 Loglass Inc. 正常ケース 2つのレール 異常ケース
© 2024 Loglass Inc. String Memo 正常ケース String Memo
© 2024 Loglass Inc. String Memo 異常ケース String CreateMemoError 文字列が140字を
超えてる!
© 2024 Loglass Inc. Railway Oriented Programming の良いとこ • 呼び元でエラーハンドリングを強制できる
◦ 検査例外がないKotlinだと例外ハンドリングを強制できない ◦ 標準のResult型はハンドリングする例外を指定できない • 複雑な処理を制御しやすい ◦ 処理の合成がしやすい(直列と並列の処理の合成)
© 2024 Loglass Inc. Railway Oriented Programming の良いとこ • 呼び元でエラーハンドリングを強制できる
◦ 検査例外がないKotlinだと例外ハンドリングを強制できない ◦ 標準のResult型はハンドリングする例外を指定できない • 複雑な処理を制御しやすい ◦ 処理の合成がしやすい(直列と並列の処理の合成) 時間の都合上 今日はこちらのみ
© 2024 Loglass Inc. Railway Oriented Programming で処理を直列に合成する • ユーザ作成(バリデーション→ユーザを作成→通知メールを送信)
バリデーション ユーザ作成 通知メール送信 Request ValidateRegister UserRequestError Validated Request User CreateUserError Unit SendEmailError
© 2024 Loglass Inc. // バリデーション fun validateRegisterUserRequest( request: RegisterUserRequest,
): Result<ValidatedRegisterUserRequest, ValidateRegisterUserRequestError> { ... } // ユーザー作成 fun createUser( request: ValidatedRegisterUserRequest, ): Result<User, CreateUserError> { ... } // メールを送信 fun sendCreateUserNotificationEmail( createdUser: User, ): Result<Unit, SendCreateUserNotificationEmailError> { ... }
© 2024 Loglass Inc. Railway Oriented Programming で処理を直列に合成する • andThenで処理を合成(binding,
flatMapも同じことが可能) バリデーション ユーザ作成 通知メール送信 Request ValidateRegister UserRequestError Validated Request User CreateUserError Unit SendEmailError andThen andThen
© 2024 Loglass Inc. // ユーザー登録処理。各処理を連結 fun registerUser( request: RegisterUserRequest,
): Result<User, RegisterUserError> { return validateCreateUserRequest(request) // バリデーション .mapError { error -> ... } .andThen { validatedRequest -> createUser(validatedRequest).mapError { error -> ... } // ユーザー作成 } .andThen { createdUser -> sendCreateUserNotificationEmail(createdUser).mapError { error -> ... } // 通知メール送信 } }
© 2024 Loglass Inc. Railway Oriented Programming で処理を並列に合成する • zipで処理を合成
zip CreateUserError UseName CreateUserError String UserName, Email Email CreateUserError String 一つ目のエラーを返す User
© 2024 Loglass Inc. fun validateAndCreate(userName: String, email: String): Result<User,
CreateUserError> { return zip( { UserName.validateAndCreate(userName) }, { Email.validateAndCreate(email) }, ) { validatedUserName, validatedEmail -> User( id = UserId.generate(), userName = validatedUserName, email = validatedEmail, ) } }
© 2024 Loglass Inc. • zipOrAccumulateで処理を合成 zipOrAccumulate List<CreateUserError> UseName CreateUserError
String Email CreateUserError String zipOrAccumulateでエラーをまとめて返す UserName, Email User
© 2024 Loglass Inc. fun validateAndCreate(userName: String, email: String): Result<User,
List<CreateUserError>> { return zipOrAccumulate( { UserName.validateAndCreate(userName) }, { Email.validateAndCreate(email) }, ) { validatedUserName, validatedEmail -> User( id = UserId.generate(), userName = validatedUserName, email = validatedEmail, ) } }
© 2024 Loglass Inc. List<Result<V, E>>を並列に処理する • いわゆる一括処理はcombineで処理を並列に合成できる combine CreateUserError
User CreateUserError Request List<User> User CreateUserError Request User CreateUserError Request User User User 一つ目のエラーを返す
© 2024 Loglass Inc. // ユーザー作成処理を並列に合成 fun createUsers( request: ValidatedRegisterUserRequest,
): Result<List<User>, CreateUserError> { val resultUserList: List<Result<User, CreateUserError>> = requests.map { createUser(it) } val userListResult: Result<List<User>, CreateUserError> = resultUserList.combine() return userListResult }
© 2024 Loglass Inc. エラーをまとめて返すなどもカスタムすればできる • combineOrAccumlateはデフォルトでないので自作が必要 combineOr Accumulate List<CreateUserError>
User CreateUserError Request User CreateUserError Request User CreateUserError Request List<User> User User User CreateUserError CreateUserError 3つの内2つ失敗した
© 2024 Loglass Inc. fun bulkCreateUser( requests: List<ValidatedRegisterUserRequest>, ): Result<List<User>,
List<CreateUserError>> { val resultUserList: List<Result<User, CreateUserError>> = requests.map { createUser(it) } val userListResult: Result<List<User>, List<CreateUserError>> = resultUserList.combineOrAccumulate() return userListResult }
© 2024 Loglass Inc. import com.github.michaelbull.result.partition // partitionメソッドを使って意外と簡単に自作が可能 fun <V,
E> List<Result<V, E>>.combineOrAccumulate(): Result<List<V>, List<E>> { val (values, errors) = this.partition() return if (errors.isEmpty()) { Ok(values) } else { Err(errors) } }
© 2024 Loglass Inc. kotlin-resultの処理の合成まとめ • 処理を「直列」に合成したい(成功したら次へ) ◦ andThen、binding、flatMapを使う •
処理を「並列」に合成したい(お互いの結果に依存しない) ◦ zip, combineを使う • 処理を「並列」に合成したいかつエラーをまとめたい ◦ zipOrAccumulateを使う。(combineOrAccumulateは拡張が必要)
© 2024 Loglass Inc. ちなみに初コントリビュートはzipOrAccumulateです
© 2024 Loglass Inc. Railway Oriented Programming & オニオンアーキテクチャ -
どの層でResultを使うの? Domain Application Presentation Infrastructure DB
© 2024 Loglass Inc. Railway Oriented Programming & オニオンアーキテクチャ -
どの層でResultを使うの? Domain Presentation Infrastructure DB Application
© 2024 Loglass Inc. Railway Oriented Programming & オニオンアーキテクチャ -
どの層でResultを使うの?→Application層とDomain層で使う Domain Presentation Infrastructure Application 異常値は 基本例外を投げて ハンドリング しない App層からの エラーを 例外に変換 メソッドは基本 Resultを返す Domain、 Repositoryの 処理を呼び出し、 処理を合成
© 2024 Loglass Inc. Domain層 - 基本Resultを返す class User(...) {
companion object { fun validateAndCreate( userName: String, email: String, ): Result<User, ValidateAndCreateUserError> {...} } }
© 2024 Loglass Inc. Application層 - Resultを返す処理を結合 class RegisterUserUseCase( private
val userRepository: UserRepository, private val createUserNotificationEmailSender: CreateUserNotificationEmailSender, ) { fun execute( param: RegisterUserDto, ): Result<Unit, RegisterUserUseCaseError> { return User.validateAndCreate(param.userName, param.email) .mapError { error -> ValidateAndCreateUserUseCaseError(error) }.andThen { createdUser -> userRepository.insert(createdUser) createUserNotificationEmailSender .send(createdUser) .mapError { error -> SendCreateUserNotificationEmailUseCaseError(error) } } } }
© 2024 Loglass Inc. Application層 - Resultを返す処理を結合 Domain層処理① Domain層処理② 保存処理
エラー値 成功値 成功値 エラー値 Unit andThen andThen データ取得 データ リクエスト
© 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 -> … // ... } }
© 2024 Loglass Inc. Infrastructure層 - 基本Resultを返さない - 基本自力復帰できないエラーが多いため -
復帰できるエラーがある場合は返す - 例)メールの送信エラーなど // Domain層 interface UserRepository { fun insert(user: User) } // Infrastructure層(Domain層のInterfaceを実装) class UserJDBCRepository(): UserRepository { override fun insert(user: User) { // ユーザー登録処理 … } }
© 2024 Loglass Inc. 補足)ユーザーが自力復帰できないエラーはResultで管理しない - 500になるようなエラーは基本ハンドリングしない Domain層処理① Domain層処理② 保存処理
エラー値 成功値 成功値 エラー値 Unit データ取得 データ リクエスト 500エラー (データ不整合) 500エラー (DB落ちてた)
© 2024 Loglass Inc. まとめ: Railway Oriented Programming & オニオンアーキテクチャ
- どの層でResultを使うの?→Application層とDomain層で使う Domain Presentation Infrastructure Application 異常値は 基本例外を投げて ハンドリング しない App層からの エラーを 例外に変換 メソッドは基本 Resultを返す Domain、 Repositoryの 処理を呼び出し、 処理を合成
© 2024 Loglass Inc. しかし一つ問題があります
© 2024 Loglass Inc. エラー時に自動でロールバックされない問題 - Application層がResultを返すと、例外を投げないためRDBが自動でロールバック しない - そのため現状ログラスではApp層でエラー→例外への変換をしている
- しかし処理を統合する責務を持つApp層がResultを返せないと価値が十分に引き出せない
© 2024 Loglass Inc. エラー時に自動でロールバックされない問題 class RegisterUserUseCase( private val userRepository:
UserRepository, private val createUserNotificationEmailSender: CreateUserNotificationEmailSender, ) { // SpringBootのアノテーションで例外投げられたら自動ロールバック @Transactional fun execute( param: RegisterUserDto, ): Unit { … } }
© 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 というアノテーションを自作 ⚠ 動くことは確認しましたが、 ログラスではまだ実運用していません
© 2024 Loglass Inc. 使ってみる class RegisterUserUseCase( private val userRepository:
UserRepository, private val createUserNotificationEmailSender: CreateUserNotificationEmailSender, ) { // 返り値がErrなら自動でロールバック @ResultTransactional fun execute( param: RegisterUserDto, ): Unit { … } }
© 2024 Loglass Inc. なんかできそう!(続報をお待ちください)
© 2024 Loglass Inc. 今日のコード https://github.com/YuitoSato/kotlin-spring-boot-jooq
© 2024 Loglass Inc.