Arquitetura limpa do tio Bob - Uma classe de entidade / modelo para cada camada?


44

FUNDO :

Estou tentando usar a arquitetura limpa do tio Bob no meu aplicativo para Android. Estudei muitos projetos de código aberto que tentavam mostrar o caminho certo para fazê-lo e encontrei uma implementação interessante baseada no RxAndroid.

O QUE EU AVISEI:

Em todas as camadas (apresentação, domínio e dados), há uma classe de modelo para a mesma entidade (UML falando). Além disso, existem classes de mapeadores que cuidam da transformação do objeto sempre que os dados cruzam os limites (de uma camada para outra).

PERGUNTA, QUESTÃO :

É necessário ter classes de modelo em todas as camadas quando eu sei que todas elas terão os mesmos atributos se todas as operações CRUD forem necessárias? Ou é uma regra ou uma prática recomendada ao usar a arquitetura limpa?

Respostas:


52

Na minha opinião, não é absolutamente assim que se trata. E é uma violação do DRY.

A idéia é que o objeto de entidade / domínio no meio seja modelado para representar o domínio da melhor maneira e com a maior conveniência possível. Está no centro de tudo e tudo pode depender, pois o domínio em si não muda na maioria das vezes.

Se o seu banco de dados externo puder armazenar esses objetos diretamente, então mapeá-los para outro formato para separar as camadas não é apenas inútil, mas criar duplicatas do modelo e essa não é a intenção.

Para começar, a arquitetura limpa foi feita com um ambiente / cenário típico diferente em mente. Aplicativos de servidor de negócios com camadas externas gigantes que precisam de seus próprios tipos de objetos especiais. Por exemplo, bancos de dados que produzem SQLRowobjetos e precisam SQLTransactionsretornar itens de atualização. Se você usasse aqueles no centro, violaria a direção da dependência, porque seu núcleo dependeria do banco de dados.

Com ORMs leves que carregam e armazenam objetos de entidade, esse não é o caso. Eles fazem o mapeamento entre SQLRowo domínio interno e o seu domínio. Mesmo se você precisar colocar uma @Entitiyanotação do ORM em seu objeto de domínio, eu argumentaria que isso não estabelece uma "menção" da camada externa. Como as anotações são apenas metadados, nenhum código que não esteja procurando especificamente as verá. E mais importante, nada precisa mudar se você os remover ou substituir por uma anotação de banco de dados diferente.

Por outro lado, se você alterar seu domínio e criar todos esses mapeadores, precisará alterar muito.


Alteração: acima é um pouco simplificado e pode até estar errado. Porque há uma parte na arquitetura limpa que deseja que você crie uma representação por camada. Mas isso deve ser visto no contexto do aplicativo.

Ou seja, o seguinte aqui https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html

O importante é que estruturas de dados isoladas e simples sejam passadas através dos limites. Não queremos trapacear e passar linhas de entidades ou de banco de dados. Não queremos que as estruturas de dados tenham qualquer tipo de dependência que viole a regra de dependência.

A passagem de entidades do centro para as camadas externas não viola a regra de dependência, mas elas são mencionadas. Mas isso tem uma razão no contexto da aplicação prevista. Passar entidades ao redor moveria a lógica do aplicativo para o exterior. As camadas externas precisariam saber como interpretar os objetos internos, teriam que efetivamente fazer o que as camadas internas, como a camada "caso de uso", devem fazer.

Além disso, também desacopla as camadas para que as alterações no núcleo não exijam necessariamente alterações nas camadas externas (consulte o comentário de SteveCallender). Nesse contexto, é fácil ver como os objetos devem representar especificamente a finalidade para a qual são usados. Além disso, essas camadas devem conversar entre si em termos de objetos feitos especificamente para os fins desta comunicação. Isso pode até significar que existem 3 representações, 1 em cada camada, 1 para transporte entre as camadas.

E há https://blog.8thlight.com/uncle-bob/2011/11/22/Clean-Architecture.html que aborda acima:

Outras pessoas temem que o resultado líquido dos meus conselhos seja muito código duplicado e muitas cópias repetidas de dados de uma estrutura de dados para outra nas camadas do sistema. Certamente também não quero isso; e nada que eu sugeri levaria inevitavelmente à repetição de estruturas de dados e a uma desordenada cópia de campo.

A IMO implica que a cópia simples de objetos 1: 1 é um cheiro na arquitetura, porque na verdade você não está usando as camadas e / ou abstrações adequadas.

