Ainda é válido falar sobre o modelo anêmico no contexto da programação funcional?

40

A maioria dos padrões de design tático DDD pertence ao paradigma orientado a objetos, e o modelo anêmico descreve a situação em que toda a lógica de negócios é colocada em serviços, em vez de objetos, tornando-os uma espécie de DTO. Em outras palavras, modelo anêmico é sinônimo de estilo procedural, o que não é recomendado para modelos complexos.

Não tenho muita experiência em programação funcional pura, mas gostaria de saber como o DDD se encaixa no paradigma do FP e se o termo 'modelo anêmico' ainda existe nesse caso.

Atualização : Recentlry publicada livro e vídeo sobre o assunto.

Pavel Voronin
fonte
11
Se você está dizendo o que acho que está dizendo aqui, os DTOs são anêmicos, mas são objetos de primeira classe no DDD, e há uma separação natural entre os DTOs e os serviços que os processam. Eu concordo em princípio. O mesmo acontece com este post do blog , aparentemente.
Robert Harvey
2
Fortemente relacionado, se não uma duplicata direta: Por que um modelo de domínio anêmico é considerado ruim na OOP, mas muito importante na FP?
Robert Harvey
11
"se o termo 'modelo anêmico' ainda existe nesse caso" Resposta curta, o termo do modelo anêmico foi cunhado no contexto de OO. Falar de um modelo anêmico no contexto da PF não faz nenhum sentido. Pode haver equivalentes no sentido de descrever o que é FP idiomático, mas não tem nada a ver com modelos anêmicos.
plalx
5
Uma vez perguntaram a Eric Evans o que ele diz às pessoas que o acusam de que o que ele descreve em seu livro é apenas um bom design orientado a objetos, e ele respondeu que não é uma acusação, é a verdade, DDD é apenas um bom OOD, ele apenas escreveu descreva algumas receitas e padrões e nomeie-os, para que seja mais fácil segui-los e falar sobre eles. Portanto, não é surpresa que o DDD esteja vinculado ao OOD. A questão mais ampla seria: quais são as interseções e diferenças entre OOD e FPD, embora você precise definir o que quer dizer com "programação funcional" primeiro.
Jörg W Mittag
2
@ JörgWMittag: Você quer dizer diferente da definição usual? Existem muitas plataformas ilustrativas, sendo Haskell a mais óbvia.
Robert Harvey

Respostas:

24

A maneira como o problema do "modelo anêmico" é descrito não se traduz bem no FP como está. Primeiro, ele precisa ser adequadamente generalizado. No fundo, um modelo anêmico é um modelo que contém conhecimento sobre como usá-lo adequadamente, que não é encapsulado pelo próprio modelo. Em vez disso, esse conhecimento está espalhado em torno de uma pilha de serviços relacionados. Esses serviços devem ser apenas clientes do modelo, mas devido à sua anemia, eles são responsabilizados por ele. Por exemplo, considere uma Accountclasse que não pode ser usada para ativar ou desativar contas ou mesmo procurar informações sobre uma conta, a menos que seja tratada por meio de uma AccountManagerclasse. A conta deve ser responsável por operações básicas, não por alguma classe de gerente externo.

Na programação funcional, existe um problema semelhante quando os tipos de dados não representam com precisão o que devem modelar. Suponha que precisamos definir um tipo representando IDs do usuário. Uma definição "anêmica" indicaria que os IDs do usuário são cadeias. Isso é tecnicamente viável, mas enfrenta grandes problemas porque os IDs de usuário não são usados ​​como sequências arbitrárias. Não faz sentido concatená-los ou cortar subcadeias deles, o Unicode não deve realmente importar e deve ser facilmente incorporado em URLs e outros contextos com limitações estritas de caracteres e formatos.

A solução desse problema geralmente ocorre em algumas etapas. Um primeiro corte simples é dizer "Bem, a UserIDé representado equivalentemente a uma string, mas são tipos diferentes e você não pode usar um onde espera o outro". Haskell (e algumas outras linguagens funcionais digitadas) fornece esse recurso via newtype:

