A importância da arquitetura em Apps iOS

Photo by Danist Soh on Unsplash

Muitas vezes nos deparamos com aplicativos grandes no trabalho. Muitas features, muitas telas, muitos componentes. O que aconteceu para que chegassem neste ponto? Nada mais normal do que o crescimento do negócio e a chegada de mais clientes, ampliando o portfólio de produtos a serem ofertados na aplicação em questão. Realmente é uma tarefa difícil, sustentar uma base grande de código sem introduzir novos bugs e quebrar o que já existe. Tem uma grande aliada nesse processo todo: a arquitetura de software. A arquitetura nas construções garante que teremos um padrão, e que as coisas sejam funcionais. Por exemplo, que teremos uma torneira com água na pia da cozinha a fim de que nos ajude a cozinhar e lavar a louça. Não tem como existir uma cozinha (completa ao menos) sem uma torneira que funcione. Fica imprático. Os engenheiros neste caso, vão garantir uma construção sólida que comporte o encanamento de água na respectiva parede e a chegada de água na mesma bem como a saída para o local correto. Assim como os arquitetos, codificamos de maneira que a manutenção e utilização seja facilitada no futuro, seja por nós mesmos ou outros desenvolvedores.

Quantos padrões de projeto vocês conhecem? Eu conheço no mínimo uns 10, mas com certeza tem muito mais. Ninguém é obrigado a conhecer todos os padrões de projeto e arquiteturas existentes, até porque é inviável. Contudo, conhecer alguns deles te ajuda a tomar decisões no dia a dia de modo a simplificar a manutenção do projeto. Um princípio que gosto de aplicar sempre é o do SOLID. De maneira geral separa muito bem as responsabilidades de cada coisa, uma espécie de organização geral da casa garantindo que você não vai assar um frango na parte de trás da televisão.(informações aprofundadas sobre o SOLID consultar este artigo).

Considere o código abaixo:

Perceba que temos 4 funções, sendo que podemos agrupar as mesmas em 2 grupos de duas funções cada. Em um grupo teremos as funções que lidam com os valores numéricos (qualquer que seja a lógica de negócio atrelada a elas), em outro temos as funções correspondente a mensagem, da saudação a validação de quantidade de caracteres. Seria então interessante separar estas funções em duas classes/arquivos diferentes a fim de que fique mais fácil o agrupamento das lógicas e regras. Se um dia for necessária a alteração da quantidade de caracteres máxima para a mensagem por exemplo, estando a propriedade junto de sua lógica em um respectivo arquivo fica fácil de achar.

Considere a função acima. Claramente ela salva informações de um usuário, mas perceba que ela é ilegível, mal cabe na tela dada a quantidade de informações sendo passadas como parâmetros. Para corrigir o problema podemos criar uma estrutura como a seguinte:

Se podemos complicar para que simplificar? Aqui estamos passando um array de informações para o mesmo método utilizado acima que salva os dados do usuário. Mas perceba que neste caso não sabemos quantas informações são nem quais são, somente seu tipo forçado a ser sempre String. O fato de ser fácil este array chegar ai sem estar preenchido por algum motivo qualquer representa uma facilidade da aplicação quebrar. Qualquer alteração neste código será muito mais difícil e aumenta a chance de quebras, além de complicar muito o debug.

Vamos destrinchar os parâmetros que tinhamos na primeira solução então:

O debug agora fica muito mais fácil, já que podemos inspecionar cada uma das constantes do usuário para ver se tem valor. Sabemos exatamente o que está sendo passado para o método que salva as informações. Foi uma melhora significativa, mas ainda não resolvemos todos os problemas. Note que temos uma estrutura de usuário tratando de muita coisa que não necessária, como por exemplo número e cep. Podemos então criar uma outra estrutura de endereço como exemplificado abaixo:

Pontos importantes a se notar neste ultimo exemplo: a chamada não foi alterada, continuamos passando um objeto usuário para ser salvo, mas que agora contempla um outro objeto endereço. Qualquer lógica referente a endereço pode ficar junto com a sua definição bem como qualquer lógica referente a usuário igualmente. Se vamos utilizar API's como as do iOS por exemplo, para Table Views ou CollectionViews, devemos sempre separar o máximo possível o nosso código, do código das API's, a fim de que seja fácil a troca de estratégia e até mesmo de API utilizada. Veja no exemplo abaixo, separei os delegates e datasources da UITableView em uma extensão da viewController bem como coloquei todas as informações a serem apresentadas em um objeto viewModel. Não fica fácil de entender assim? A nossa viewController fica enxuta e se preciso podemos ainda colocar a extensão em outro arquivo, a fim de que os arquivos não fiquem muito grande.

Uma idéia bacana é agrupar as camadas existentes na sua aplicação. Deixar por exemplo todas as regras de negócio de determinado fluxo em um arquivo específico que pode ser uma interactor (no caso da utilização do clean). São comuns os pedidos de alteração das regras de negócio e concentrar as mesmas em um lugar padrão facilita a manutenção posterior por quaisquer desenvolvedores que vierem a trabalhar naquela funcionalidade específica. As configurações das telas também podem ficar em um lugar específico, bem como qualquer outra abstração que consiga pensar e valha a pena separar (como chamadas para o backend por exemplo). Essa abordagem favorece muito também a criação de testes unitários. Qual a sua importância e relevância? Principalmente adicionam uma segunda camada de verificação na aplicação a fim de garantir que as regras explicitadas no código não vão sofrer alterações indesejadas. Lembra do exemplo acima que validava se a mensagem escrita pelo usuário contem mais de 240 caracteres?

Aqui criamos dois códigos que validam a função para garantir o resultado esperado. Se algum desenvolvedor desavisado for lá e alterar o número máximo de caracteres para 310 por exemplo, o segundo teste quebrará indicando um possível bug. Caso a alteração seja mesmo necessária aí será preciso alterar o teste de modo que não quebre.

Vou mostrar um exemplo mais complexo, que envolve testes e arquitetura. Considere o código abaixo, bem comum em uma aplicação grande, analise com cuidado, pense nas possibilidades:

Perceba que temos uma UIview customizada com um botão e o seu método de toque executa uma ação que pode ser injetada. Essa abordagem permite que as ações/regras fiquem concentradas em um único lugar, facilitando a manutenção. No teste unitário podemos garantir que a injeção está funcionando, bem como garantir a implementação do mesmo. Neste caso, se alguém alterar a implementação, o teste falhará. Fica o desafio aqui para que vocês implementem esses testes unitários.

Já ouviram falar de mock? Como vimos antes é interessante que todas as informações estejam o máximo explicitas possíveis no código, bem como podemos utilizar protocolos para que o código fique organizado e testável. O uso de mock permite executar uma simulação dos fluxos, camadas, testes. Caso o código não esteja bem escrito, ficará difícil a simulação com mocks. Pense bem, podemos simular até as requisições feitas ao backend de modo que tiramos qualquer dependência para garantir que a aplicação está ok. As estratégias aqui vão muito além da proposta deste artigo, mas quem sabe um próximo.

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store