Desenvolvimento modular ou caminho até lá, não de volta


Como chegamos a uma nova abordagem para trabalhar com módulos no aplicativo RaiffeisenBank iOS.

O problema


Nas aplicações do Raiffeisenbank, cada tela consiste em vários módulos que são tão independentes quanto possível um do outro. "Módulo" chamamos de componente visual que tem sua própria idéia. Ao projetar um aplicativo, é muito importante escrever lógica para que os módulos sejam independentes e possam ser facilmente adicionados ou removidos sem recorrer à refatoração.

Que dificuldades enfrentamos:


Destacando abstração sobre padrões arquiteturais
Já no primeiro estágio do desenvolvimento, ficou claro que não queríamos estar vinculados a um padrão arquitetural específico. O MVC é bom se você precisar exibir uma página com algumas informações. Ao mesmo tempo, a interação com o usuário é mínima ou nenhuma. Por exemplo: a página "sobre a empresa" ou "contrato de usuário". O VIPER é uma boa ferramenta para módulos complexos que têm sua própria lógica de trabalhar com serviços, roteamento e muito mais.

O problema da interação e encapsulamento
Cada padrão arquitetural possui sua própria estrutura de construção e seus próprios protocolos, que impõem restrições ao trabalho com o módulo. Para abstrair o módulo, você precisa destacar as principais interfaces de interação de entrada / saída .

Destacando a lógica de roteamento
Um módulo como unidade visual não deve e não pode saber onde e como é mostrado. Um e o mesmo módulo deve e pode ser implementado como uma unidade independente em qualquer tela ou como uma composição. A responsabilidade por isso não pode ser responsabilizada pelo próprio módulo.

Solução anterior: // Mau negócio


A primeira solução que escrevemos no Objective-C, e foi baseada no NSProxy. O problema de encapsulamento do padrão arquitetural foi resolvido por definição, que foi determinada pelas condições fornecidas, ou seja, a entrada / saída do módulo, que tornou possível proxy de todas as chamadas do módulo para sua entrada e receber mensagens através da saída , se houver.

Foi um passo adiante, mas surgiram novas dificuldades:

  • A interface do proxy não garantiu a implementação do protocolo de entrada ;
  • A saída tinha que ser descrita, mesmo que não fosse necessária;
  • Foi necessário adicionar a propriedade de saída à interface de entrada .

Além do NSProxy, também implementamos o roteamento considerando a ideia do ViperMcFlurry: criamos uma categoria no ViewController , que começou a crescer à medida que apareciam diferentes opções para exibir o módulo na tela. É claro que dividimos a categoria, mas ainda estava longe de ser uma boa solução.

Em geral ... a primeira panqueca é irregular, ficou claro que você precisa resolver o problema de maneira diferente.

Solução: // Final


Percebendo que não havia mais nada com o NSProxy , pegamos marcadores e fomos desenhar. Como resultado, isolamos o protocolo RFModule :

@objc protocol RFModule { var view: ViewController { get } var input: AnyObject? { get } var output: AnyObject? { get set } var transition: Transitioning { get set } } 

Abandonamos propositalmente os tipos associados no nível do protocolo, e havia uma boa razão para isso: naquele momento, 90% do código estava no Objective-C. Interoperabilidade entre módulos ObjC ← → Swift não seria possível.

Para ainda usar genéricos e garantir o uso digitado de módulos, introduzimos a classe Module que satisfaz o protocolo
RFModule :

 final class Module<I: Any, O: Any>: RFModule { public typealias Input = I public typealias Output = O public var setOutput: ((O?) -> Void)? //... public var input: I? { get { return inputObjc as? I} set { inputObjc = newValue as AnyObject } } public var output: O? { get { return outputObjc as? O} set { outputObjc = newValue as AnyObject } } @objc(input) public weak var inputObjc: AnyObject? @objc(moduleOutput) public weak var outputObjc: AnyObject? { didSet{ setOutput?(output) } } } @objc protocol RFModule { var view: ViewController { get } @objc(input) var inputObjc: AnyObject? { get } @objc(moduleOutput) var outputObjc: AnyObject? { get set } var transition: Transitioning { get set } } public extension RFModule { public var input: AnyObject? { return inputObjc } public var output: AnyObject? { get { return outputObjc } set { outputObjc = newValue} } } 

Então, nós temos um módulo digitado. De fato, Swift usa a classe Module e no Objective-C RFModule . Além disso, acabou por ser uma ferramenta conveniente para tipos de trituração no local em que você precisa criar matrizes: por exemplo, TabContainer .

Como o DI para criar o módulo está no escopo do UserStory, e atribuir o valor da saída no local em que será usado não pode descrever um seter simples. "SetOutput" é, em essência, uma definição que, no estágio de atribuição da saída, passará para a pessoa responsável, dependendo da lógica do módulo.

 class SomeViewController: UIViewController, ModuleInput { weak var delegate: ModuleOutput } class Assembly { func someModule() -> Module<ModuleInput, ModuleOutput> { let view = SomeViewController() let module = Module<ModuleInput, ModuleOutput>(view: view, input: view) { [weak view] output in view?.delegate = output } return module } } ... let assembly: Assembly let module = assembly.someModule() module.output = self 

A transição é um protocolo cujas implementações, como o nome indica, são responsáveis ​​pela lógica de mostrar e ocultar o módulo.

 protocol Transitioning { var destination: ViewController? { get } // should be weak func perform(_ completion: (()->())?) // present func reverse(_ completion: (()->())?) // dissmiss } 

Para exibição, é causado - executar , ocultar - reverter . Apesar do fato de haver destino no protocolo e, a princípio, parece que deve haver fonte . De fato, a origem pode não ser e seu tipo nem sempre é o ViewController . Por exemplo, se precisamos que o módulo seja aberto em uma nova janela, esta é Window , e se precisamos incorporar , precisamos AND parent: ViewController AND container: UIView .

 class PresentTransition: Transitioning { weak var source: ViewController? weak var destination: ViewController? ... func perform(_ completion: (()->())?) { source.present(viewController: self.destinaton) } } 

Assim, nos livramos da idéia de escrever extensões no ViewController e descrevemos a lógica de como exibimos nossos módulos em vários objetos. Isso nos deu flexibilidade no roteamento, ou seja, agora podemos mostrar qualquer módulo de forma independente e complexa, e também variar entre como tudo é exibido na tela: na janela Presente, na navegação (pressione a navegação), incorporar, na cortina (capa) .

Isso é tudo?


Há mais uma coisa assustadora até agora. Para a oportunidade de escolher facilmente a maneira como o módulo é exibido e remover essa lógica, pagamos pela perda da capacidade de definir as propriedades da aparência. Por exemplo, se o mostrarmos em Navegação, precisaremos especificar qual cor deve ser a barraTintColor ; ou, se mostrarmos o módulo na cortina, é necessário definir a cor do manipulador .

Até agora, resolvemos esse problema com a aparência não digitada: Qualquer propriedade e Transição ao abrir o módulo levam ao tipo com o qual ele trabalha e, se for bem-sucedido, tira as propriedades necessárias.

Source: https://habr.com/ru/post/pt448206/


All Articles