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
Composable Architecture
Search
ry-itto
June 01, 2020
Programming
810
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Composable Architecture
uzimaru 生誕LT
ry-itto
June 01, 2020
More Decks by ry-itto
See All by ry-itto
決定版!?OSSアプリプロジェクトでのBeta版アプリ配布の方法「Xcode Cloud + TestFlight」
ryitto
0
410
CA.swift#14
ryitto
3
5.9k
swift-argument-parserで 簡単 CLI ツール作り
ryitto
1
170
Data Essentials in SwiftUI
ryitto
1
540
CollectionViewの 新しいレイアウトの作り方
ryitto
0
79
Swift5.1 SwiftUI
ryitto
0
140
Other Decks in Programming
See All in Programming
PostgreSQL 18で考えるUUID主キー
kazuhiro1982
0
340
Terraform標準の組織で AWS CDKをどう使うか
mu7889yoon
1
350
AI時代の仕事技芸論〜ソフトウェア開発で「遊ぶように働く」職人的熟達のすすめ(スクフェス仙台 2026バージョン)
kuranuki
0
710
act1-costs.pdf
sumedhbala
0
250
『コードを書く以外の』エンジニアリング〜課金基盤移行プロジェクト推進のためのTips4選
yuriko1211
0
540
PHP Application における Kubernetes 内 gRPC 通信
ganchiku
0
530
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
190
今さら聞けない .NET CLI
htkym
0
120
PHP に部分適用が来るぞ!……ところで何それ?おいしいの? #phpcon / phpcon-2026
shogogg
0
350
Apache Hive: そしてCloud Native Lakehouseへ
okumin
1
170
任せる範囲はこう広がった / How the Scope of AI Delegation Has Expanded
nrslib
1
270
Hatena Engineer Seminar #37「言語モデルの活用に関する研究」
slashnephy
0
540
Featured
See All Featured
B2B Lead Gen: Tactics, Traps & Triumph
marketingsoph
0
180
Lightning talk: Run Django tests with GitHub Actions
sabderemane
0
220
svc-hook: hooking system calls on ARM64 by binary rewriting
retrage
2
380
The Language of Interfaces
destraynor
162
27k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
910
Winning Ecommerce Organic Search in an AI Era - #searchnstuff2025
aleyda
1
2.1k
Ten Tips & Tricks for a 🌱 transition
stuffmc
0
150
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
17k
Jess Joyce - The Pitfalls of Following Frameworks
techseoconnect
PRO
1
310
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
34
2.8k
Game over? The fight for quality and originality in the time of robots
wayneb77
1
230
The SEO Collaboration Effect
kristinabergwall1
1
510
Transcript
Composable Architecture ҏ౻྇ (@ry_itto)ɹ2020/06/01
ࣗݾհ • ໊લɿҏ౻྇ • ֶߍɿձେֶɾֶ෦4 • iOS ΤϯδχΞ • Χϐόϥ͕͖
Twitter/GitHub Facebook
Composable Architecture ͱʁ SwiftUI ͷొʹΑͬͯߟ͑ΒΕͨΞʔΩςΫνϟɻ Point-Free ͱ͍͏ Swift ʹؔ͢ΔಈըΛओʹग़͍ͯ͠ΔαΠ τͰհ͞Ε͍ͯΔɻ
ݱࡏ Part1 ͔Β Part4 ·Ͱհಈը͕ग़͍ͯΔɻ Part1, 2 ͰΞʔΩςΫνϟͷઆ໌ Part3,4 Ͱςετؔ࿈
ͱͷؔ࿈ The Composable Architecture was built on a foundation of
ideas started by other libraries, in particular Elm and Redux.
ͱͷؔ࿈
ͪΐͬͱৄࡉʹ Composable Architecture ͷॏཁͳ ֓೦ • State management • Composition
• Side effects • Testing • Ergonomics
ͪΐͬͱৄࡉʹ Redux ͱͷࠩҟ Redux ෭࡞༻Λॲཧ͢Δ࣌ʹͲͷΑ͏ʹ͢Δ ͖͔͕ΞʔΩςΫνϟϨϕϧͰఆٛ͞ Ε͍ͯͳ͍ Composable Architecture ෭࡞༻Λॲཧ͢Δ࣌ʹ
Effect ܕΛ Reducer ͔Βඞͣฦ͢Α͏ʹઃܭ͞ Ε͍ͯΔ Composable Architecture ͷํ͕ ݻ੍͍Λ͍࣋ͬͯΔ
ͪΐͬͱৄࡉʹ Elm ͱͷࠩҟ Elm Cmd ͰͲΜͳछྨͷ࡞༻Λߦ͑Δ੍͔ ޚ͍ͯ͠Δ Composable Architecture Combine
ͷ Publisher protocol ʹ४ ڌ͍ͯ͠ΔͨΊɺ༷ʑͳछྨͷ࡞༻ ΛΤεέʔϓϋονʢ༻ʣͰ͖Δ Composable Architecture ͷํ͕ ؇੍͍Λ͍࣋ͬͯΔ
ͪΐͬͱৄࡉʹ ଞͷϥΠϒϥϦ (ΞʔΩςΫνϟܥ) ʹͳ͍ಛ Composition • େ͖ΊͳػೳΛΑΓখ͍͞୯Ґ ʹ͢Δ͜ͱ͕Ͱ͖Δ • ΞϓϦ։ൃΛ্͍ͯ͘͠Ͱͷਏ
ΈʢίϯύΠϧ͕͍࣌ؒʣΛ ϞδϡʔϧԽ͢Δ͜ͱͰղܾ͢ Δ͜ͱ͕Ͱ͖Δ
ͪΐͬͱৄࡉʹ ্࣮བྷΉϥΠϒϥϦ ͷΫϥε/ߏମ • Store<State, Action> Redux Flux ͷ
Store ͱ΄΅ಉ͡ ͷ • Reducer<State, Action, Environment> ૾ʹ͘͠ͳ͍ Reducer Ͱ͢ɻ ଞͱҧͬͯ Environment ܕύϥ ϝʔλʹࢦఆ͠·͢ ※ Reducer ͷఆٛ (inout State, Action, Environment) -> Effect<Action, Never>
ͪΐͬͱৄࡉʹ ࣮͢Δࡍʹ࡞Δͷ • State ΞϓϦͷঢ়ଶ • Action ΞΫγϣϯ • Environment
͜͜ʹAPI ΫϥΠΞϯτΛ࣋ͬͨ ΓɺϝΠϯͷεϨουΛࢦఆͨ͠ Γ͢Δɻ DI ͱ͔Ͱ͖ͬͱΑ͘͏
σϞɾαϯϓϧ༻ʹ࡞ͬͨͷ • uzimaru-evolution (https://github.com/ry-itto/uzimaru-evolution) ͏͡·ΔΞΠίϯͷਐԽ • ComposableGitHubApp (https://github.com/ry-itto-playground/ComposableGitHubApp) GitHub ΫϥΠΞϯτʢϦϙδτϦݕࡧͷΈʣΛ
Composable Architecture Ͱ࣮ͨ͠αϯϓϧΞϓϦ
uzimaru-evolution ػೳ໘ • ը໘Լ෦ͷҹΛԡ͢ͱਐԽ/ୀ ԽͰ͖Δ • ཛͷঢ়ଶͷ࣌ୟ͘ͱਐԽͨ͠Γ ͢Δ ෦ •
΄΅΄΅ Flux/Redux ʹͳͬͯ͠ ·͍ͬͯΔ
struct ContentView: View { let store: Store<EvolutionState, EvolutionAction> var body:
some View { WithViewStore(self.store) { viewStore in ZStack { Color("background") VStack { Text("uzimaru Evolution") Image.init(viewStore.evolution.text) .onTapGesture { viewStore.send(.poke) } HStack(spacing: 30) { Button( action: { viewStore.send(.degenerate) }, label: { Image(systemName: "arrow.left.circle") }) Text(viewStore.evolution.text) Button( action: { viewStore.send(.evolve) }, label: { Image(systemName: "arrow.right.circle") }) } } } } } } uzimaru-evolution View
enum EvolutionAction { case evolve case degenerate case poke }
struct EvolutionState: Equatable { var pokeCount: Int var evolution: Evolution init(pokeCount: Int = 0, evolution: Evolution) { self.pokeCount = pokeCount self.evolution = evolution } } uzimaru-evolution Action / State
struct EvolutionReducerID: Hashable {} typealias EvolutionReducer = Reducer<EvolutionState, EvolutionAction, EvolutionEnvironment>
let evolutionReducer = EvolutionReducer { (state, action, _) in switch action { case .evolve: guard let next = state.evolution.next else { return .cancel(id: EvolutionReducerID()) } state.evolution = next case .degenerate: guard let previous = state.evolution.previous else { return .cancel(id: EvolutionReducerID()) } state.evolution = previous case .poke: if case .v1 = state.evolution { state.pokeCount += 1 if state.pokeCount == 4 { state.pokeCount = 0 return .init(value: .evolve) } } else { return .cancel(id: EvolutionReducerID()) } } return .none } uzimaru-evolution Reducer
struct EvolutionReducerID: Hashable {} typealias EvolutionReducer = Reducer<EvolutionState, EvolutionAction, EvolutionEnvironment>
let evolutionReducer = EvolutionReducer { (state, action, _) in switch action { case .evolve: guard let next = state.evolution.next else { return .cancel(id: EvolutionReducerID()) } state.evolution = next case .degenerate: guard let previous = state.evolution.previous else { return .cancel(id: EvolutionReducerID()) } state.evolution = previous case .poke: if case .v1 = state.evolution { state.pokeCount += 1 if state.pokeCount == 4 { state.pokeCount = 0 return .init(value: .evolve) } } else { return .cancel(id: EvolutionReducerID()) } } return .none } uzimaru-evolution ཛঢ়ଶͷ࣌ʹୟ͘ͱਐԽ͢ Δࡍͷέʔεఆٛ 4ճୟ͍ͨΒผͷΞΫγϣϯ Λୟ͘Α͏ͳܗʹ͍ͯ͠Δ
෭࡞༻ͷશ͘ͳ͍αϯϓϧΞϓϦΛ ࡞ͬͯ͠·ͬͨͷͰ
ComposableGitHubApp ػೳ໘ • ϦϙδτϦݕࡧͯ͠ɺ݁ՌͷϦϙ δτϦ໊Λཏྻ͢Δ͚ͩ ෦ • Composable Architecture Λ͓ͦ
Β͍͍͘ײ͡ʹ༻Ͱ͖͍ͯΔ
struct ContentView: View { let store: Store<AppState, AppAction> var body:
some View { WithViewStore(self.store) { viewStore in VStack { TextField( "ݕࡧϫʔυ", text: viewStore.binding( get: { $0.query }, send: { .queryChanged(text: $0) } ) ) .textFieldStyle(RoundedBorderTextFieldStyle()) List { ForEach(viewStore.repositories, id: \.id) { repository in Text(repository.name) } } }.onAppear { viewStore.send(.load) } } } } ComposableGitHubApp View
enum AppAction { case queryChanged(text: String) case load case repositoriesResponse(Result<[Repository],
Error>) } struct AppEnvironment { let apiClient: GitHubAPIClient let mainQueue: AnySchedulerOf<DispatchQueue> } struct AppState: Equatable { var query: String = "" var repositories: [Repository] = [] static func == (lhs: AppState, rhs: AppState) -> Bool { return lhs.query == rhs.query && lhs.repositories.count == rhs.repositories.count && !(0..<lhs.repositories.count) .contains { lhs.repositories[$0].id != rhs.repositories[$0].id } } } ComposableGitHubApp Action / State / Environment
enum AppAction { case queryChanged(text: String) case load case repositoriesResponse(Result<[Repository],
Error>) } struct AppEnvironment { let apiClient: GitHubAPIClient let mainQueue: AnySchedulerOf<DispatchQueue> } struct AppState: Equatable { var query: String = "" var repositories: [Repository] = [] static func == (lhs: AppState, rhs: AppState) -> Bool { return lhs.query == rhs.query && lhs.repositories.count == rhs.repositories.count && !(0..<lhs.repositories.count) .contains { lhs.repositories[$0].id != rhs.repositories[$0].id } } } ComposableGitHubApp Action / State / Environment લճͷঢ়ଶͱൺֱͯ͠ಉ ͩ͡ͱఆͨ͠߹ʹ ը໘ߋ৽Λ͠ͳ͍Α͏ʹ ͳ͍ͬͯΔ
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment
in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp Reducer
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment
in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp Reducer
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment
in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp APIClient ͷϦϙδτϦݕࡧ ༻ͷϝιουΛݺͿ ฦΓ Effect<[Repository], Error>
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment
in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp ϝΠϯεϨουͰऔಘͨ͠ ΛόΠϯυ RxSwift ͩͱ subscribe(on:) ʹ͋ͨΔ
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment
in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp ࣦഊ͠ͳ͍ Effect ʹม͢ Δ Effect<[Repository], Error> ͔Β Effect<Result<[Repository], Error>, Never> ʹܕม͞ΕΔ
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment
in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp Action ʹมͯ͠ ࣮ߦதͰ ΩϟϯηϧՄೳͳͷͱ ͯ͠ొ͢Δ
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment
in switch action { case .queryChanged(let text): state.query = text return .init(value: .load) case .load: return environment.apiClient .fetchRepositories(state.query) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.repositoriesResponse) .cancellable(id: GitHubAPIClientID(), cancelInFlight: true) case .repositoriesResponse(.success(let repositories)): state.repositories = repositories case .repositoriesResponse(.failure(let e)): assertionFailure(e.localizedDescription) } return .none } ComposableGitHubApp load έʔεͷ࣍ʹೖΔέʔ εɻ ޭ্࣌ɺࣦഊ࣌Լʹ ೖΔɻ
·ͱΊ • Composable Architecture ݁ߏྑͦ͞͏ • SwiftUI ͕ϓϩμΫτͷίʔυʹऔΓࠐ·Ε࢝ΊΔͰ͋Ζ͏དྷҎ ߱ɺଞʹͲΜͳΞʔΩςΫνϟ͕ఏএ͞ΕΔͷ͔ɺָ͠Έ •
ӳޠྗ͕ͬͱཉ͍͠
͋Γ͕ͱ͏͍͟͝·ͨ͠
ੜ͓ΊͰͱ͏