Artigo original: How to Learn Software Design and Architecture - a Roadmap

Tradução em português europeu
Este artigo é um resumo do que estou a escrever no meu projeto mais recente, solidbook.io - The Handbook to Software Design and Architecture with TypeScript (link em inglês). Dá uma vista de olhos por lá se gostares desta publicação.

É uma loucura para mim considerar o facto do Facebook ter sido em tempos um ficheiro de texto vazio no computador de alguém. 🤣

Neste último ano, tenho-me esforçado muito em design e arquitetura de software, Design Orientado a Domínio (link em inglês) e a escrever um livro sobre isso, e queria tirar um tempo para tentar juntar as peças todas em algo útil que pudesse partilhar com a comunidade.

Aqui está o meu roteiro para como aprender design e arquitetura de software.

Dividi-o em duas partes: o stack (pilha) e o map (mapa).

O stack

De modo semelhante ao modelo OSI em redes, cada camada é construída sobre a base da camada anterior.

e727h5b9nozcuo4za2yw

O map

Embora eu ache que a pilha é boa para ver o panorama geral de como tudo funciona em conjunto, o mapa é um pouco mais detalhado (e inspirado pelo roteiro do programador web – link em inglês) e, como resultado, penso que seja mais útil.

Aqui está ele! Para duplicar o repositório, ler a minha redação detalhada e fazer o download em alta resolução, clica aqui.

65834517-bb39f980-e2a9-11e9-8a75-0e1559c5ed56
Nota da tradução: houve uma revisão do conteúdo em março deste ano. Caso deseje acessar o conteúdo mais recente (em inglês) acesse aqui.

Fase 1: código limpo

O primeiro passo em direção à criação de software duradouro é descobrir como escrever código limpo.

Código limpo é código que é fácil de compreender e alterar. Logo no início, isto manifesta-se em algumas escolhas de design como:

  • Ser consistente
  • Dar preferência a nomes significativos de variáveis, métodos e classes em vez de explicar em comentários
  • Garantir que o código está corretamente indentado e espaçado
  • Garantir que todos os testes possam ser executados
  • Escrever funções puras sem efeitos secundários
  • Não passar nulos

Escrever código limpo é incrivelmente importante

Pensa nisso como se fosse o jogo do Jenga.

De modo a manter a estrutura do nosso projeto estável ao longo do tempo, coisas como indentação, pequenas classes e métodos, e nomes significativos, compensam bastante a longo termo.

O melhor recurso para aprender como escrever código limpo é livro de Uncle Bob, "Código Limpo" (link em inglês).

Fase 2: paradigmas de programação

Agora que estamos a escrever código legível que é fácil de fazer a manutenção, seria uma boa ideia compreender realmente os 3 maiores paradigmas da programação e a maneira como influenciam o nosso modo de escrever código.

No livro de Uncle Bob, "Arquitetura Limpa" (link em inglês), ele destaca que:

  • Programação Orientada a Objetos é a ferramenta mais adequada para definir como passamos limites arquitetónicos com polimorfismo e plugins
  • Programação funcional é a ferramenta que utilizamos para enviar dados para os limites das nossas aplicações
  • E programação estruturada é a ferramenta que utilizamos para escrever algoritmos

Isso implica que o software eficiente utiliza uma mistura de todos estes 3 estilos de paradigmas de programação em alturas diferentes.

Embora possas adotar uma abordagem estritamente funcional ou estritamente orientada a objetos para escrever código, compreender onde cada um deles se destaca melhorará a qualidade dos teus designs.

Se tudo o que tiveres for um martelo, tudo se parece com um prego.

Recursos

Para programação funcional, dá uma vista de olhos:

Fase 3: programação orientada a objetos

É importante saber como cada um dos paradigmas funciona e como te incentivam a estruturar o código dentro deles, mas no que diz respeito à arquitetura, Programação Orientada a Objetos é claramente a ferramenta ideal para o trabalho.

A programação Orientada a Objetos não só nos permite criar uma arquitetura de plugin e criar flexibilidade nos nossos projetos; Programação Orientada a Objetos vem com 4 princípios de Programação Orientada a Objetos (encapsulamento, herança, polimorfismo e abstração) que nos ajudam a criar modelos de domínio ricos.

A maior parte dos programadores a aprender Programação Orientada a Objetos nunca chega a essa parte: aprender como criar uma implementação de software para o domínio do problema, e localizá-la no centro de uma aplicação web com camadas.

