Há muita propaganda sobre VIPER no último ano, e todos estão tão inspirados por ele. A maioria destes artigos são tendenciosos sobre isso e tentam demonstrar como é fixe. Não é nada. Ele tem pelo menos a mesma quantidade de problemas, se não mais, que outros padrões de arquitetura. Neste artigo, quero explicar porque é que VIPER não é tão bom como é anunciado e não é adequado para a maioria das vossas aplicações.
Alguns artigos sobre a comparação de arquitecturas costumam dizer que VIPER é uma arquitectura totalmente diferente de qualquer arquitectura baseada em MVC. Esta afirmação não é verdadeira: É apenas um MVC normal, onde estamos a dividir o controlador em duas partes: interautor e apresentador. A vista permanece a mesma, mas o modelo é renomeado para entidade. O roteador merece algumas palavras especiais: Sim, é verdade, outras arquitecturas não promovem esta parte na sua abreviatura, mas ela ainda existe, de forma implícita (sim, quando chama pushViewController
está a escrever um router simples) ou mais explícita (por exemplo FlowCoordinators).
Agora quero falar sobre os benefícios que o VIPER lhe está a oferecer. Vou usar este livro como uma referência completa sobre VIPER. Vamos começar pelo segundo objectivo, em conformidade com o SRP (Single Responsibility Principle). É um pouco rude, mas que mente estranha pode chamar a isto um benefício? Você é pago para resolver tarefas, não para se conformar com alguma palavra da moda. Sim, você ainda está usando TDD, BDD, testes unitários, Realm ou SQLite, injeção de dependência, e o que quer que você possa lembrar, mas você está usando-o para resolver o problema do cliente, não apenas para usá-lo.
Um outro objetivo é muito mais interessante. A testabilidade é uma preocupação muito importante. Merece um artigo isolado uma vez que muitas pessoas estão a falar sobre ele, mas apenas algumas estão realmente a testar as suas aplicações e ainda menos estão a fazê-lo correctamente.
Uma das principais razões para isso é a falta de bons exemplos. Há muitos artigos sobre como criar um teste unitário com assert 2 + 2 == 4
mas nenhum exemplo da vida real (btw, Artsy está a fazer um excelente trabalho com o sourcing aberto das suas aplicações, deve dar uma vista de olhos aos seus projectos).
VIPER está a sugerir separar toda a lógica num monte de pequenas classes com responsabilidades separadas. Isso pode facilitar os testes, mas nem sempre. Sim, escrever testes unitários para uma classe simples é fácil, mas a maioria destes testes não estão a testar nada. Por exemplo, vamos olhar para a maioria dos métodos de um apresentador, eles são apenas um proxy entre uma visão e outros componentes. Você pode escrever testes para esse proxy, ele irá aumentar a cobertura de teste do seu código, mas esses testes são inúteis. Você também tem um efeito colateral: você deve atualizar esses testes inúteis após cada edição no código principal.
A abordagem adequada aos testes deve incluir o interautor e apresentador de testes ao mesmo tempo, porque essas duas partes estão profundamente integradas. Além disso, porque separamos entre duas classes, precisamos de muito mais testes em comparação com um caso em que temos apenas uma classe. É um simples сombinatorics: a classe A tem 4 estados possíveis, a classe B tem 6 estados possíveis, por isso uma combinação de A e B tem 20 estados, e deve testar tudo.
A abordagem correcta para simplificar os testes é introduzir pureza ao código em vez de apenas dividir o estado complicado num monte de classes.
Pode parecer estranho, mas testar vistas é mais fácil do que testar algum código de negócios. Uma vista expõe o estado como um conjunto de propriedades e herda a aparência visual dessas propriedades. Então você pode usar o FBSnapshotTestCase para combinar o estado com a aparência. Ele ainda não lida com alguns casos de bordas como transições personalizadas, mas com que frequência o implementa?
Desenho super-engenharia
VIPER é o que acontece quando antigos programadores Java empresariais invadem o mundo iOS. -n0 danificação, comentário vermelho
Honestly, alguém pode olhar para isto e dizer: “sim, essas classes e protocolos extras realmente melhoraram minha habilidade de entender o que está acontecendo com minha aplicação”.
Imagine tarefa simples: temos um botão que aciona uma atualização de um servidor e uma visualização com dados de uma resposta do servidor. Adivinha quantas classes/protocolos serão afectados por esta alteração? Sim, pelo menos 3 classes e 4 protocolos vão ser alterados para esta funcionalidade simples. Ninguém se lembra de como o Spring começou com algumas abstracções e terminou com AbstractSingletonProxyFactoryBean
? Eu sempre quero um “conveniente proxy factory bean superclass for proxy factory beans that create only singletons” no meu código.
Redundant components
Como já mencionei antes, um apresentador é normalmente uma classe muito burra que é apenas proxying calls from view to interactor (algo como isto). Sim, às vezes contém lógica complicada, mas na maioria dos casos é apenas um componente redundante.
quantidade de protocolos “DI-friendly”
Existe uma confusão comum com esta abreviação: VIPER está a implementar os princípios SOLID onde DI significa “inversão de dependência”, não “injecção”. Injeção de dependência é um caso especial de Inversão de Controle, que está relacionado, mas diferente de Inversão de Dependência.
A Inversão de Dependência consiste em separar módulos de diferentes níveis, introduzindo abstrações entre eles. Por exemplo, o módulo UI não deve depender diretamente da rede ou do módulo de persistência. Inversão de Controle é diferente, é quando um módulo (normalmente de uma biblioteca que não podemos alterar) delega algo a outro módulo, que é normalmente fornecido ao primeiro módulo como uma dependência. Sim, quando você implementa a fonte de dados para o seu UITableView
você está usando o princípio de IoC. Usar as mesmas características de linguagem para diferentes propósitos de alto nível é uma fonte de confusão aqui.
Voltemos ao VIPER. Existem muitos protocolos (pelo menos 5) entre cada classe dentro de um módulo. E eles não são nada necessários. Apresentador e Interactor não são módulos de diferentes camadas. Aplicar o princípio IoC pode fazer sentido, mas faça a si mesmo uma pergunta: com que frequência você implementa pelo menos dois apresentadores para uma visão? Eu acredito que a maioria de vocês respondeu zero. Então porque é necessário criar este monte de protocolos que nunca iremos utilizar?
Também, por causa destes protocolos, não pode navegar facilmente pelo seu código com uma IDE, porque o cmd+click irá navegar para um protocolo, em vez de uma implementação real.
Problemas de desempenho
É uma coisa crucial e há muitas pessoas que simplesmente não se importam com isso, ou estão apenas subestimando um impacto de más decisões de arquitetura.
Não vou falar sobre o framework Typhoon (que é muito popular para injeção de dependência no mundo ObjC). Claro que tem algum impacto na performance, especialmente quando se usa a auto-injecção, mas VIPER não requer que você a use. Em vez disso, eu quero falar sobre tempo de execução e inicialização do aplicativo e como VIPER está atrasando seu aplicativo literalmente em todos os lugares.
tempo de inicialização do aplicativo. Raramente é discutido, mas é um tópico importante, se o seu aplicativo for iniciado muito lentamente os usuários evitarão usá-lo. Houve uma sessão na última WWDC sobre a otimização do tempo de inicialização do aplicativo no ano passado. TL; DR: O tempo de inicialização do seu aplicativo depende diretamente de quantas aulas você tem. Se você tem 100 aulas, tudo bem, ninguém vai notar isso. No entanto, se o seu aplicativo tem apenas 100 aulas, você realmente precisa dessa arquitetura complicada? Mas se o seu aplicativo é enorme, por exemplo, você está trabalhando no aplicativo do Facebook (18k classes) esse impacto é enorme, algo em torno de 1 segundo, de acordo com a sessão mencionada anteriormente. Sim, lançar a sua aplicação a frio demora 1 segundo apenas para carregar todos os metadados das classes e nada mais, leu-o correctamente.
Despacho de tempo de execução. É mais complicado, muito mais difícil de fazer o perfil e aplicável principalmente apenas para o compilador Swift (porque o ObjC tem capacidades ricas em tempo de execução e um compilador não pode realizar estas optimizações com segurança). Vamos falar sobre o que está acontecendo quando você chama algum método (eu uso “call” ao invés de “send a message” porque o segundo termo nem sempre é correto para Swift). Existem 3 tipos de despacho no Swift (de mais rápido para mais lento): estático, despacho de tabela e envio de mensagem. O último é o único tipo que é usado no ObjC, e é usado no Swift quando é necessário interop com código ObjC, ou quando declaramos um método como dynamic
. Claro, esta parte do tempo de execução é enormemente otimizada e escrita em assembler para todas as plataformas. Mas e se pudermos evitar esta sobrecarga porque o compilador tem conhecimento sobre o que vai ser chamado em tempo de compilação? É exatamente o que o compilador Swift está fazendo com a estática e o despacho de tabelas. O despacho estático é rápido, mas o compilador não pode usá-lo sem 100% de confiança nos tipos na expressão. Mas quando o tipo da nossa variável é um protocolo, o compilador é forçado a usar o dispatch através de tabelas de testemunhas de protocolo. Não é literalmente lento, mas 1ms aqui, 1ms ali, e agora o tempo total de execução é mais de um segundo maior do que poderíamos conseguir usando código Swift puro. Este parágrafo está relacionado com o anterior sobre protocolos, mas penso que é melhor separar as preocupações sobre apenas uma quantidade imprudente de protocolos não utilizados com uma verdadeira confusão com o compilador com ele.
Sete abstracções fracas separação
Devia haver uma – e de preferência apenas uma – forma óbvia de o fazer.
Uma das questões mais populares na comunidade VIPER é “onde devo colocar X? Portanto, de um lado, há muitas regras de como devemos fazer as coisas bem, mas de outro lado muitas coisas são baseadas em opiniões. Podem ser casos complicados, por exemplo, lidar com CoreData com NSFetchedResultsController
, ou UIWebView
. Mas mesmo casos comuns, como o uso de UIAlertController
, é um tópico para discussão. Vamos dar uma olhada, aqui temos um roteador lidando com alerta, mas lá temos uma visão apresentando um alerta. Pode tentar bater isto com um argumento que é um alerta simples é um caso especial para um alerta simples sem qualquer acção excepto fechar.
Casos especiais não são especiais o suficiente para quebrar as regras.
Yeah, mas porque é que aqui temos uma fábrica para criar esse tipo de alertas? Então, temos uma confusão mesmo com um caso tão simples com UIAlertController
. Quere-lo?
Geração de código
Contas de leitura.
Como é que isto pode ser um problema com uma arquitectura? É apenas gerar um monte de classes de modelos em vez de escrevê-lo por mim mesmo. Qual é o problema com isso? O problema é que na maioria das vezes você está lendo código (a menos que esteja trabalhando em uma empresa de terceirização descartável), e não escrevendo-o. Então você está lendo código de caldeira mexido com um código real na maior parte do seu tempo. É bom? Não me parece.
Conclusão
Não procuro um objectivo para o desencorajar de usar VIPER. Ainda pode haver algumas aplicações que podem beneficiar de todas. No entanto, antes de começar a desenvolver a sua aplicação deve fazer a si mesmo algumas perguntas:
- Esta aplicação vai ter uma vida útil longa?
- As especificações são suficientemente estáveis? Alternativamente, você pode acabar com infinitos refactorings enormes mesmo para pequenas mudanças.
- Você realmente testa seus aplicativos? Seja honesto consigo mesmo.
Apenas se responder “sim” a todas as perguntas VIPER pode ser uma boa escolha para a sua aplicação.
Finalmente, a última: você deve usar o seu próprio cérebro para as suas decisões, não confie cegamente num tipo qualquer da Medium ou da conferência em que participou e que está a dizer: “Usa X, X é fixe.” Esses caras também podem estar errados.