Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Composable Architecture

Composable Architecture

uzimaru 生誕LT

21e251d97f836f77e7dce83fcaf6daff?s=128

ry-itto

June 01, 2020
Tweet

More Decks by ry-itto

Other Decks in Programming

Transcript

  1. Composable Architecture ҏ౻྇໵ (@ry_itto)ɹ2020/06/01

  2. ࣗݾ঺հ • ໊લɿҏ౻྇໵ • ֶߍɿձ௡େֶɾֶ෦4೥ • iOS ΤϯδχΞ • Χϐόϥ͕޷͖

    Twitter/GitHub Facebook
  3. Composable Architecture ͱ͸ʁ SwiftUI ͷొ৔ʹΑͬͯߟ͑ΒΕͨΞʔΩςΫνϟɻ Point-Free ͱ͍͏ Swift ʹؔ͢ΔಈըΛओʹग़͍ͯ͠ΔαΠ τͰ঺հ͞Ε͍ͯΔɻ

    ݱࡏ Part1 ͔Β Part4 ·Ͱ঺հಈը͕ग़͍ͯΔɻ Part1, 2 ͰΞʔΩςΫνϟͷઆ໌ Part3,4 Ͱςετؔ࿈
  4. ͱͷؔ࿈ The Composable Architecture was built on a foundation of

    ideas started by other libraries, in particular Elm and Redux.
  5. ͱͷؔ࿈

  6. ͪΐͬͱৄࡉʹ Composable Architecture ͷॏཁͳ ֓೦ • State management • Composition

    • Side effects • Testing • Ergonomics
  7. ͪΐͬͱৄࡉʹ Redux ͱͷࠩҟ Redux ෭࡞༻Λॲཧ͢Δ࣌ʹͲͷΑ͏ʹ͢Δ΂ ͖͔͕ΞʔΩςΫνϟϨϕϧͰ͸ఆٛ͞ Ε͍ͯͳ͍ Composable Architecture ෭࡞༻Λॲཧ͢Δ࣌ʹ

    Effect ܕΛ Reducer ͔Βඞͣฦ͢Α͏ʹઃܭ͞ Ε͍ͯΔ Composable Architecture ͷํ͕ ݻ੍͍໿Λ͍࣋ͬͯΔ
  8. ͪΐͬͱৄࡉʹ Elm ͱͷࠩҟ Elm Cmd ͰͲΜͳछྨͷ࡞༻Λߦ͑Δ੍͔ ޚ͍ͯ͠Δ Composable Architecture Combine

    ͷ Publisher protocol ʹ४ ڌ͍ͯ͠ΔͨΊɺ༷ʑͳछྨͷ࡞༻ ΛΤεέʔϓϋονʢ࢖༻ʣͰ͖Δ Composable Architecture ͷํ͕ ؇੍͍໿Λ͍࣋ͬͯΔ
  9. ͪΐͬͱৄࡉʹ ଞͷϥΠϒϥϦ (ΞʔΩςΫνϟܥ) ʹͳ͍ಛ௃ Composition • େ͖ΊͳػೳΛΑΓখ͍͞୯Ґ ʹ͢Δ͜ͱ͕Ͱ͖Δ • ΞϓϦ։ൃΛ্͍ͯ͘͠Ͱͷਏ

    ΈʢίϯύΠϧ͕࣌ؒ௕͍ʣΛ ϞδϡʔϧԽ͢Δ͜ͱͰղܾ͢ Δ͜ͱ͕Ͱ͖Δ
  10. ͪΐͬͱৄࡉʹ ࣮૷্བྷΉϥΠϒϥϦ ͷΫϥε/ߏ଄ମ • Store<State, Action> Redux ΍ Flux ͷ

    Store ͱ΄΅ಉ͡ ΋ͷ • Reducer<State, Action, Environment> ૝૾ʹ೉͘͠ͳ͍ Reducer Ͱ͢ɻ ଞͱҧͬͯ Environment ΋ܕύϥ ϝʔλʹࢦఆ͠·͢ ※ Reducer ͷఆٛ
 (inout State, Action, Environment) -> Effect<Action, Never>
  11. ͪΐͬͱৄࡉʹ ࣮૷͢Δࡍʹ࡞Δ΋ͷ • State ΞϓϦͷঢ়ଶ • Action ΞΫγϣϯ • Environment

    ͜͜ʹAPI ΫϥΠΞϯτΛ࣋ͬͨ ΓɺϝΠϯͷεϨουΛࢦఆͨ͠ Γ͢Δɻ DI ͱ͔Ͱ͖ͬͱΑ͘࢖͏
  12. σϞɾαϯϓϧ༻ʹ࡞ͬͨ΋ͷ • uzimaru-evolution (https://github.com/ry-itto/uzimaru-evolution) ͏͡·ΔΞΠίϯͷਐԽ • ComposableGitHubApp (https://github.com/ry-itto-playground/ComposableGitHubApp) GitHub ΫϥΠΞϯτʢϦϙδτϦݕࡧͷΈʣΛ

    Composable Architecture Ͱ࣮૷ͨ͠αϯϓϧΞϓϦ
  13. uzimaru-evolution ػೳ໘ • ը໘Լ෦ͷ໼ҹΛԡ͢ͱਐԽ/ୀ ԽͰ͖Δ • ཛͷঢ়ଶͷ࣌͸ୟ͘ͱਐԽͨ͠Γ ͢Δ ಺෦ •

    ΄΅΄΅ Flux/Redux ʹͳͬͯ͠ ·͍ͬͯΔ
  14. 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
  15. 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
  16. 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
  17. 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ճୟ͍ͨΒผͷΞΫγϣϯ Λୟ͘Α͏ͳܗʹ͍ͯ͠Δ
  18. ෭࡞༻ͷશ͘ͳ͍αϯϓϧΞϓϦΛ ࡞ͬͯ͠·ͬͨͷͰ

  19. ComposableGitHubApp ػೳ໘ • ϦϙδτϦݕࡧͯ͠ɺ݁ՌͷϦϙ δτϦ໊Λཏྻ͢Δ͚ͩ ಺෦ • Composable Architecture Λ͓ͦ

    Β͍͍͘ײ͡ʹ࢖༻Ͱ͖͍ͯΔ
  20. 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
  21. 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
  22. 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 લճͷঢ়ଶͱൺֱͯ͠ಉ ͩ͡ͱ൑ఆͨ͠৔߹ʹ͸ ը໘ߋ৽Λ͠ͳ͍Α͏ʹ ͳ͍ͬͯΔ
  23. 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
  24. 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
  25. 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>
  26. 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:) ʹ͋ͨΔ
  27. 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> ʹܕม׵͞ΕΔ
  28. 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 ʹม׵ͯ͠ ࣮ߦதͰ ΋ΩϟϯηϧՄೳͳ΋ͷͱ ͯ͠ొ࿥͢Δ
  29. 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 έʔεͷ࣍ʹೖΔέʔ εɻ ੒ޭ࣌͸্ɺࣦഊ࣌͸Լʹ ೖΔɻ
  30. ·ͱΊ • Composable Architecture ݁ߏྑͦ͞͏ • SwiftUI ͕ϓϩμΫτͷίʔυʹऔΓࠐ·Ε࢝ΊΔͰ͋Ζ͏དྷ೥Ҏ ߱ɺଞʹͲΜͳΞʔΩςΫνϟ͕ఏএ͞ΕΔͷ͔ɺָ͠Έ •

    ӳޠྗ͕΋ͬͱཉ͍͠
  31. ͋Γ͕ͱ͏͍͟͝·ͨ͠

  32. ஀ੜ೔͓ΊͰͱ͏