Stack Overflow em Português Asked on December 16, 2021
Em orientação a objetos existe o conceito do encapsulamento: reunir em uma mesma classe estado e as funções que operam sobre esse estado.
Porém há situações em que é útil separar estado e comportamento. O seguinte exemplo chegou até mim:
Numa compra feita em um e-commerce como a Amazon, deve existir uma taxa de envio? Essa decisão se baseia tanto no objeto Cliente (trata-se de um cliente Amazon Prime ou não) como também nos itens sendo comprados (se são todos da Amazon, não há custos de envio, porém se o item é de um outro fornecedor, então há uma taxa de envio).
Você não pode colocar esses comportamentos no objeto Cliente ou no Item… então você coloca fora de ambos os objetos, em um objeto que faz checagem de constraints (restrições) e que é parte do fluxo de trabalho de apresentar o preço para o usuário.
Em domain-driven design existe um objeto em particular chamado de "constraint" ou uma especificação. É uma interface bastante estilizada, onde você passa um objeto do domínio de negócio e o objeto-constraint responde a uma pergunta ehSatisfeitaPor(objetoDeDominio) como um resultado booleano.
Isso é útil quando você tem diversas checagens diferentes no estado do objeto de negócio que você quer encapsuladas em um único objeto (a constraint) que tem um conjunto muito específico de regras de negócio.
Você poderia da mesma forma colocar esses comportamentos no objeto de negócio, mas então teria uma lista sempre crescente dos mesmos. Por outro lado, o que há de errado nisso, você pode se perguntar? Bem, se você não quer abarrotar um objeto do domínio do negócio com regras de processamento específicas (que podem ter que mudar dinamicamente baseadas em diferentes fluxos de trabalho), então é melhor isolar essas checagens em diferentes objetos constraints.
Ou, conforme recomenda o domain-driven design, você pode também querer separar esses métodos ehSatisfeitaPor() em objetos distintos quando dois objetos de negócio estão envolvidos e você não quer colocar arbitrariamente o comportamento em um dos objetos de domínio.
Se em algum momento você se encontrar escrevendo código que está checando uma coisa antes de realizar o trabalho, então pergunte a si mesmo se não seria interessante ter um objeto-constraint para responder à pergunta.
A pergunta que quero fazer é: em quais outras situações é útil separar estado de comportamento? O final do exemplo já meio que responde à pergunta, mas não cita nenhum exemplo específico.
Outro exemplo que aparentemente cai no mesmo caso é o do EmbeddedDocument citado pelo Martin Fowler no seu artigo sobre o princípio Tell Don’t Ask, porém o exemplo está em Ruby e eu não consegui compreendê-lo.
P.S.: Quem quiser transformar os exemplos em código para ilustrar a situação, acredito que também é uma contribuição útil à pergunta.
Eu sou favorável à separação, a tal ponto que muitas vezes a melhor forma nem é orientada a objeto, quanto mais separado mais você consegue compor. Essa ideia de juntar tudo atrapalha a composição que é a base para facilitar a manutenção. Tudo feito para funcionar bem com componentes diferentes, ter encaixes fáceis e flexíveis.
Não sei falar assim em quais situações é útil porque pra mim é sempre útil. Você junta o que não pode ficar fora, onde o comportamento está muito vinculado ao estado, que a forma como é feita tem que ser muito detalhe de implementação do objeto, e que não tenha contexto.
Vou até dizer que prefiro vazar abstração e expor o que não deve para separar certas partes, o que vai contra o que os proponentes de OOP dizem que é para fazer. É muito mais fácil dar manutenção assim, até mesmo, ou melhor ainda, quando não sabe o que virá pela frente, o que é o mais comum acontecer.
Não pode radicalizar, não pode ser ideológico. Modelo anêmico pode ser ótimo, mas pode não ser a solução. Muitas vezes opta-se por ele para agradar um mecanismo específico, e não porque a modelagem exige.
O exemplo citado é ótimo e as pessoas vivem quebrando a cara porque sempre muda alguma coisa no projeto. E tenho visto soluções malucas para atender a teoria que criaram de como tem que fazer, quando muitas vezes, e nem digo que é o caso do exemplo, uma função solta é a solução.
Quanto mais estudo DDD mais vejo a utilidade, e que a ideia é muito boa e vai na direção certa, ou seja, vai contra muito do que OOP prega (pelo menos alguns pontos). Porém também vejo que é tanto código, tanta camada, tanto conceito, tanto penduricalho, que você perde tempo demais com isso, você começa ter mecanismos demais e lógica de negócio de menos, o que é o oposto que o DDD prega. DDD só deveria ser aplicado, e sem a parte ideológica, que tem, se tiver um framework muito bom, o que estou tentando achar e não apareceu pra mim até agora.
ehSatisfeitaPor()
é mecanismo, será que precisa? Será que não é só burocracia? Precisa desta flexibilidade? Ela é suficiente? Depende. Novamente acho preocupação demais com mecanismo. É um caso abstrato demais, então tem que fazer e ver se precisa, e tem que refatorar. É importante ser fácil refatorar quando algo não deu certo. Quando junta tudo é mais complicado separar, e o que é separado é mais fácil juntar.
Constraints podem ser contextuais, o que complica. Sei que DDD tem algo sobre contexto e isso é legal, mas começa complicar, será que precisa?
Não estou dizendo que não deva usar assim, só questionar se é a solução, se agrega ou está só seguindo receita de bolo.
Eu acho que há exagero na dinamicidade que se dá em muitas aplicações, muitas vezes em nome de testes, de DDD, de princípios que a pessoa nem sabe dizer porque está usando.
Eu costumo dizer que existem dois motivos para resolver algo em tempo de execução:
Um terceiro motivo é a comodidade, e ela é válida em alguns casos, mas frequentemente é abusada.
O exemplo parece abusar um pouco disto, DDD parece abusar. Você acaba fazendo muito late binding só porque quer seguir a moda. Mas se for útil, então use uma linguagem que incentive isto.
Tem hora que Lego demais começa complicar também. Por isso estou adorando as linguagens que permitem composição de um jeito mais fácil, que permite mais coesão e menos acoplamento. Coisa que o procedural sempre foi bom, tanto que estes dois conceitos existem antes de OOP. Hoje quem defende muito OOP não viu outra coisa, ouviu falar que procedural era ruim, e era mesmo, porque era mal feito. Por isso o meio termo é mais interessante.
Metaprogramação é algo que está começando conseguir facilitar a manutenção, manter performance e diminuir a complexidade, mas ainda é cedo pra comemorar.
Inclusive generics muitas vezes resolve melhor. Permite mais composição. O pessoal do C++ dizem que se tivessem pensado nisso antes nem precisaria ter OO na linguagem.
É comum usar lambda para injetar comportamento quando se sabe que ele existe, mas o detalhes deve ser configurável no objeto.
E já fui mais adepto de certa flexibilidade. Ainda sou, sob certa linha, mas qualquer coisa que que dependa de estrutura precisa de um engenheiro muito bom cuidando. Tem empresa que tem engenheiro e comitê pra escolher o código de um produto, um por um individualmente, de tão importante que é fazer boas escolhas com coisas que você vai "casar" pra sempre. Hoje pra mim esses exemplos em Ruby são coisas horrorosas e desnecessárias de fato.
A flexibilidade tem que vir da arquitetura e não da solução. Precisa ser fácil adaptar, não fácil metamorfosear :) Nem tente fazer isso em Java, ou em casa :D O exemplo do documento embarcado é cheio de ideologia e nem sei se tem alguma coisa a ver com a pergunta aqui.
Pegue a biblioteca moderna do C++, quase tudo ali é tentando separar. As estruturas de dados estão cada vez mais "peladas" e existem bibliotecas fora que manipulam-nas sempre que ela tenha uma determinada característica. Exemplo.
Pega o LINQ do C#, tudo separado, é tudo composição, é tudo uma abstração para lidar com diversos objetos que tenham pelo menos uma coisa em comum. Documentação. O mesmo vale para os Streams do Java.
Se as pessoas observarem, um monte de coisa é naturalmente separada sem se dar conta, porque a decisão deve ser natural, não impositiva por uma metodologia ou paradigma.
Percebe que a maioria dos padrões de projeto mais conhecidos, desses que todo mundo fala, incentivam separação e não junção? Um exemplo muito comum é o Strategy (onde o exato comportamento não é definido no objeto), ou o Visitor (que nem que comportamento pode ter), mas todos os comportamentais e estruturais são muito fortes nisso.
Bem feito, alguns DPs são desnecessários. E tem mecanismos que os tornam mais desnecessários. Paul Graham e Peter Novig já diziam isso. Ou pelo menos tem formas mais simples de fazer.
Veja mais sobre em Policy-based design.
Por isso que eu falo que Cliente ou Fornecedor não são derivações de Pessoa, são facetas da Pessoa, são partes opcionais, eventuais da Pessoa. O que pode ter hierarquia é Pessoa Jurídica e Física, que são sempre Pessoas. E Pessoa nunca existe de fato, é só taxonomia, é abstrato.
Quase toda herança deve ser abstrata (alguns dizem toda). Orientação a objeto prega que deve ter algo como um esqueleto e derivações devem conter os detalhes. Mas tem combinação demais, aí não funciona. Já falei disso para o AP em outra resposta.
Note que esse exemplo não é só o comportamento separado, tem estado também, por exemplo: limite de crédito é um estado que está em Cliente, mas não em Fornecedor, e não em Pessoa. Agora pode estar pensando que não importa. Mas como uma Pessoa pode ser um Cliente e um Fornecedor ao mesmo tempo, para conciliar as duas em um objeto só vira uma bagunça completa, por outro lado se fizer separado a mesma pessoa existirá duas vezes no sistema. Tudo errado porque não modela o mundo real, e os exemplos fundamentais de OO incentivam esse erro.
Inclusive andam falando para não modelar o mundo real. Quando faz isso flerta com o desastre porque o mundo real muda e seu sistema não está preparado para tanto. É o que sempre falo, difícil é enxergar o mundo real.
O artigo é quase bobo, pelo menos para quem não é muito novato, e o novato comprará aquilo como o holy graal, o que não é. Também não sei se tem a ver com a pergunta.
Até onde sei, DDD prega evitar getter/setter :D
Se o comportamento não é visceralmente ligado ao estado e não pode estar separado sem ter um preço a pagar, deve ficar separado, a não ser por alguma conveniência, o que é preciso muito cuidado. Há metodologias ou guidelines que pregam exatamente isso.
No geral tudo isso faz sentido? Veja outras visões, não só a minha.
Note que arquitetura perfeita não existe, a não ser em casos muito triviais.
Eu acho meio amplo transcrever códigos e não sei se serve para algo. Para exemplos mais concretos precisa de pergunta mais específica.
Answered by Maniero on December 16, 2021
Get help from others!
Recent Questions
Recent Answers
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP