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

Efeitos colaterais em apps iOS

Efeitos colaterais em apps iOS

Código que lida apenas com funções puras e valores imutáveis em vez de objetos complexos pode ser executado com a certeza de que só o que importa são valores de saída em relação a valores de entrada. Este tipo de código oferece máxima testabilidade, mas vem com uma dúvida fundamental: como escrever aplicações reais dessa forma, sem comunicação com a Internet ou sem persistência local?

Nesta palestra vamos conhecer como aplicar conceitos de programação funcional na vida real, isolando completamente o gerenciamento de efeitos colaterais como I/O ou acesso aos sensores do dispositivo e solucionando problemas que existem hoje, deixando a parte impura da aplicação com pouca ou nenhuma lógica.

Avatar for Fellipe Caetano

Fellipe Caetano

November 10, 2018
Tweet

More Decks by Fellipe Caetano

Other Decks in Programming

Transcript

  1. final class LoginViewController: UIViewController { private let loginService = Environment.current.loginService

    // ... override func viewDidLoad() { super.viewDidLoad() loginButton.addTarget(self, action: #selector(performLogin(_:)), for: .touchUpInside ) } @objc private func performLogin(_ sender: UIButton) { let validation = LoginValidation( login: loginTextField.text, password: passwordTextField.text ) switch validation { case let .success(login, password): activityIndicator.startAnimating() loginService.perform(login: login, password: password) { [weak self] result in self?.interpret(result: result) self?.activityIndicator.stopAnimating() } case let .failed(error): interpret(validationError: error) } } }
  2. Existem algumas abordagens possíveis para testar o código deste view

    controller. 
 Dentre elas: 1. Em isolamento 2. De forma integrada
  3. Existem algumas abordagens possíveis para testar o código deste view

    controller. 
 Dentre elas: 1. Em isolamento 2. De forma integrada
  4. final class LoginViewController: UIViewController { private let loginService = Environment.current.loginService

    // ... override func viewDidLoad() { super.viewDidLoad() loginButton.addTarget(self, action: #selector(performLogin(_:)), for: .touchUpInside ) } @objc private func performLogin(_ sender: UIButton) { let validation = LoginValidation( login: loginTextField.text, password: passwordTextField.text ) switch validation { case let .success(login, password): activityIndicator.startAnimating() loginService.perform(login: login, password: password) { [weak self] result in self?.interpret(result: result) self?.activityIndicator.stopAnimating() } case let .failed(error): interpret(validationError: error) } } }
  5. struct Environment { private static var stack: [Environment] = [.default]

    static var current: Environment { return stack.last! } private static var `default`: Environment { return .init(loginService: LoginService()) } static func push(loginService: LoginServiceProtocol) { stack.append(.init(loginService: loginService)) } @discardableResult static func pop() -> Environment? { return stack.popLast() } let loginService: LoginServiceProtocol }
  6. final class LoginViewControllerTests: XCTestCase { func testLoginInCaseOfSuccess() { let loginService

    = StubLoginService(result: .success(/* ... */)) Environment.push(loginService: loginService) let loginViewController = LoginViewController() XCTAssert(loginViewController.view != nil) loginViewController.loginTextField = /* ... */ loginViewController.passwordTextField = /* ... */ loginViewController.loginButton.sendActions(for: .touchUpInside) XCTAssertFalse(loginViewController.activityIndicator.isAnimating) Environment.pop() } }
  7. Existem algumas abordagens possíveis para testar o código deste view

    controller. 
 Dentre elas: 1. Em isolamento 2. De forma integrada
  8. ?

  9. final class LoginViewController: UIViewController { private let loginService = Environment.current.loginService

    // ... override func viewDidLoad() { super.viewDidLoad() loginButton.addTarget(self, action: #selector(performLogin(_:)), for: .touchUpInside ) } @objc private func performLogin(_ sender: UIButton) { let validation = LoginValidation( login: loginTextField.text, password: passwordTextField.text ) switch validation { case let .success(login, password): activityIndicator.startAnimating() loginService.perform(login: login, password: password) { [weak self] result in self?.interpret(result: result) self?.activityIndicator.stopAnimating() } case let .failed(error): interpret(validationError: error) } } }
  10. final class LoginViewController: UIViewController { private let dispatch: (Action) ->

    Void init (dispatch: @escaping (Action) -> Void) { super.init(nibName: nil, bundle: nil) self.dispatch = dispatch } // ... override func viewDidLoad() { super.viewDidLoad() loginButton.addTarget(self, action: #selector(performLogin(_:)), for: .touchUpInside ) } @objc private func performLogin(_ sender: UIButton) { let validation = LoginValidation( login: loginTextField.text, password: passwordTextField.text ) switch validation { case let .success(login, password): dispatch(LoginAction.perform(login: login, password: password)) case let .failed(error): interpret(validationError: error) } } }
  11. func LoginReducer(state: LoginState, action: LoginAction) -> LoginState { var newState

    = state switch action { case .perform: newState.isPerforming = true case let .success(user): newState.isPerforming = false newState.loggedInUser = user newState.error = nil case let .failure(error): newState.isPerforming = false newState.loggedInUser = nil newState.error = error } return newState }
  12. func LoginMiddleware() -> Middleware<LoginState, LoginAction> { return { getState, dispatch

    in { next in { action in next(action) guard case let LoginAction.perform(login, password) = action else { return } let loginService = Environment.current.loginService loginService.perform(login: login, password: password) { result in switch result { case let .success(user): dispatch(LoginAction.success(user)) case let .failure(error): dispatch(LoginAction.failure(error)) } } }}} }
  13. // Um nível acima do LoginViewController private let store: Store<LoginState,

    LoginAction> private var subscriptionToken: SubscriptionToken? // ... subscriptionToken = store.subscribe { state in loginViewController.render(state: state) } final class LoginViewController: UIViewController { // ... func render(state: LoginState) { if state.isPerforming { activityIndicator.startAnimating() } else { activityIndicator.stopAnimating() } } }
  14. func LoginReducer(state: LoginState, action: LoginAction) -> LoginState { var newState

    = state switch action { case .perform: newState.isPerforming = true case let .success(user): newState.isPerforming = false newState.loggedInUser = user newState.error = nil case let .failure(error): newState.isPerforming = false newState.loggedInUser = nil newState.error = error } return newState }
  15. func LoginMiddleware() -> Middleware<LoginState, LoginAction> { return { getState, dispatch

    in { next in { action in next(action) guard case let LoginAction.perform(login, password) = action else { return } let loginService = Environment.current.loginService loginService.perform(login: login, password: password) { result in switch result { case let .success(user): dispatch(LoginAction.success(user)) case let .failure(error): dispatch(LoginAction.failure(error)) } } }}} }
  16. - Caminhos de execução
 - Dependências
 + Decisões
 
 Tornam-se

    mais simples de testar usando métodos como testes de UI e snapshots
  17. - Caminhos de execução
 - Dependências
 + Decisões
 
 Decisões

    de apresentação podem ser encapsuladas em camadas intermediárias (e. g. view models)
  18. Fellipe Caetano
 
 Especialista em desenvolvimento iOS com 7+ de

    experiência implementando aplicações de nível enterprise para diversos segmentos.
 
 Atua como líder técnico de desenvolvimento iOS na Sympla. [email protected] fellipecaetano fellipecaetano_