NoUML com níveis de abstração

Volodymyr Frolov Blocked Unblock Seguir Seguindo 8 de janeiro

Neste artigo, discutiremos como definir e impor limites arquitetônicos em sistemas de software. Você também verá a diferença entre Injeção de Dependência e Inversão de Controle.

Este é o meu segundo artigo na série NoUML. Se você ainda não leu o primeiro , faça isso agora, pois este artigo usa todas as notações e intuições ali desenvolvidas.

Níveis de Abstração

Você já ouviu a frase “Levels of Abstraction” [1], significando que nem todas as abstrações são iguais? Em meu artigo anterior, discutimos que as abstrações não têm estrutura interna, são átomos indivisíveis, pontos em um diagrama maior. Então, como é possível que alguns desses pontos possam ser mais abstratos que os outros? Bem, não é sobre abstrações em si. É tudo sobre o conhecimento de domínio e o contexto em que essas abstrações são organizadas em uma imagem maior que chamamos de Arquitetura de Software. Em um contexto, uma abstração pode estar no coração da Arquitetura e, no outro contexto, a mesma abstração é apenas um detalhe sujo que ninguém se importa.

Deixe-me lhe dar um exemplo. Vamos dizer que você é um arquiteto de um sistema contábil. Haverá abstrações representando Débito e Crédito no centro da sua Arquitetura. Muito provavelmente também haverá abstrações representando ORM (mapeamento objeto-relacional), mas esses são apenas detalhes de implementação relacionados à persistência de dados. Um dia, se você decidir alterar o banco de dados relacional para NoSQL, essas abstrações relacionadas a ORM desaparecerão, mas seu sistema ficará bem. Contrariamente, se o próprio conceito de dinheiro sai a favor da humanidade, seu sistema não tem mais razão para existir junto com as abstrações de Débito e Crédito.

Agora vamos imaginar que você esteja arquitetando sua própria estrutura ORM (espero que você não esteja). As abstrações do ORM desempenharão o papel principal em sua arquitetura, enquanto as abstrações de Contabilidade, Débito e Crédito poderiam ser apenas um mostruário familiar para todos, o mais sujo para todos os detalhes. Agora é o contrário. Seu sistema não tem razão para existir sem bancos de dados relacionais, mas vai ficar bem sem contabilidade. Você poderia facilmente encontrar outro mostruário sem alterar nada em sua arquitetura.

Fronteiras Arquitetônicas

O conceito de Níveis de Abstração é importante porque queremos proteger abstrações de alto nível de serem interrompidas por mudanças em níveis mais baixos [2]. Então, queremos que as abstrações voláteis dependam das estáveis, mas não o contrário. E sabemos que as abstrações de nível mais alto são imensamente estáveis, enquanto as abstrações de nível mais baixo são notoriamente voláteis.

Na prática, isso significa que queremos proibir quaisquer relações que vão de nível superior para abstrações de nível inferior. Queremos dividir nossas abstrações em subgrupos e controlar como esses subgrupos se relacionam. Não importa como você chama esses subgrupos de abstrações em seu sistema: componentes, camadas, serviços, microsserviços ou qualquer outra coisa. Esses subgrupos são arquitetonicamente significativos apenas se fornecerem o nível necessário de isolamento entre abstrações [3].

Você também pode pensar nesses subgrupos de abstrações como se fossem abstrações. Você pode fingir que não sabe nada sobre sua estrutura interna e desenhá-los em diagramas NoUML de ordem superior.

No NoUML nós desenhamos limites arquitetônicos como linhas sólidas separando abstrações. Aqui está como a Arquitetura Limpa de Robert Martin se pareceria:

diagrama NoUML de ordem superior

Como você pode ver neste diagrama, todas as relações, incluindo as transitivas, estão cruzando limites arquitetônicos em uma direção.

Inversão de controle

Digamos que você tenha um limite arquitetônico, mas uma das relações é atravessá-lo na direção errada:

O limite arquitectónico é violado

Neste exemplo, o Controle de Execução flui de Serviço para Cliente e a relação entre essas duas abstrações aponta na mesma direção.

O fluxo de controle é mostrado em azul neste diagrama, mas não é uma relação no sentido NoUML.

O fluxo de controle não é transitivo. Se houver alguns cenários em que o controle flui de A para B e outros cenários em que ele flui de B para C, isso não significa necessariamente que existam cenários nos quais o controle flui de A para C.

O Fluxo de Controle também não pode ser composto com nenhuma relação. Se A usa B e controla os fluxos de B para C, isso não significa que o controle flui de A para C ou que A usa C.

Você sempre pode inverter a direção de qualquer relação usando o truque Inversion of Control [4] sem alterar a direção do Controle de Execução.

Fronteira arquitetônica é restaurada