Programação funcional pode parecer a solução para todos os problemas nesse cenário, mas eu recomendo familiarizar-te com o design orientado a modelos e Design Orientado a Domínio para compreender o panorama de como os modeladores de objetos são capazes de encapsular um negócio completo com um modelo de domínio com zero dependências.

Por que é que isso é importante?

É muito importante porque, se podes criar um modelo mental de um negócio, podes criar uma implementação de software desse negócio.

Fase 4: princípios de design

Neste momento, a tua compreensão de que a Programação Orientada a Objetos é muito útil para encapsular modelos de domínio ricos e para resolver o terceiro tipo de "Problemas de Software Difíceis"– Domínios Complexos (link em inglês).

No entanto, a Programação Orientada a Objetos pode introduzir alguns desafios de design.

Onde devo utilizar composição?

Quando devo utilizar herança?

Quando devo utilizar uma classe abstrata?

Princípios de design são boas práticas muito bem estabelecidas e testadas que podes utilizar como segurança.

Alguns exemplos de princípios de design comuns com que te deves familiarizar são:

Certifica-te de que alcanças as tuas próprias conclusões, no entanto. Não te limites a seguir o que outra pessoa diga que deves fazer. Certifica-te que faz sentido para ti.

Fase 5: padrões de design

Praticamente todos os problemas de software já foram categorizados e resolvidos em algum momento. Chamamos a esses padrões: padrões de design, na verdade.

Existem 3 categorias de padrões de design: criativo, estrutural e comportamental.

Criativo

Padrões criativos são padrões que controlam como os objetos são criados.

Exemplos de padrões criativos incluem:

  • O padrão Singleton, para garantir que existe uma única instância de uma classe.
  • O padrão Abstract Factory, para criar uma instância de várias famílias de classes.
  • O padrão Prototype, para começar com uma instância que é clonada a partir de uma já existente.

Estrutural

Padrões estruturais que simplificam como definimos relações entre componentes.

Exemplos de padrões de design estruturais incluem:

  • O padrão Adapter, para criar uma interface para acionar classes que normalmente não funcionam em conjunto, para funcionarem em conjunto.
  • O padrão Bridge, para dividir uma classe que deve ser um ou mais, num conjunto de classes que pertencem a uma hierarquia, permitindo que as implementações sejam desenvolvidas de forma independente umas das outras.
  • O padrão Decorator, para adicionar responsabilidades a objetos dinamicamente.

Comportamental

Padrões comportamentais são padrões comuns para facilitar comunicações elegantes entre objetos.

Exemplos de padrões comportamentais são:

  • O padrão Template, para diferir os passos exatos de um algoritmo para uma subclasse.
  • O padrão Mediator, para definir os canais de comunicação exatos permitidos entre classes.
  • O padrão Observer, para habilitar classes para descrever algo de interesse e para notificar quando ocorrer uma alteração.

Criticas sobre o padrão de design

Padrões de design são bons, mas por vezes podem trazer complexidade extra aos nossos designs. É importante lembrar o YAGNI e tentar manter os nossos designs tão simples quanto possível. Utiliza apenas padrões de design quando tiveres mesmo a certeza que precisas deles. Saberás quando for o momento.

Se soubermos o que cada um destes padrões faz, quando utilizá-los, e quando nem sequer nos preocuparmos em utilizá-los, estaremos em boa forma para começar a compreender como fazer a arquitetura de sistemas complexos.

A razão por trás disso é que padrões arquitetónicos são simplesmente padrões de design com a escala elevada para alto nível, enquanto que os padrões de design são implementações de baixo nível (mais próximas a classes e funções).

Recursos

Refactoring Guru - Padrões de Design (link em inglês)

Fase 6: princípios arquitetónicos

Agora estás num nível de pensamento mais elevado do que o nível de classe.

Compreendemos que as decisões que tomamos para organizar e criar relações entre componentes, a nível elevado e a nível baixo, vão ter um impacto significativo na facilidade de manutenção, flexibilidade e capacidade de testagem do nosso projeto.

Aprende os princípios orientadores que te ajudam a criar a flexibilidade que o teu código-base necessita para ser capaz de reagir a novas funcionalidades e exigências, com o mínimo esforço possível.

