
Olá pessoal! Meu nome é Ilya, sou desenvolvedor iOS no Tinkoff.ru. Neste artigo, quero falar sobre como reduzir a duplicação de código na camada de apresentação usando protocolos.
Qual é o problema?
À medida que o projeto cresce, a quantidade de duplicação de código aumenta. Isso não se torna imediatamente aparente e fica difícil corrigir os erros do passado. Percebemos esse problema em nosso projeto e o resolvemos usando uma abordagem, vamos chamá-lo, condicionalmente, de características.
Exemplo de vida
A abordagem pode ser usada com várias soluções arquiteturais diferentes, mas considerarei o uso do VIPER como exemplo.
Considere o método mais comum no roteador - o método que fecha a tela:
func close() { self.transitionHandler.dismiss(animated: true, completion: nil) }
Está presente em muitos roteadores e é melhor escrevê-lo apenas uma vez.
A herança nos ajudaria nisso, mas no futuro, quando tivermos mais e mais classes com métodos desnecessários em nosso aplicativo, ou não conseguiremos criar a classe de que precisamos porque os métodos necessários estão em diferentes classes base, as grandes aparecerão problemas
Como resultado, o projeto se transformará em muitas classes base e classes descendentes com métodos supérfluos. A herança não vai nos ajudar.
O que é melhor do que a herança? Claro que a composição.
Você pode criar uma classe separada para o método que fecha a tela e adicioná-la a cada roteador em que é necessário:
struct CloseRouter { let transitionHandler: UIViewController func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } }
Ainda precisamos declarar esse método no protocolo de entrada do roteador e implementá-lo no próprio roteador:
protocol SomeRouterInput { func close() } class SomeRouter: SomeRouterInput { var transitionHandler: UIViewController! lazy var closeRouter = { CloseRouter(transitionHandler: self. transitionHandler) }() func close() { self.closeRouter.close() } }
Descobriu-se muito código que simplesmente proxies a chamada para o método close.
Preguiçoso Bom programador não vai gostar.
Solução de protocolo
Protocolos vêm para o resgate. Essa é uma ferramenta bastante poderosa que permite implementar a composição e pode conter métodos de implementação em extensão. Assim, podemos criar um protocolo contendo o método close e implementá-lo em extensão.
É assim que ficará:
protocol CloseRouterTrait { var transitionHandler: UIViewController! { get } func close() } extension CloseRouterTrait { func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } }
A questão é: por que a palavra característica aparece no nome do protocolo? É simples - você pode especificar que esse protocolo implementa seus métodos em extensão e deve ser usado como uma mistura com outro tipo para expandir sua funcionalidade.
Agora, vamos ver como será o uso desse protocolo:
class SomeRouter: CloseRouterTrait { var transitionHandler: UIViewController! }
Sim, é tudo. Parece ótimo :). Obtivemos a composição adicionando o protocolo à classe do roteador, não escrevemos uma única linha extra e tivemos a oportunidade de reutilizar o código.
O que é incomum nessa abordagem?
Você já pode ter feito esta pergunta. Usar protocolos como uma característica é bastante comum. A principal diferença é usar essa abordagem como uma solução arquitetural dentro da camada de apresentação. Como qualquer solução arquitetural, deve haver suas próprias regras e recomendações.
Aqui está a minha lista:
- As características não devem armazenar e alterar o estado. Eles só podem ter dependências na forma de serviços etc., que são propriedades de obtenção apenas.
- Os traços não devem ter métodos que não são implementados em extensão, pois isso viola seu conceito
- Os nomes dos métodos no traço devem refletir explicitamente o que fazem, sem estar vinculados ao nome do protocolo. Isso ajudará a evitar colisões de nomes e tornar o código mais claro.
Do VIPER ao MVP
Se você mudar completamente para usar essa abordagem com protocolos, as classes roteador e interator terão algo parecido com isto:
class SomeRouter: CloseRouterTrait, OtherRouterTrait { var transitionHandler: UIViewController! } class SomeInteractor: SomeInteractorTrait { var someService: SomeServiceInput! }
Isso não se aplica a todas as classes; na maioria dos casos, o projeto simplesmente terá roteadores e interadores vazios. Nesse caso, você pode interromper a estrutura do módulo VIPER e alternar suavemente para o MVP adicionando protocolos de impureza ao apresentador.
Algo assim:
class SomePresenter: CloseRouterTrait, OtherRouterTrait, SomeInteractorTrait, OtherInteractorTrait { var transitionHandler: UIViewController! var someService: SomeSericeInput! }
Sim, a capacidade de implementar roteador e interator como dependências é perdida, mas em alguns casos, esse é o caso.
A única desvantagem é transitHandler = UIViewController. E de acordo com as regras do VIPER Presenter, nada deve ser conhecido sobre a camada Visualizar e como ela é implementada usando quais tecnologias. Isso é resolvido neste caso simplesmente - os métodos de transição do UIViewController são "fechados" pelo protocolo, por exemplo, TransitionHandler. Então o Presenter irá interagir com a abstração.
Alterando o comportamento das características
Vamos ver como você pode mudar o comportamento em tais protocolos. Este será um análogo da substituição de algumas partes do módulo, por exemplo, por testes ou um esboço temporário.
Como exemplo, considere um interator simples com um método que executa uma solicitação de rede:
protocol SomeInteractorTrait { var someService: SomeServiceInput! { get } func performRequest(completion: @escaping (Response) -> Void) } extension SomeInteractorTrait { func performRequest(completion: @escaping (Response) -> Void) { someService.performRequest(completion) } }
Este é um código abstrato, por exemplo. Suponha que não precisamos enviar uma solicitação, mas precisamos retornar algum tipo de esboço. Aqui vamos ao truque - crie um protocolo vazio chamado Mock e faça o seguinte:
protocol Mock {} extension SomeInteractorTrait where Self: Mock { func performRequest(completion: @escaping (Response) -> Void) { completion(MockResponse()) } }
Aqui, a implementação do método performRequest foi alterada para tipos que implementam o protocolo Mock. Agora você precisa implementar o protocolo Mock para a classe que implementará SomeInteractor:
class SomePresenter: SomeInteractorTrait, Mock { // Implementation }
Para a classe SomePresenter, a implementação do método performRequest será chamada, localizada na extensão, onde Self satisfaz o protocolo Mock. Vale a pena remover o protocolo Mock e a implementação do método performRequest será obtida da extensão usual para SomeInteractor.
Se você usar isso apenas para testes, é melhor colocar todo o código associado à substituição da implementação no destino de teste.
Resumir
Em conclusão, vale a pena observar os prós e os contras dessa abordagem e em quais casos, na minha opinião, vale a pena usar.
Vamos começar com os contras:
- Se você se livrar do roteador e do interator, como mostrado no exemplo, a capacidade de implementar essas dependências será perdida.
- Outro ponto negativo é o número cada vez maior de protocolos.
- Às vezes, o código pode não parecer tão claro quanto usar abordagens convencionais.
Os aspectos positivos dessa abordagem são os seguintes:
- Mais importante e óbvio, a duplicação é bastante reduzida.
- A ligação estática é aplicada aos métodos de protocolo. Isso significa que a determinação da implementação do método ocorrerá no estágio de compilação. Portanto, durante a execução do programa, não será gasto tempo adicional na busca de uma implementação (embora esse tempo não seja particularmente significativo).
- Devido ao fato de os protocolos serem pequenos “tijolos”, qualquer composição pode ser facilmente composta a partir deles. Mais no karma para flexibilidade no uso.
- Facilidade de refatoração, sem comentários aqui.
- Você pode começar a usar essa abordagem em qualquer estágio do projeto, uma vez que não afeta o projeto inteiro.
Considerar essa decisão boa ou não é um assunto particular para todos. Nossa experiência com essa abordagem foi positiva e resolveu problemas.
Isso é tudo!