O limite arquitetural é restaurado neste diagrama. O Controle de Execução ainda flui de Serviço para Cliente, mas agora o Serviço não sabe nada sobre o Cliente. Em vez disso, ele sabe sobre alguma interface que reside no mesmo lado do Architectural Boundary, de modo que a relação de notificação não ultrapasse o limite arquitetônico. O cliente implementa essa interface e é assim que o Serviço pode passar o Controle de Execução para ela. O fluxo de controle cruza o Limite Arquitetônico na direção oposta de todas as relações, mas isso é bom porque o fluxo de controle em si não é uma relação.

Por favor, note que não há uma boa maneira de mostrar a relação "é" como um diagrama de Venn aqui, quando as extremidades da relação residem em lados opostos do limite arquitectónico. Neste caso, apenas desenhamos uma seta e rotulamos esta seta com o tipo de relação.

Injeção de dependência

A maneira mais fácil de implementar a Inversão de Controle é usar alguma estrutura de Injeção de Dependência. Nesse caso, o Cliente será autowired para o Service e não haverá abstração sabendo sobre ambos. Mas estritamente falando, não é necessário, pois o Cliente poderia apenas se conectar manualmente ao Serviço. Nesse caso, a relação de Cliente para Servidor irá cruzar o limite arquitetônico na direção permitida.

Injeção de Dependência vs Inversão de Controle

Os frameworks de injeção de dependência são freqüentemente chamados de Inversion of Control Containers, mas acho que esse nome é enganoso.

Em primeiro lugar, a Injeção de Dependência não é a única maneira de implementar a Inversão de Controle. Outro exemplo famoso seria a Arquitetura de Plugin [5]. Digamos que temos algum mecanismo e seu comportamento pode ser estendido por plug-ins. Todos os Plugins implementam uma interface exposta pelo Engine, portanto, para cada Plugin existe uma relação " is " do conteúdo do Plugin para o Engine. No entanto, o Controle de Execução flui na direção oposta, de Mecanismo para Plug-ins. Assim, a Arquitetura de Plugin é um exemplo de Inversão de Controle sem Injeção de Dependência.

Além disso, a Injeção de Dependência não inverte necessariamente o Controle de Execução. Considere o seguinte exemplo em que o Cliente usa algum Singleton. O Singleton não tem nenhuma interface para que o Cliente saiba tudo sobre ele. No entanto, a instância desse Singleton é criada e gerenciada pela estrutura de injeção de dependência. O cliente não precisa chamar new ou getInstance , o framework Injection de Dependência assume essa responsabilidade.

Injeção de Dependência sem Inversão de Controle

A relação de “ usos ” e o fluxo de controle entre o Cliente e o Singleton vão na mesma direção. Assim, neste diagrama, temos um exemplo de Injeção de Dependência sem Inversão de Controle.

Diagrama de casos de uso

Os níveis de abstração não são o único caso em que o limite arquitetônico é útil. Pense, por exemplo, em como traduzir o diagrama de casos de uso [6] de UML para NoUML. Você pode dividir todas as abstrações em dois subgrupos não sobrepostos: Atores e Casos de Uso. Dentro de cada um desses subgrupos, as relações não são restritas de nenhuma forma e poderiam ser de diferentes tipos, mas entre os subgrupos só poderíamos ter relações de “ uso ” indo apenas em uma direção, de Atores a Casos de Uso.

Esse é outro exemplo do Architectural Boundary que divide Casos de Uso de Atores. Nesse caso, não apenas restringimos a direção das relações que cruzam esse limite, mas também restringimos esse tipo de relação.

Diagrama de casos de uso no NoUML

Você pode facilmente convencer-se de que, se todas as relações explícitas que cruzam esse limite são “ usos ” e estão indo na direção certa, então todas as relações implícitas transitivas que o atravessam também são “ usos ” seguindo o mesmo caminho.

Isso não seria possível se quiséssemos ter apenas relações “ é ” cruzando a fronteira, já que elas geralmente se encaixam nas relações implícitas “ tem ” e “ usa ”.

Conclusões

O Architectural Boundary é um importante conceito que define a Arquitetura de Software, que merece seu próprio lugar no NoUML. Neste artigo, vimos como o Architectural Boundaries molda a arquitetura dos sistemas de software.

Referências

[1] Kent Beck. Smalltalk Best Practice Patterns, 1ª ed., 1996
[2] Matthias Noback. Princípios do Design de Pacotes: Criando Componentes de Software Reutilizáveis, 1ª ed., 2018, p. 217-249.
[3] Robert Martin. The Clean Architecture, 1ª ed., 2012, p. 239
[4] Martin Fowler. Inversão do Controle , 2005.
[5] Martin Fowler, David Rice, Matt Foemmel. Padrões de Arquitetura de Aplicativos Corporativos, 1ª ed., 2003, p. 499
[6] Grady Booch, James Rumbaugh e Ivar Jacobson. O Guia do Usuário do Unified Modeling Language, 1ª ed., 1998, p 225–238.