1. Introdução
No momento, existem muitos artigos sobre VIPER - arquitetura limpa, várias variações das quais ao mesmo tempo se tornaram populares para projetos iOS. Se você não conhece o Viper, pode lê-lo
aqui ,
aqui ou
aqui .
Eu gostaria de falar sobre a alternativa VIPER - Clean Swift. O Clean Swift à primeira vista se parece com o VIPER, no entanto, as diferenças se tornam visíveis após o estudo do princípio de interação entre os módulos. No VIPER, a interação é baseada no Presenter, transfere solicitações de usuário ao Interator para processamento e formata os dados recebidos dele de volta para exibição no View Controller:

No Clean Swift, os módulos principais, como no VIPER, são o View Controller, Interactor, Presenter.

A interação entre eles ocorre em ciclos. A transferência de dados é baseada em protocolos (novamente, da mesma forma que o VIPER), que permite alterações futuras em um dos componentes do sistema para substituí-lo por outro. O processo de interação em geral se parece com o seguinte: o usuário clica no botão, o View Controller cria um objeto com uma descrição e o envia ao Interactor. O Interactor, por sua vez, implementa um cenário específico de acordo com a lógica de negócios, cria um objeto de resultado e o passa para o Presenter. O Presenter forma um objeto com dados formatados para exibição ao usuário e o envia ao View Controller. Vamos dar uma olhada em cada módulo Clean Swift com mais detalhes.
Ver (Ver Controlador)
O View Controller, como no VIPER, executa todas as configurações do VIew, sejam as configurações de fonte de cores, UILabel ou Layout. Portanto, cada UIViewController nessa arquitetura implementa um protocolo de entrada para exibir dados ou responder a ações do usuário.
Interactractor
O Interactor contém toda a lógica de negócios. Ele aceita ações do usuário do controlador, com parâmetros (por exemplo, texto alterado do campo de entrada, pressionando um botão) definido no protocolo de Entrada. Após elaborar a lógica, o Interactor, se necessário, deve transferir os dados para sua preparação para o Presenter antes de exibi-los no ViewController. No entanto, o Interactor aceita apenas solicitações do View como entrada, diferentemente do VIPER, onde essas solicitações passam pelo Presenter.
Apresentador
O Presenter processa os dados para exibição ao usuário. O resultado nesse caso é o protocolo de entrada do ViewController, aqui você pode, por exemplo, alterar o formato do texto, converter o valor da cor de enum para rgb etc.
Trabalhador
Para não complicar desnecessariamente o Interactor e não duplicar os detalhes da lógica de negócios, você pode usar um elemento Worker adicional. Em módulos simples, isso nem sempre é necessário, mas em módulos suficientemente carregados, permite remover algumas tarefas do Interactor. Por exemplo, a lógica da interação com o banco de dados pode ser feita no worker, especialmente se as mesmas consultas ao banco de dados puderem ser usadas em módulos diferentes.
Roteador
O roteador é responsável por transferir dados para outros módulos e transições entre eles. Ele tem um link para o controlador, porque no iOS, infelizmente, os controladores, entre outras coisas, são historicamente responsáveis pelas transições. O uso de segue pode simplificar a inicialização das transições chamando os métodos do roteador em Preparar para segue, porque o roteador sabe como transferir dados e o fará sem nenhum código de loop extra do Interactor / Presenter. Os dados são transferidos usando os protocolos de data warehouse de cada módulo implementado no Interactor. Esses protocolos também limitam a capacidade de acessar dados do módulo interno do roteador.
Modelos
Modelos é uma descrição das estruturas de dados para transferir dados entre os módulos. Cada implementação da função de lógica de negócios tem sua própria descrição dos modelos.
- Solicitação - para enviar uma solicitação do controlador ao interator.
- Resposta - a resposta do interator para transmitir ao apresentador com os dados.
- ViewModel - para transferência de dados em um formulário pronto para exibição no controlador.
Exemplo de implementação
Vamos dar uma olhada mais de perto nesta arquitetura usando um
exemplo simples. Eles serão atendidos pelo aplicativo ContactsBook de forma simplificada, mas suficiente para entender a essência do formulário da arquitetura. O aplicativo inclui uma lista de contatos, além de adicionar e editar contatos.
Um exemplo de um protocolo de entrada:
protocol ContactListDisplayLogic: class { func displayContacts(viewModel: ContactList.ShowContacts.ViewModel) }
Cada controlador contém uma referência a um objeto que implementa o protocolo Interactor de entrada
var interactor: ContactListBusinessLogic?
bem como ao objeto Roteador, que deve implementar a lógica da transferência de dados e da comutação de módulos:
var router: (NSObjectProtocol & ContactListRoutingLogic & ContactListDataPassing)?
Você pode implementar a configuração do módulo em um método privado separado:
private func setup() { let viewController = self let interactor = ContactListInteractor() let presenter = ContactListPresenter() let router = ContactListRouter() viewController.interactor = interactor viewController.router = router interactor.presenter = presenter presenter.viewController = viewController router.viewController = viewController router.dataStore = interactor }
ou crie um singleton do Configurator para remover esse código do controlador (para aqueles que acreditam que o controlador não deve estar envolvido na configuração) e não tente o acesso a partes do módulo no controlador. Não há classe configuradora na exibição do tio Bob e no VIPER clássico. O uso do configurador para o módulo adicionar contato se parece com o seguinte:
override func awakeFromNib() { super.awakeFromNib() AddContactConfigurator.sharedInstance.configure(self) }
O código do configurador contém o único método de configuração absolutamente idêntico ao método de configuração no controlador:
final class AddContactConfigurator { static let sharedInstance = AddContactConfigurator() private init() {} func configure(_ control: AddContactViewController) { let viewController = control let interactor = AddContactInteractor() let presenter = AddContactPresenter() let router = AddContactRouter() viewController.interactor = interactor viewController.router = router interactor.presenter = presenter presenter.viewController = viewController router.viewController = viewController router.dataStore = interactor } }
Outro ponto muito importante na implementação do controlador é o código no método de preparação padrão para segue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let scene = segue.identifier { let selector = NSSelectorFromString("routeTo\(scene)WithSegue:") if let router = router, router.responds(to: selector) { router.perform(selector, with: segue) } } }
Um leitor atento provavelmente percebeu que o roteador também é necessário para implementar o NSObjectProtocol. Isso é feito para que possamos usar os métodos padrão deste protocolo para roteamento ao usar segues. Para dar suporte a esse redirecionamento simples, a nomeação do identificador segue deve corresponder às terminações dos nomes dos métodos do roteador. Por exemplo, para visualizar um contato, existe um segue, que está associado à escolha de uma célula com um contato. Seu identificador é "ViewContact", eis o método correspondente no roteador:
func routeToViewContact(segue: UIStoryboardSegue?)
A solicitação para exibir dados para o Interactor também parece muito simples:
private func fetchContacts() { let request = ContactList.ShowContacts.Request() interactor?.showContacts(request: request) }
Vamos passar para o Interactor. O Interactor implementa o protocolo ContactListDataStore, responsável por armazenar / acessar dados. No nosso caso, isso é apenas uma matriz de contatos, limitada apenas pelo método getter, para mostrar ao roteador a inadmissibilidade de alterá-lo de outros módulos. Um protocolo que implementa a lógica comercial da nossa lista é o seguinte:
func showContacts(request: ContactList.ShowContacts.Request) { let contacts = worker.getContacts() self.contacts = contacts let response = ContactList.ShowContacts.Response(contacts: contacts) presenter?.presentContacts(response: response) }
Ele recebe dados de contato do ContactListWorker. Nesse caso, o trabalhador é responsável por como os dados são baixados. Ele pode recorrer a serviços de terceiros que decidem, por exemplo, coletar dados do cache ou fazer download da rede. Após receber os dados, o Interactor envia uma resposta ao apresentador para se preparar para a exibição, pois esse interator contém um link para o apresentador:
var presenter: ContactListPresentationLogic?
O Presenter implementa apenas um protocolo - o ContactListPresentationLogic, no nosso caso, simplesmente altera forçosamente o nome e o sobrenome do contato, forma o modelo de apresentação DisplayedContact a partir do modelo de dados e passa para o Controller para exibição:
func presentContacts(response: ContactList.ShowContacts.Response) { let mapped = response.contacts.map { ContactList .ShowContacts .ViewModel .DisplayedContact(firstName: $0.firstName.uppercaseFirst, lastName: $0.lastName.uppercaseFirst) } let viewModel = ContactList.ShowContacts.ViewModel(displayedContacts: mapped) viewController?.displayContacts(viewModel: viewModel) }
Depois disso, o ciclo termina e o controlador exibe os dados, implementando o método do protocolo ContactListDisplayLogic:
func displayContacts(viewModel: ContactList.ShowContacts.ViewModel) { displayedContacts = viewModel.displayedContacts tableView.reloadData() }
É assim que os modelos para exibição de contatos se parecem:
enum ShowContacts { struct Request { } struct Response { var contacts: [Contact] } struct ViewModel { struct DisplayedContact { let firstName: String let lastName: String var fullName: String { return firstName + " " + lastName } } var displayedContacts: [DisplayedContact] } }
Nesse caso, a solicitação não contém dados, pois essa é apenas uma lista de contatos geral; no entanto, se, por exemplo, a tela da lista contiver um filtro, o tipo de filtro poderá ser incluído nessa solicitação. O modelo de resposta do Intrecator contém a lista de contatos desejada, o ViewModel também contém uma matriz de dados prontos para exibição - DisplayedContact.
Por que Clean Swift
Considere os prós e os contras dessa arquitetura. Primeiro, o Clean Swift possui modelos de código que facilitam a criação de um módulo. Esses modelos podem ser escritos para muitas arquiteturas, mas quando estão prontos para uso, economizam várias horas do seu tempo.
Em segundo lugar, essa arquitetura, como VIPER, é bem testada; exemplos de testes estão disponíveis no projeto. Como o módulo com o qual a interação ocorre é fácil de substituir por um stub, determinar a funcionalidade de cada módulo usando protocolos permite implementar isso sem dor de cabeça. Se criarmos simultaneamente a lógica de negócios e os testes correspondentes (Interactor, Interactor tests), isso se ajustará bem ao princípio do TDD. Devido ao fato de que a saída e a entrada de cada caso de lógica são definidas pelo protocolo, basta escrever primeiro um teste que determine seu comportamento e depois implementar diretamente a lógica do método.
Em terceiro lugar, o Clean Swift (ao contrário do VIPER) implementa um fluxo unidirecional de processamento de dados e tomada de decisão. Somente um ciclo é sempre executado - Visualizar - Interator - Apresentador - Visualizar, o que também simplifica a refatoração, pois muitas vezes é necessário alterar menos entidades. Devido a isso, projetos com lógica que geralmente muda ou é complementada são mais convenientes para refatorar usando a metodologia Clean Swift. Usando o Clean Swift, você separa as entidades de duas maneiras:
- Isolar componentes declarando protocolos de entrada e saída
- Isole os recursos usando estruturas e encapsulando dados em pedidos / respostas / modelos de interface do usuário separados. Cada recurso tem sua própria lógica e é controlado dentro da estrutura de um processo, sem se cruzar em um módulo com outros recursos.
O Clean Swift não deve ser usado em pequenos projetos sem uma perspectiva de longo prazo, em projetos de protótipo. Por exemplo, é muito caro implementar um aplicativo para o agendamento de uma conferência de desenvolvedores usando essa arquitetura. Projetos de longo prazo, projetos com muita lógica de negócios, pelo contrário, se encaixam bem na estrutura dessa arquitetura. É muito conveniente usar o Clean Swift quando o projeto é implementado para duas plataformas - Mac OS e iOS, ou se planeja portá-lo no futuro.