Mais tarde, ele explica como ele imagina todas as "cópias"

Você separa a interface do usuário das regras de negócios passando estruturas de dados simples entre as duas. Você não deixa seus controladores saberem nada sobre as regras de negócios. Em vez disso, os controladores descompactam o objeto HttpRequest em uma estrutura de dados simples de baunilha e passam essa estrutura de dados para um objeto de interação que implementa o caso de uso invocando objetos de negócios. O interator reúne os dados de resposta em outra estrutura de dados de baunilha e os passa de volta para a interface do usuário. As visualizações não sabem sobre os objetos de negócios. Eles apenas olham nessa estrutura de dados e apresentam a resposta.

Nesta aplicação, há uma grande diferença entre as representações. Os dados que fluem não são apenas as entidades. E isso garante e exige classes diferentes.

No entanto, aplicado a um aplicativo Android simples como um visualizador de fotos em que a Photoentidade possui cerca de 0 regras de negócios e o "caso de uso" que lida com elas é quase inexistente e está realmente mais preocupado com o cache e o download (esse processo deve ser IMO representado de forma mais explícita), o ponto de fazer representações separadas de uma foto começa a desaparecer. Tenho até a sensação de que a foto em si é o objeto de transferência de dados enquanto a camada principal da lógica de negócios está ausente.

Há uma diferença entre "separar a interface do usuário das regras de negócios passando estruturas de dados simples entre os dois" e "quando você deseja exibir uma foto, renomeie-a três vezes no caminho" .

Além disso, o ponto em que vejo esses aplicativos de demonstração falharem ao representar a arquitetura limpa é que eles acrescentam grande ênfase à separação de camadas para separá-las, mas ocultam efetivamente o que o aplicativo faz. Isso contrasta com o que é dito em https://blog.8thlight.com/uncle-bob/2011/09/30/Screaming-Architecture.html - ou seja,

a arquitetura de um aplicativo de software grita sobre os casos de uso do aplicativo

Não vejo essa ênfase na separação de camadas na arquitetura limpa. Trata-se da direção da dependência e do foco em representar o núcleo do aplicativo - entidades e casos de uso - em java idealmente simples, sem dependências externas. Não se trata tanto de dependências em relação a esse núcleo.

Portanto, se seu aplicativo realmente tiver um núcleo que represente regras de negócios e casos de uso e / ou pessoas diferentes trabalhem em camadas diferentes, separe-as da maneira pretendida. Por outro lado, se você está escrevendo um aplicativo simples, não exagere. 2 camadas com limites fluentes podem ser mais que suficientes. E camadas podem ser adicionadas mais tarde também.


1
@RamiJemli Idealmente, as entidades são as mesmas em todos os aplicativos. Essa é a diferença entre "regras de negócios em toda a empresa" e "regras de negócios de aplicativos" (às vezes lógica de negócios versus aplicativos). O núcleo é uma representação muito abstrata de suas entidades que é genérica o suficiente para que você possa usá-lo em qualquer lugar. Imagine um banco que tenha muitos aplicativos, um para suporte ao cliente, um rodando em caixas eletrônicos e outro como interface da web para os próprios clientes. Todos podem usar o mesmo, BankAccountmas com regras específicas do aplicativo, o que você pode fazer com essa conta.

4
Eu acho que um ponto importante na arquitetura limpa é que, usando a camada do adaptador de interface para converter (ou como você diz o mapa) entre a representação das diferentes camadas da entidade, você reduz a dependência dessa entidade. Se houver uma alteração nas camadas Usecase ou Entity (provavelmente improvável, mas conforme os requisitos mudarem, essas camadas serão), o impacto da alteração estará contido na camada do adaptador. Se você optar por usar a mesma representação da entidade em toda a sua arquitetura, o impacto dessa mudança seria muito maior.
SteveCallender

1
@RamiJemli, é bom usar estruturas que simplifiquem a vida, o ponto é que você deve ter cuidado quando sua arquitetura se basear nelas e começar a colocá-las no centro de tudo. Aqui está até um artigo sobre o RxJava blog.8thlight.com/uncle-bob/2015/08/06/let-the-magic-die.html - não está dizendo que você não deve usá-lo. É mais como: eu já vi isso, será diferente em um ano e quando seu aplicativo ainda estiver por perto, você ficará preso a ele. Faça um detalhe e faça as coisas mais importantes no java antigo simples, ao aplicar os princípios antigos do SOLID.
Zapl