Aqui está o que eu recomendo que aprendas logo à partida:

  • Princípios de design de componentes: o Princípio de Abstração Estável (link em inglês), o Princípio de Dependência Estável (link em inglês), e o Princípio de Dependência Acíclica, para como organizar componentes, as suas dependências, quando juntá-las, e as implicações de criar acidentalmente ciclos de dependência e depender de componentes instáveis.
  • Política x Detalhe (Link em inglês), para compreender como separar as regras da tua aplicação a partir dos detalhes de implementação.
  • Limites e como identificar os subdomínios a que as funcionalidades da tua aplicação pertencem.

O Uncle Bob descobriu e documentou originalmente muitos destes princípios. Então, o melhor recurso para aprender isto é, novamente, "Arquitetura Limpa" (link em inglês).

Fase 7: estilos arquitetónicos

Arquitetura é sobre as coisas que importam.

É sobre identificar as necessidades do sistema de maneira a que este seja bem-sucedido e de seguida aumentar as hipóteses de sucesso ao escolher a arquitetura que melhor se adequa aos requisitos.

Por exemplo, um sistema que tem muita complexidade de lógica de negócio beneficiaria da utilização de uma arquitetura por camadas para encapsular essa complexidade.

Um sistema como a Uber precisa de ser capaz de lidar com muitos eventos em tempo real de uma só vez e atualizar a localização do condutor, então, um estilo de arquitetura publicar-subscrever poderia ser o mais eficaz.

Vou-me repetir a mim mesmo aqui porque é importante observar que as 3 categorias de estilos arquitetónicos são semelhantes às 3 categorias de padrões de design, porque estilos arquitetónicos são padrões de design de alto nível.

Estrutural

Projetos com níveis variantes de componentes e grande intervalo de funcionalidades beneficiarão ou serão prejudicados pela adopção de uma arquitetura estrutural.

Aqui estão alguns exemplos:

  • Arquiteturas com base em componentes enfatizam separação de preocupações entre os componentes individuais dentro de um sistema. Imagina a Google por um segundo. Considera quantas aplicações eles têm dentro da empresa (Google Docs, Google Drive, Google Maps, etc). Para plataformas com muitas funcionalidades, arquiteturas com base em componentes dividem as preocupações em componentes independentes vagamente agrupados. Isto é uma separação horizontal.
  • Monolítico significa que a aplicação é combinada numa única plataforma ou programa, implementada completamente. Observação: podes ter uma arquitetura com base em componentes E monolítica se separares as tuas aplicações adequadamente, enquanto implementas tudo em conjunto.
  • Arquiteturas por camadas separam verticalmente as preocupações ao dividir o software em camadas de infraestrutura, aplicação e domínio.
app-logic-layers
Um exemplo de corte das responsabilidades/preocupações de uma aplicação de modo vertical usando uma arquitetura de camadas. aqui (texto em inglês) para obter mais informações sobre como fazer isso.

Mensagens

Dependendo do teu projeto, enviar mensagens pode ser um componente realmente importante para o sucesso do sistema. Para projetos como esse, arquiteturas com base em mensagens criadas sobre princípios de programação funcional e padrões de design comportamental como o padrão observer.

Aqui estão alguns exemplos de estilos de arquiteturas com base em mensagens:

  • Arquiteturas Orientadas por Eventos vêm todas as alterações significativas a estados como eventos. Por exemplo, numa aplicação de venda de discos de vinil, o estado de uma oferta pode alterar de "pendente" para "aceite" quando ambas as partes concordam com a troca.
  • Arquiteturas Publicar-subscrever são criadas sobre o padrão de design Observer ao fazer com que seja o método de comunicação principal entre o próprio sistema, utilizadores finais/clientes, e outros sistemas e componentes.

Distributivo

Uma arquitetura distributiva significa simplesmente que os componentes do sistema são implementados separadamente e operam ao comunicar num protocolo de rede. Sistemas distributivos podem ser muito eficazes para escalar a taxa de transferência, escalar equipas e delegar (tarefas potencialmente caras) responsabilidades a outros componentes.

Alguns exemplos de estilos de arquiteturas distributivas são:

  • Arquitetura client-servidor. Uma das arquiteturas mais comuns, onde dividimos o trabalho a ser feito entre o client (apresentação) e o servidor (lógica de negócio).
  • Arquiteturas ponto-a-ponto distribuem tarefas de camada de aplicação entre participantes igualmente privilegiados, formando uma rede ponto-a-ponto.

Fase 8: padrões arquitetónicos

Padrões arquitetónicos explicam em grande detalhe tático como realmente implementar um destes estilos arquitetónicos.

