Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Railway Oriented Programming を オニオンアーキテクチャに適用する...
Search
YuitoSato
October 25, 2024
Technology
1.8k
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
11k
大AI時代で輝くために今こそドメインにディープダイブしよう / Deep Dive into Domain in AI-Agent-Era
yuitosato
2
5.6k
50人の組織でAIエージェントを使う文化を作るためには / How to Create a Culture of Using AI Agents in a 50-Person Organization
yuitosato
6
10k
リファクタリングへの耐性が高いモデルベースの統合テストの紹介 / Model-Base Integration Test for Refactoring
yuitosato
7
4.2k
Expressing Business Logic with Types: Functional DDD for OOP
yuitosato
1
250
ビジネスロジックを「型」で表現するOOPのための関数型DDD / Functional And Type-Safe DDD for OOP
yuitosato
44
33k
Java21とKotlinの代数的データ型 & パターンマッチの紹介と本当に嬉しい使い方 / Algebraic Data Type in Java and Kotlin: Happy Use of Pattern Match
yuitosato
14
5.9k
ログラスの継続的ライブラリアップデートのWhyとHow / Why and How to Update Libraries Continuously in Loglass
yuitosato
0
590
リプレイス「後」が大事!Reactフルリプレイスから2年で良かったこと・その後大事なこと / The Important Point After The Framework Replacement
yuitosato
3
1.2k
Other Decks in Technology
See All in Technology
契約書画像からの情報抽出に特化した視覚接地モデルを育てる話
sansantech
PRO
1
160
継続的なAIコスト最適化
bgpat
1
280
DWH の限界突破! AI エージェント向け爆速リアルタイムデータ基盤 ClickHouse
jozono
1
370
今こそ知りたいAmplifyGen2
mkdev10
2
160
AI駆動開発の時代になったのでトヨタ生産方式から見直す
terurou
6
5.7k
AIは爆速なのに、私が詰まっていた話 ― 音声入力と鳴くマスコットでボトルネックを削る
yama3133
0
590
生成AIを使って「人が」考える技術 ― AI時代の人機共想と実践ノウハウ|UNITT AC2026
ishiirikie
0
580
無理せず続ける、子育てエンジニアの学び
codmoninc
PRO
0
650
V8のArray.prototype.flatを最大約20倍速くするまでと、巨大OSSへの大規模コミットの道のり
riyaamemiya
0
190
Jetson Orin Nanoとk3sとローカルLLMで音声文字起こしシステム作ってみた
y_sera15
0
120
AI時代だからこそ推したい!Gitコマンド再入門
munakata
0
170
事業活動を AI Ready にする攻めと守りのデータエンジニアリング / data-engineering-for-ai-ready-business
pei0804
7
2.5k
Featured
See All Featured
Noah Learner - AI + Me: how we built a GSC Bulk Export data pipeline
techseoconnect
PRO
0
450
Leveraging Curiosity to Care for An Aging Population
cassininazir
1
530
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
300
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
23k
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.9k
<Decoding/> the Language of Devs - We Love SEO 2024
nikkihalliwell
1
350
Raft: Consensus for Rubyists
vanstee
142
7.7k
What’s in a name? Adding method to the madness
productmarketing
PRO
24
4.2k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
254
22k
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.4k
Side Projects
sachag
456
43k
Data-driven link building: lessons from a $708K investment (BrightonSEO talk)
szymonslowik
1
1.4k
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.