newtype UserID = UserID String

Isso define uma UserIDfunção que, quando fornecida, Stringconstrói um valor que é tratado como um UserIDpelo sistema de tipos, mas que ainda é apenas um Stringem tempo de execução. Agora, as funções podem declarar que exigem UserIDuma sequência em vez de uma sequência; usando UserIDs em que você estava usando cordas guardas contra o código que concatena duasUserID s juntos. O sistema de tipos garante que isso não pode acontecer, sem a necessidade de testes.

A fraqueza aqui é que o código ainda pode pegar qualquer Stringtipo arbitrário "hello"e construir um a UserIDpartir dele. Outras etapas incluem a criação de uma função "construtor inteligente" que, quando recebe uma string, verifica alguns invariantes e retorna apenas um UserIDse estiver satisfeito. Em seguida, o UserIDconstrutor "burro" é tornado privado, portanto, se um cliente quiser umUserID ele deve usar o construtor inteligente, impedindo que IDs de usuário malformados passem a existir.

Outras etapas definem o UserIDtipo de dados de tal maneira que é impossível construir um que esteja malformado ou "impróprio", simplesmente por definição. Por exemplo, definindo a UserIDcomo uma lista de dígitos:

data Digit = Zero | One | Two | Three | Four | Five | Six | Seven | Eight | Nine
data UserID = UserID [Digit]

Para construir uma UserIDlista de dígitos deve ser fornecido. Dada essa definição, é trivial mostrar que é impossível UserIDexistir um que não possa ser representado em um URL. A definição de modelos de dados como este em Haskell geralmente é auxiliada por recursos avançados do sistema de tipos, como tipos de dados e tipos de dados algébricos generalizados (GADTs) , que permitem ao sistema de tipos definir e provar mais invariantes sobre seu código. Quando os dados são dissociados do comportamento, sua definição de dados é o único meio de você impor o comportamento.

Jack
fonte
2
E quanto aos agregados e raízes agregadas que protegem invariáveis?, Os últimos também podem ser expressos e facilmente compreendidos pelos desenvolvedores posteriormente? Para mim, a parte mais valiosa do DDD é um mapeamento direto do modelo de negócios para o código. E você responde é exatamente sobre isso.
Pavel Voronin
2
Bom discurso, mas não há resposta para a pergunta do OP.
SerG 12/09
10

Em grande medida, a imutabilidade torna desnecessário associar firmemente suas funções aos seus dados, como advogam o OOP. Você pode fazer quantas cópias quiser, até criando estruturas de dados derivadas, em códigos muito distantes do código original, sem medo de que a estrutura de dados original seja inesperadamente alterada sob você.

No entanto, uma maneira melhor de fazer essa comparação é provavelmente examinar quais funções você está alocando para a camada de modelo versus a camada de serviços . Mesmo que não pareça o mesmo que no OOP, é um erro bastante comum no FP tentar agrupar o que deveria ser vários níveis de abstração em uma função.

Até onde eu sei, ninguém o chama de modelo anêmico, pois esse é um termo OOP, mas o efeito é o mesmo. Você pode e deve reutilizar funções genéricas sempre que aplicável, mas para operações mais complexas ou específicas de aplicativos, você também deve fornecer um rico conjunto de funções apenas para trabalhar com seu modelo. Criar camadas de abstração apropriadas é um bom design em qualquer paradigma.

Karl Bielefeldt
fonte
2
"Em grande medida, a imutabilidade torna desnecessário acoplar suas funções aos seus dados como advogados da OOP.": Outro motivo para acoplar dados e procedimentos é implementar o polimorfismo por meio de despacho dinâmico.
Giorgio
2
A principal vantagem do acoplamento do comportamento aos dados no contexto do DDD é fornecer uma interface significativa relacionada aos negócios; está sempre à mão. Temos uma maneira natural de auto-documentar código (pelo menos é o que estou acostumado), e essa é a chave para uma comunicação bem-sucedida com especialistas em negócios. Como então é realizado no FP? Provavelmente a tubulação ajuda, mas o que mais? A natureza genérica do FP dificulta os requisitos de negócios para fazer a engenharia reversa a partir do código?
Pavel Voronin
7