Aqui estão alguns exemplos de padrões arquitetónicos e os estilos que estes herdam:

  • Design Orientado a Domínio é uma abordagem ao desenvolvimento de software para problemas de domínio realmente complexos. Para que o design orientado a domínio seja bem-sucedido, precisamos de implementar uma arquitetura de camadas de maneira a separar as preocupações de um modelo de domínio dos detalhes infra-estruturais que fazem a aplicação realmente executar, como bases de dados, servidores web, caches etc.
  • Controlador Modelo-Vista é provavelmente o padrão arquitetónico mais conhecido para desenvolver aplicações baseadas na interface de utilizador. Funciona ao dividir a aplicação em 3 componentes: modelo, vista e controlador. O controlador modelo-vista é incrivelmente útil quando estás no início e ajuda-te a aproveitar outras arquiteturas, mas existe uma altura em que percebemos que o controlador Modelo-Vista não é suficiente para problemas com muita lógica de negócio.
  • Fornecimento de eventos é uma abordagem funcional onde armazenamos apenas as transações e nunca o estado. Se alguma vez precisarmos do estado, podemos aplicar todas as transações desde o início.

Fase 9: padrões empresariais

Qualquer padrão arquitetónico que escolhas introduzirá um número de construções e linguagem especializada para te familiarizares e decidir se vale a pena o esforço ou não.

Ao pegar num exemplo que muitos de nós conhecemos, no Controlador Modelo-Vista, a vista armazena todo o código da camada de apresentação, o controlador é comandos de tradução e consultas da vista para pedidos que são tratados pelo modelo e retornados pelo controlador.

Em que parte do Modelo (M) lidamos com estas coisas?:

  • Lógica de validação
  • Regras invariáveis
  • Eventos de domínio
  • Casos de utilização
  • Consultas complexas
  • E lógica empresarial

Se simplesmente utilizarmos um ORM (object-relational mapper) como Sequelize ou TypeORM como o modelo, todas essas coisas importantes são deixadas para interpretação onde devem estar, e acabam por ficar numa capada não especificada entre o (que deveria ser um rico) modelo e o controlador.

mvc-2
Retirado de "3.1 - Slim (Logic-less) models" em solidbook.io (link em inglês).

Se existe alguma coisa que eu tenha aprendido até agora na minha jornada para lá do controlador modelo-vista, é que existe uma construção para tudo.

Para cada uma destas coisas em que o Controlador Modelo-Vista falha em abordar, existem outros padrões empresariais para resolvê-las. Por exemplo:

  • Entidades (link em inglês) descrevem modelos que têm uma identidade.
  • Objetos de valor (link em inglês) são modelos que não têm identidade e podem ser utilizados de modo a encapsular a lógica de validação.
  • Eventos de domínio (link em inglês) são eventos que significam algum evento empresarial relevante a acontecer e podem ser subscritos de outros componentes.

Com base no estilo arquitetónico que tiveres escolhido, vão existir muitos outros padrões empresariais para aprenderes de maneira a implementar esse padrão no seu potencial máximo.

Padrões de integração

Assim que a tua aplicação estiver operacional, à medida que recebes mais e mais utilizadores, podes obter alguns problemas de desempenho. Chamadas de API podem demorar muito tempo, os servidores podem quebrar por estares sobrecarregados de pedidos etc. Para resolver esses problemas, podes ler sobre a integração de coisas como consulta de mensagens ou caches de modo a melhorar o desempenho.

Estas são provavelmente as coisas mais desafiantes: escalar, auditar e desempenho.

Desenhar um sistema para escala pode ser incrivelmente desafiante. Isto requer uma compreensão profunda sobre as limitações de cada componente dentro da arquitetura e um plano de ação para como mitigar o stress na tua arquitetura e continuar a servir pedidos em situações de grande tráfego.

Existe também a necessidade de auditar o que está a acontecer na tua aplicação. Grandes empresas precisam de ser capazes de fazer auditorias de modo a identificar potenciais problemas de segurança, compreender como os utilizadores estão a utilizar as suas aplicações, e ter um registo de tudo o que aconteceu até agora.

Isso pode ser desafiante de implementar, mas as arquiteturas comuns acabam por parecer baseadas em eventos e são criadas sobre uma grande variedade de conceitos de design de software e sistemas, princípios e práticas como Event Storming, DDD, CQRS (command query response segregation) e Event Sourcing.