1
@zapl Você sente o mesmo sobre uma camada de serviço da web? Em outras palavras, você colocaria @SerializedNameanotações do Gson em um modelo de domínio? Ou você criaria um novo objeto responsável pelo mapeamento da resposta da Web ao modelo de domínio?
tir38

2
@ tir38 A separação em si não oferece o benefício, é o custo de futuras mudanças que se reduzem. => Depende da aplicação. 1) custa tempo para criar e manter o estágio adicional que se transforma entre diferentes representações. Por exemplo, adicionar um campo ao domínio e esquecer de adicioná-lo em outro lugar não é algo inédito. Não pode acontecer com a abordagem simples. 2) Custa a transição para uma configuração mais complexa posteriormente, caso você precise. Adicionar camadas não é fácil, é, portanto, mais fácil em grandes aplicações para justificar mais camadas que não são necessários imediatamente
zapl

7

Você realmente acertou. E não há violação do DRY porque você aceita o SRP.

Por exemplo: Você possui um método comercial createX (nome da string) e, em seguida, um método createX (nome da string) na camada DAO, chamado dentro do método comercial. Eles podem ter a mesma assinatura e talvez exista apenas uma delegação, mas eles têm finalidades diferentes. Você também pode ter um createX (nome da string) no UseCase. Mesmo assim, não é redundante. O que quero dizer com isso é: As mesmas assinaturas não significam a mesma semântica. Escolha outros nomes para que a semântica seja clara. Nomear a si mesmo, isso não afeta o SRP.

O UseCase é responsável pela lógica específica do aplicativo, o objeto de negócios é responsável pela lógica independente do aplicativo e o DAO é responsável pelo armazenamento.

Devido à semântica diferente, todas as camadas podem ter seu próprio modelo de representação e comunicação. Muitas vezes, você vê "entidades" como "objetos de negócios" e, frequentemente, não vê a necessidade de separá-las. Mas em projetos "grandes", o esforço deve ser feito para separar adequadamente as camadas. Quanto maior o projeto, aumenta a possibilidade de que você precise das diferentes semânticas representadas em diferentes camadas e classes.

Você pode pensar em diferentes aspectos da mesma semântica. Um objeto de usuário deve ser exibido na tela, possui algumas regras de consistência interna e deve ser armazenado em algum lugar. Cada aspecto deve ser representado em uma classe diferente (SRP). Criar os mapeadores pode ser complicado, na maioria dos projetos em que trabalhei nesses aspectos são fundidos em uma classe. Isso é claramente uma violação do SRP, mas ninguém se importa.

Eu chamo a aplicação de arquitetura limpa e o SOLID "não socialmente aceitável". Eu trabalharia com isso se me fosse permitido. Atualmente não tenho permissão para fazê-lo. Espero o momento em que tivermos que pensar em levar o SOLID a sério.


Eu acho que nenhum método na camada de dados deve ter a mesma assinatura que qualquer método na camada de domínio. Na camada Domínio, você usa convenções de nomenclatura relacionadas aos negócios, como inscrição ou login, e, na camada Dados, usa save (se padrão DAO) ou adiciona (se Repository, porque esse padrão usa Collection como uma metáfora). Finalmente, não estou falando de entidades (dados) e modelo (domínio), estou destacando a inutilidade do UserModel e seu Mapper (camada de apresentação). Você pode chamar a classe User do domínio dentro da apresentação e isso não viola a regra de dependência.
Rami Jemli

Concordo com Rami, o mapeador é desnecessário porque você pode fazer o mapeamento diretamente na implementação do interator.
9309 Christopher Christopher

5

Não, você não precisa criar classes de modelo em todas as camadas.

Entidade ( DATA_LAYER) - é uma representação total ou parcial do objeto Banco de Dados.DATA_LAYER

Mapper ( DOMAIN_LAYER) - na verdade é uma classe que converte Entity em ModelClass, que será usada emDOMAIN_LAYER

Dê uma olhada: https://github.com/lifedemons/photoviewer


1
Obviamente, não sou contra entidades na camada de dados, mas, no seu exemplo, a classe PhotoModel na camada de apresentação tem os mesmos atributos da classe Photo na camada de domínio. É tecnicamente da mesma classe. isso é necessário?

Eu acho que algo está fora no seu exemplo como camada de domínio não deve depender de outras camadas como no seu exemplo, seus mapeadores dependem das entidades em sua camada de dados que IMO, deve-se vice-versa
navid_gh
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.