Como adotamos Arquitetura modular em projeto iOS na Cora

Entenda como estruturamos a arquitetura modular em nosso projeto iOS e como tudo começou

Renato Sarro Matos
Cora
5 min readJan 11, 2023

--

Arquitetura modular é um assunto muito falado hoje em dia e o objetivo aqui, é compartilhar um pouco como estruturamos nossa arquitetura modular e em que momento fizemos isso.

Não, nosso projeto não nasceu modularizado (com exceção da nossa biblioteca de rede e nosso Design System). Quando se inicia um projeto do zero, você não tem visão do que o projeto vai virar ou como ele vai crescer e quando criamos módulos desde o primeiro dia, a tendência é gerar atrito na evolução do projeto, pois ele se torna mais sensível a mudanças estruturais.

Pensando nisso, priorizamos 2 premissas para o nosso projeto:

  • Ter o mínimo de dependência possível
    Eliminar o acoplamento, cada parte deve funcionar de forma independente
  • Ter um único entry point pra cada feature/sessão
    A feature é sempre acessada de um único lugar

Abordamos um pouco mais sobre isso neste artigo sobre nossa arquitetura.

Passados 3 anos, já tínhamos uma visão muito clara do que era nosso projeto, o que evoluímos, o que ele tinha de pontos fortes e pontos fracos em sua estrutura e percebemos que ele já havia alcançado uma maturidade muito boa, o que nos dava uma segurança para começar a quebrar nosso projeto em pequenos módulos.

O primeiro passo, foi olhar pra nossa arquitetura e definir quais módulos teríamos e como eles se integrariam dentro da nossa estrutura.

Olhando para o nosso projeto, ele tinha o seguinte formato:

Como dito anteriormente, tínhamos o módulo de rede (Mamute) e o módulo de Design System. Dentro do módulo App, a primeira quebra que fizemos foi da camada de Domain. Este foi o primeiro módulo escolhido, pois esta camada provia recursos pra grande parte das features. Sempre que era preciso acessar dados, seja local ou remoto, precisávamos de um UseCase.

Antes de pensar em criar um módulo de feature, precisávamos preparar os recursos que as features precisariam. Por esse motivo, decidimos começar por aqueles módulos que eram utilizados em qualquer lugar do projeto.

Neste módulo ficariam nossos UseCases, responsáveis por conectar a camada de apresentação com o Mamute (nossa lib de rede).

Um detalhe importante que podemos observar aqui, é a aplicação do conceito de Dependency Inversion do SOLID. Perceba que o módulo de alto nível (Presentation) não depende do módulo de baixo nível (Domain) ou vice versa. Ambos dependem da abstração (DomainInterface).

Este conceito é de extrema importância, pois além de garantir o desacoplamento, fazendo com que nossos módulos funcionem de forma independente, mantemos a testabilidade, a manutenabilidade e ainda não somos impactados em tempo de build. Ou seja, a premissa inicial se mantém.

Injeção de Dependência

Aqui vale a pena mencionar também que criamos uma estrutura de injeção de dependência nativa baseada no @propertyWrapper. Então ao utilizar um módulo, em vez de fazermos a injeção via inicializador, fazemos via @Inject, totalmente transparente pra quem está utilizando:

Todas as dependências necessárias pra inicializar o módulo são registradas em um DependencyContainer ao inicializar um módulo. Aqui é possível identificar também um pouco da inversão de controle (IoC), pois a dependência vem de fora pra dentro.

Outros módulos de uso global, como Resources, Helpers, Validators, Templates, entre outros, seguem a mesma estrutura e são injetados de forma transparente.

Micro Features?

Bom, uma vez que temos a base estruturada conseguimos pensar em começar a criar módulo de feature e como cada módulo se integra dentro do nosso projeto.

Assim como microsserviços e micro fron-tend, micro feature vem ganhando muita força ultimamente e é uma abordagem de modularização bem utilizada em diversos projetos. Porém quando pensamos a respeito, vimos que no longo prazo passaríamos a ter que lidar com uma interdependência entre os módulos, o que traria uma complexidade um tanto difícil de lidar.

Aqui vale a pena trazer um princípio operacional que direciona muito nosso desenvolvimento e novas iniciativas: "Pequenas ideias que crescem com o tempo".

Nossa arquitetura evoluiu com o tempo, nossa compreensão do projeto cresceu com o tempo, as bibliotecas, os padrões, a própria estrutura modular, tudo começou pequeno e foi evoluindo até chegar no que temos hoje. Então não fazia muito sentido também partirmos pra uma abordagem que ainda não sabíamos como iria se comportar em nossa estrutura.

Com isso em mente, em vez de criar um módulo pra cada feature, vimos que faria mais sentido segmentar as features por contexto e criar um módulo pra cada segmento.

Estrutura modular por contexto

Desta forma, reduzimos muito a dependência, pois o único lugar que conhece todos os módulos é o módulo app.

Gestão de Dependência

Aqui vale a pena comentar também um pouco sobre como integramos cada módulo ao App.

Hoje, é muito comum encontrar projetos utilizando Carthage ou Cocoapods e, até avançar no processo de modularização utilizávamos o Cocoapods. De fato são recursos que dão muita agilidade nessa parte. Porém enquanto evoluíamos o projeto, começamos a dar uma olhada no Tuist. Inclusive, construímos nosso módulo de rede utilizando o Tuist para organizar o módulo com todas as suas dependências.

Em paralelo, também passamos a olhar para o Swift Package Manager, o dependency manager lançado pela Apple, que vinha ganhando muita visibilidade desde o seu lançamento.

Após um longo período de testes, resolvemos utilizar o SPM, justamente por ser algo nativo, da própria plataforma.

Ainda utilizamos Cocoapods, pois três libs que utilizamos ainda não oferecem suporte ao SPM, mas a expectativa é, no curto prazo, termos apenas SPM operando em nossa plataforma.

Estratégia

Como comentei no início, nosso projeto não nasceu modularizado. E assim como toda iniciativa, essa mudança estrutural acaba gerando um débito técnico.

Pensando nisso, toda nova implementação estamos fazendo dentro de cada módulo e conforme a necessidade, vamos alimentando os módulos cross — Domain e Core.

Desta forma, além de irmos validando a estrutura, configurações e elementos que os módulos vão precisar, conseguimos fazer esta mudança pouco a pouco, sem precisar parar o capítulo por um determinado período pra fazer tudo de uma vez, com pressa. E assim colaboramos para sempre reforçar nosso princípio operacional de que pequenas ideias que crescem com o tempo.

Que tal interagir clicando nos claps (de 1 a 50) pra mostrar que curtiu o texto? 👏

Você também pode acessar nossa Página de Carreiras para saber mais sobre nossas vagas e acompanhar a Cora no Linkedin e no Instagram de Corajosers.

--

--

Renato Sarro Matos
Cora

iOS Lead na Cora, metido a músico e apreciador da arte do tricô