Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Dica
Esse conteúdo é um trecho do eBook, Arquiteto de Aplicativos Web Modernos com ASP.NET Core e Azure, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.
"Se os construtores construíssem edifícios como programadores escreveram programas, então o primeiro pica-pau que apareceu destruiria a civilização."
- Gerald Weinberg
Você deve arquitetar e projetar soluções de software com a manutenção em mente. Os princípios descritos nesta seção podem ajudar a orientá-lo para decisões arquitetônicas que resultarão em aplicativos limpos e mantenedíveis. Geralmente, esses princípios orientarão você a criar aplicativos de componentes discretos que não estão firmemente acoplados a outras partes do seu aplicativo, mas se comunicam por meio de interfaces explícitas ou sistemas de mensagens.
Princípios comuns de design
Separação de preocupações
Um princípio norteador ao desenvolver é Separação de Responsabilidades. Esse princípio afirma que o software deve ser separado com base nos tipos de trabalho que executa. Por exemplo, considere um aplicativo que inclui lógica para identificar itens de destaque a serem exibidos para o usuário e que formata esses itens de uma maneira particular para torná-los mais perceptíveis. O comportamento responsável por escolher quais itens formatar deve ser mantido separado do comportamento responsável pela formatação dos itens, pois esses comportamentos são preocupações separadas que estão relacionadas apenas coincidentemente entre si.
Arquitetônicamente, os aplicativos podem ser criados logicamente para seguir esse princípio separando o comportamento comercial principal da infraestrutura e da lógica de interface do usuário. Idealmente, as regras de negócios e a lógica devem residir em um projeto separado, que não deve depender de outros projetos no aplicativo. Essa separação ajuda a garantir que o modelo de negócios seja fácil de testar e possa evoluir sem estar firmemente acoplado a detalhes de implementação de baixo nível (também ajuda se as preocupações com a infraestrutura dependem de abstrações definidas na camada de negócios). A separação de preocupações é uma consideração fundamental por trás do uso de camadas em arquiteturas de aplicativos.
Encapsulamento
Diferentes partes de um aplicativo devem usar o encapsulamento para isolá-las de outras partes do aplicativo. Os componentes e as camadas do aplicativo devem ser capazes de ajustar sua implementação interna sem interromper seus colaboradores, desde que os contratos externos não sejam violados. O uso adequado do encapsulamento ajuda a obter acoplamento flexível e modularidade em designs de aplicativos, já que objetos e pacotes podem ser substituídos por implementações alternativas, desde que a mesma interface seja mantida.
Nas classes, o encapsulamento é obtido limitando o acesso externo ao estado interno da classe. Se um ator externo quiser manipular o estado do objeto, ele deverá fazê-lo por meio de uma função bem definida (ou setter de propriedade), em vez de ter acesso direto ao estado privado do objeto. Da mesma forma, os próprios componentes de aplicativo e aplicativos devem expor interfaces bem definidas para seus colaboradores usarem, em vez de permitir que seu estado seja modificado diretamente. Essa abordagem libera o design interno do aplicativo para evoluir ao longo do tempo sem se preocupar que isso interromperá os colaboradores, desde que os contratos públicos sejam mantidos.
O estado global mutável é antitético ao encapsulamento. Um valor buscado do estado global mutável em uma função não é garantido ter o mesmo valor em outra função (ou até mesmo na mesma função). Entender as preocupações com o estado global mutável é um dos motivos pelos quais linguagens de programação como C# têm suporte para diferentes regras de escopo, que são usadas em todos os lugares, de instruções a métodos a classes. Vale a pena observar que as arquiteturas controladas por dados que dependem de um banco de dados central para integração dentro e entre aplicativos estão, por si só, optando por depender do estado global mutável representado pelo banco de dados. Uma consideração fundamental no design controlado pelo domínio e na arquitetura limpa é como encapsular o acesso aos dados e como garantir que o estado do aplicativo não seja inválido pelo acesso direto ao seu formato de persistência.
Inversão de dependência
A direção da dependência dentro do aplicativo deve estar na direção da abstração, não dos detalhes da implementação. A maioria dos aplicativos é escrita de modo que a dependência de tempo de compilação flui na direção da execução do runtime, produzindo um grafo de dependência direta. Ou seja, se a classe A chamar um método da classe B e a classe B chamar um método da classe C, então, em tempo de compilação, a classe A dependerá da classe B, e a classe B dependerá da classe C, conforme mostrado na Figura 4-1.
Figura 4-1. Grafo de dependência direta.
A aplicação do princípio de inversão de dependência permite que A chame métodos em uma abstração que B implementa, possibilitando que A chame B em tempo de execução, mas que B dependa de uma interface controlada por A em tempo de compilação (portanto, invertendo a dependência típica de tempo de compilação). Em tempo de execução, o fluxo de execução do programa permanece inalterado, mas a introdução de interfaces significa que diferentes implementações dessas interfaces podem ser facilmente conectadas.
Figura 4-2. Grafo de dependência invertido.
A inversão de dependência é uma parte fundamental da criação de aplicativos com acoplamento flexível, pois os detalhes de implementação poderão ser gravados para depender e implementar as abstrações de nível superior, em vez do contrário. Os aplicativos resultantes são mais testáveis, modulares e mantêveis como resultado. A prática de injeção de dependência é possível seguindo o princípio de inversão de dependência.
Dependências explícitas
Métodos e classes devem exigir explicitamente todos os objetos colaboradores necessários para funcionar corretamente. Ele é chamado de Princípio de Dependências Explícitas. Os construtores de classe oferecem uma oportunidade para as classes identificarem as coisas necessárias para estarem em um estado válido e funcionarem corretamente. Se você definir classes que podem ser construídas e chamadas, mas que só funcionarão corretamente se determinados componentes globais ou de infraestrutura estiverem em vigor, essas classes estarão sendo desonestas com seus clientes. O contrato do construtor está informando ao cliente que ele só precisa das coisas especificadas (possivelmente nada se a classe estiver apenas usando um construtor sem parâmetros), mas, em tempo de execução, acontece que o objeto realmente precisava de outra coisa.
Seguindo o princípio de dependências explícitas, suas classes e métodos estão sendo honestos com seus clientes sobre o que eles precisam para funcionar. Seguir o princípio torna seu código mais auto-documentável e seus contratos de codificação mais amigáveis, uma vez que os usuários passarão a confiar que, desde que forneçam o que é necessário na forma de método ou parâmetros de construtor, os objetos com os quais estão trabalhando se comportarão corretamente em tempo de execução.
Responsabilidade única
O princípio de responsabilidade única se aplica ao design orientado a objetos, mas também pode ser considerado como um princípio arquitetônico semelhante à separação de preocupações. Ele afirma que os objetos devem ter apenas uma responsabilidade e que eles devem ter apenas um motivo para mudar. Especificamente, a única situação em que o objeto deve ser alterado é se a maneira como ele executa sua única responsabilidade deve ser atualizada. Seguir esse princípio ajuda a produzir sistemas mais flexívelmente acoplados e modulares, uma vez que muitos tipos de novo comportamento podem ser implementados como novas classes, em vez de adicionar responsabilidade adicional às classes existentes. Adicionar novas classes é sempre mais seguro do que alterar classes existentes, pois nenhum código ainda depende das novas classes.
Em um aplicativo monolítico, podemos aplicar o princípio de responsabilidade única em um alto nível às camadas no aplicativo. A responsabilidade da apresentação deve permanecer no projeto de interface do usuário, enquanto a responsabilidade de acesso a dados deve ser mantida dentro de um projeto de infraestrutura. A lógica de negócios deve ser mantida no projeto do application core, onde pode ser facilmente testada e pode evoluir independentemente de outras responsabilidades.
Quando esse princípio é aplicado à arquitetura do aplicativo e levado ao ponto de extremidade lógico, você obtém microsserviços. Um determinado microsserviço deve ter uma única responsabilidade. Se você precisar estender o comportamento de um sistema, geralmente é melhor fazê-lo adicionando microsserviços adicionais, em vez de adicionar responsabilidade a um existente.
Saiba mais sobre a arquitetura de microsserviços
DRY (Don't Repeat Yourself)
O aplicativo deve evitar especificar o comportamento relacionado a um conceito específico em vários locais, pois essa prática é uma fonte frequente de erros. Em algum momento, uma alteração nos requisitos exigirá a alteração desse comportamento. É provável que pelo menos uma instância do comportamento não seja atualizada e o sistema se comporte inconsistentemente.
Em vez de duplicar a lógica, encapsule-a em uma construção de programação. Faça com que essa construção seja a única autoridade sobre esse comportamento e tenha qualquer outra parte do aplicativo que exija esse comportamento use o novo constructo.
Observação
Evite associar um comportamento que seja apenas coincidentemente repetitivo. Por exemplo, apenas porque duas constantes diferentes têm o mesmo valor, isso não significa que você deve ter apenas uma constante, se conceitualmente elas estiverem se referindo a coisas diferentes. A duplicação sempre é preferível ao acoplamento à abstração errada.
Ignorância de persistência
A PI (ignorância de persistência) refere-se aos tipos que precisam ser persistidos, mas cujo código não é afetado pela opção de tecnologia de persistência. Esses tipos no .NET às vezes são chamados de POCOs (Objetos CLR Antigos Simples), pois não precisam herdar de uma classe base específica ou implementar uma interface específica. A ignorância de persistência é importante porque permite que o mesmo modelo de negócios seja persistente de várias maneiras, oferecendo flexibilidade adicional ao aplicativo. As opções de persistência podem mudar ao longo do tempo, de uma tecnologia de banco de dados para outra, ou outras formas de persistência podem ser necessárias além do que o aplicativo começou (por exemplo, usando um cache Redis ou o Azure Cosmos DB, além de um banco de dados relacional).
Alguns exemplos de violações desse princípio incluem:
Uma classe base necessária.
Implementação obrigatória de interface.
Classes responsáveis por salvar a si mesmas (como o padrão Active Record).
Construtor sem parâmetros necessário.
Propriedades que exigem palavra-chave virtual.
Atributos necessário específicos da persistência.
O requisito de que as classes tenham qualquer um dos recursos ou comportamentos acima adiciona o acoplamento entre os tipos a serem persistidos e a escolha da tecnologia de persistência, tornando mais difícil adotar novas estratégias de acesso a dados no futuro.
Contextos limitados
Contextos limitados são um padrão central no design Domain-Driven. Eles fornecem uma maneira de lidar com a complexidade em grandes aplicativos ou organizações dividindo-a em módulos conceituais separados. Cada módulo conceitual, em seguida, representa um contexto separado de outros contextos (portanto, limitado) e pode evoluir de forma independente. Cada contexto limitado deve ser idealmente livre para escolher seus próprios nomes para conceitos dentro dele e deve ter acesso exclusivo ao seu próprio repositório de persistência.
No mínimo, os aplicativos Web individuais devem se esforçar para serem seu próprio contexto limitado, com seu próprio repositório de persistência para seu modelo de negócios, em vez de compartilhar um banco de dados com outros aplicativos. A comunicação entre contextos limitados ocorre por meio de interfaces programáticas, em vez de por meio de um banco de dados compartilhado, o que permite que a lógica de negócios e os eventos ocorram em resposta às alterações que ocorrem. Os contextos limitados são mapeados estreitamente aos microsserviços, que também são idealmente implementados como seus próprios contextos limitados individuais.