Ao usar o DDD no OOP, um dos principais motivos para colocar a lógica de negócios nos próprios objetos de domínio é que a lógica de negócios geralmente é aplicada pela mutação do estado do objeto. Isso está relacionado ao encapsulamento: Employee.RaiseSalaryprovavelmente modifica o salarycampo da Employeeinstância, que não deve ser publicamente configurável.

No FP, a mutação é evitada; portanto, você implementaria esse comportamento criando uma RaiseSalaryfunção que pega uma Employeeinstância existente e retorna uma nova Employee instância com o novo salário. Portanto, nenhuma mutação está envolvida: apenas lendo o objeto original e criando o novo objeto. Por esse motivo, essa RaiseSalaryfunção não precisa ser definida como um método na Employeeclasse, mas pode estar em qualquer lugar.

Nesse caso, torna-se natural separar os dados do comportamento: uma estrutura representa os Employeedados (completamente anêmicos), enquanto um (ou vários) módulos contêm funções que operam nesses dados (preservando a imutabilidade).

Observe que, quando você associa dados e comportamento como no DDD, geralmente viola o Princípio de Responsabilidade Única (SRP): Employeepode ser necessário mudar se as regras para alterações salariais mudarem; mas também pode ser necessário alterar se as regras para o cálculo do bônus EOY mudarem. Com a abordagem dissociada, esse não é o caso, pois você pode ter vários módulos, cada um com uma responsabilidade.

Portanto, como sempre, a abordagem FP fornece maior modularidade / composibilidade.

la-yumba
fonte
-1

Eu acho que a essência da questão é que um modelo anêmico com toda a lógica de domínio nos serviços que operam no modelo é basicamente programação procedural - em oposição à programação OO "real", na qual você tem objetos "inteligentes" e não apenas dados mas também a lógica mais intimamente ligada aos dados.

E o mesmo contraste existe com a programação funcional: FP "real" significa usar funções como entidades de primeira classe, que são passadas como parâmetros, assim como construídas em tempo real e retornadas como valor de retorno. Mas quando você falha em usar todo esse poder e possui apenas funções que operam nas estruturas de dados que são passadas entre eles, você acaba no mesmo local: basicamente está fazendo programação procedural.

Michael Borgwardt
fonte
5
Sim, é basicamente o que o OP diz em sua pergunta. Parece que vocês dois não entenderam que ainda podem ter composição funcional.
Robert Harvey
-3

Eu gostaria de saber como o DDD se encaixa no paradigma do FP

Eu acho que sim, mas em grande parte como uma abordagem tática para fazer a transição entre objetos de valor imutáveis ​​ou como uma maneira de acionar métodos em entidades. (Onde a maior parte da lógica ainda vive na entidade.)

e se o termo "modelo anêmico" ainda existe nesse caso.

Bem, se você quer dizer "de maneira análoga à OOP tradicional", ajuda a ignorar os detalhes de implementação usuais e voltar ao básico: que idioma seus especialistas em domínio usam? Que intenção você está capturando de seus usuários?

Supondo que eles falem sobre encadear processos e funções juntos, parece que funções (ou pelo menos objetos "executores") são basicamente seus objetos de domínio!

Portanto, nesse cenário, um "modelo anêmico" provavelmente ocorreria quando suas "funções" não forem realmente executáveis ​​e, em vez disso, são apenas constelações de metadados que são interpretados por um Serviço que faz o trabalho real.

Darien
fonte
11
Um modelo anêmico ocorreria quando você passava tipos abstratos de dados, como tuplas, registros ou listas, para diferentes funções de processamento. Você não precisa de nada tão exótico quanto uma "função que não executa" (seja lá o que for).
Robert Harvey
Daí as aspas em torno de "funções", para enfatizar o quão inadequado o rótulo se torna quando eles são anêmicos.
Darien
Se você está sendo irônico, é um pouco sutil.
Robert Harvey