Uma implementação típica de um repositório DDD não parece muito OO, por exemplo, um save()
método:
package com.example.domain;
public class Product { /* public attributes for brevity */
public String name;
public Double price;
}
public interface ProductRepo {
void save(Product product);
}
Parte da infraestrutura:
package com.example.infrastructure;
// imports...
public class JdbcProductRepo implements ProductRepo {
private JdbcTemplate = ...
public void save(Product product) {
JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)",
product.name, product.price);
}
}
Essa interface espera Product
que seja um modelo anêmico, pelo menos com getters.
Por outro lado, o OOP diz que um Product
objeto deve saber como se salvar.
package com.example.domain;
public class Product {
private String name;
private Double price;
void save() {
// save the product
// ???
}
}
O problema é que, quando ele Product
sabe como se salvar, significa que o código da infraestrutura não é separado do código do domínio.
Talvez possamos delegar a gravação em outro objeto:
package com.example.domain;
public class Product {
private String name;
private Double price;
void save(Storage storage) {
storage
.with("name", this.name)
.with("price", this.price)
.save();
}
}
public interface Storage {
Storage with(String name, Object value);
void save();
}
Parte da infraestrutura:
package com.example.infrastructure;
// imports...
public class JdbcProductRepo implements ProductRepo {
public void save(Product product) {
product.save(new JdbcStorage());
}
}
class JdbcStorage implements Storage {
private final JdbcTemplate = ...
private final Map<String, Object> attrs = new HashMap<>();
private final String tableName;
public JdbcStorage(String tableName) {
this.tableName = tableName;
}
public Storage with(String name, Object value) {
attrs.put(name, value);
}
public void save() {
JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)",
attrs.get("name"), attrs.get("price"));
}
}
Qual é a melhor abordagem para conseguir isso? É possível implementar um repositório orientado a objetos?
Respostas:
Você escreveu
e em um comentário.
Este é um mal-entendido comum.
Product
é um objeto de domínio, por isso deve ser responsável pelas operações de domínio que envolvem um único objeto de produto, nem menos, nem mais - então definitivamente não é para todas as operações. Normalmente, a persistência não é vista como uma operação de domínio. Muito pelo contrário, em aplicativos corporativos, não é incomum tentar obter ignorância da persistência no modelo de domínio (pelo menos até certo ponto), e manter a mecânica da persistência em uma classe de repositório separada é uma solução popular para isso. "DDD" é uma técnica que visa esse tipo de aplicação.Então, o que poderia ser uma operação de domínio sensata para a
Product
? Isso depende, na verdade, do contexto do domínio do sistema de aplicativos. Se o sistema é pequeno e suporta apenas operações CRUD exclusivamente, então, de fato, aProduct
pode permanecer bastante "anêmico", como no seu exemplo. Para esse tipo de aplicativo, pode ser discutível se colocar as operações do banco de dados em uma classe de repo separada ou usar DDD, vale a pena.No entanto, assim que seu aplicativo suporta operações de negócios reais, como comprar ou vender produtos, mantê-los em estoque e gerenciá-los ou calcular impostos para eles, é bastante comum que você comece a descobrir operações que podem ser colocadas de maneira sensata em uma
Product
classe. Por exemplo, pode haver uma operaçãoCalcTotalPrice(int noOfItems)
que calcule o preço de `n itens de um determinado produto ao considerar os descontos por volume.Portanto, quando você cria classes, precisa pensar em seu contexto, em qual dos cinco mundos de Joel Spolsky você está e se o sistema contém lógica de domínio suficiente para que o DDD seja benéfico. Se a resposta for sim, é bastante improvável que você acabe com um modelo anêmico apenas porque mantém a mecânica de persistência fora das classes de domínio.
fonte
Account.transfer(amount)
deve persistir a transferência. Como isso é responsabilidade do objeto, não de alguma entidade externa. A exibição do objeto, por outro lado, geralmente é uma operação de domínio! Os requisitos geralmente descrevem em grande detalhe como as coisas devem parecer. Faz parte do idioma entre os membros do projeto, negócios ou outros.Account.transfer
geralmente envolve dois objetos de conta e uma unidade de objeto de trabalho. A operação persistente transacional pode fazer parte desta última (juntamente com chamadas para repos relacionados), portanto, fica fora do método "transfer". Dessa forma,Account
pode permanecer ignorante a persistência. Não estou dizendo que isso seja necessariamente melhor do que sua suposta solução, mas a sua também é apenas uma das várias abordagens possíveis.Pratique a teoria dos trunfos.
A experiência nos ensina que Product.Save () leva a muitos problemas. Para contornar esses problemas, inventamos o padrão de repositório.
Claro que quebra a regra OOP de ocultar os dados do produto. Mas funciona bem.
É muito mais difícil criar um conjunto de regras consistentes que cubram tudo do que criar algumas boas regras gerais com exceções.
fonte
É bom lembrar que não há a intenção de haver tensão entre essas duas idéias - objetos de valor, agregados, repositórios são uma série de padrões usados, o que alguns consideram ser OOP feito corretamente.
Não tão. Os objetos encapsulam suas próprias estruturas de dados. Sua representação na memória de um Produto é responsável por exibir comportamentos do produto (sejam eles quais forem); mas o armazenamento persistente está lá (atrás do repositório) e tem seu próprio trabalho a fazer.
É necessário que haja alguma maneira de copiar dados entre a representação em memória do banco de dados e sua lembrança persistente. No limite , as coisas tendem a ficar bem primitivas.
Fundamentalmente, os bancos de dados somente de gravação não são particularmente úteis e seus equivalentes na memória não são mais úteis que a classificação "persistente". Não há sentido em colocar informações em um
Product
objeto se você nunca vai tirar essas informações. Você não usará necessariamente "getters" - não está tentando compartilhar a estrutura de dados do produto e certamente não deve compartilhar acesso mutável à representação interna do Produto.Isso certamente funciona - seu armazenamento persistente se torna efetivamente um retorno de chamada. Eu provavelmente tornaria a interface mais simples:
Não está indo a ser acoplamento entre a representação em memória e o mecanismo de armazenamento, porque a informação precisa para chegar daqui para lá (e vice-versa). Alterar as informações a serem compartilhadas afetará os dois lados da conversa. Então, podemos também deixar isso explícito onde pudermos.
Essa abordagem - passando dados por retornos de chamada, desempenhou um papel importante no desenvolvimento de zombarias no TDD .
Observe que passar as informações para o retorno de chamada tem as mesmas restrições que retornar as informações de uma consulta - você não deve passar cópias mutáveis de suas estruturas de dados.
Essa abordagem é um pouco contrária ao que Evans descreveu no Blue Book, onde o retorno de dados por meio de uma consulta era a maneira normal de realizar as coisas, e os objetos de domínio foram projetados especificamente para evitar misturar "preocupações de persistência".
Uma coisa a ter em mente - o Blue Book foi escrito quinze anos atrás, quando o Java 1.4 vagava pela terra. Em particular, o livro é anterior aos genéricos de Java - temos muito mais técnicas disponíveis para nós agora do que quando Evans estava desenvolvendo suas idéias.
fonte
Storage
interface da mesma maneira que você, depois considerei o acoplamento alto e mudei. Mas você está certo, há um acoplamento inevitável de qualquer maneira, então por que não torná-lo mais explícito?Muito boas observações, concordo plenamente com você. Aqui está uma palestra minha (correção: apenas slides) sobre exatamente esse assunto: Design Orientado a Domínio Orientado a Objetos .
Resposta curta: não. Não deve haver um objeto em seu aplicativo que seja puramente técnico e não tenha relevância no domínio. É como implementar a estrutura de log em um aplicativo de contabilidade.
Seu
Storage
exemplo de interface é excelente, assumindo que,Storage
então, é considerada alguma estrutura externa, mesmo se você a escrever.Além disso,
save()
em um objeto só deve ser permitido se isso fizer parte do domínio (o "idioma"). Por exemplo, eu não deveria ser obrigado a "salvar" explicitamenteAccount
depois que eu ligartransfer(amount)
. Eu deveria esperar, com razão, que a função comercialtransfer()
persistisse na minha transferência.Em suma, acho que as idéias do DDD são boas. Usando linguagem onipresente, exercitando o domínio com conversas, contextos limitados, etc. Os blocos de construção, no entanto, precisam de uma revisão séria para serem compatíveis com a orientação a objetos. Veja o deck vinculado para detalhes.
fonte
AccountNumber
deve saber que pode ser representado como aTextField
. Se outros (como uma "Visualização") souberem disso, isso é acoplamento que não deveria existir, porque esse componente precisaria saber o queAccountNumber
consiste, ou seja, os internos.Evite espalhar o conhecimento dos campos desnecessariamente. Quanto mais coisas soubermos sobre um campo individual, mais difícil será adicionar ou remover um campo:
Aqui, o produto não faz ideia se você está salvando em um arquivo de log ou em um banco de dados ou em ambos. Aqui, o método save não faz ideia se você possui 4 ou 40 campos. Isso é fracamente acoplado. É uma coisa boa.
Claro que este é apenas um exemplo de como você pode alcançar esse objetivo. Se você não gosta de criar e analisar uma string para usar como seu DTO, também pode usar uma coleção.
LinkedHashMap
é um dos meus favoritos antigos, pois preserva a ordem e é toString () que fica bem em um arquivo de log.Seja como for, não espalhe o conhecimento dos campos. Essa é uma forma de acoplamento que as pessoas geralmente ignoram até que seja tarde demais. Quero que o mínimo de coisas saiba estaticamente quantos campos meu objeto tem quanto possível. Dessa forma, adicionar um campo não envolve muitas edições em muitos lugares.
fonte
Map
, você propõe umString
ou umList
. Mas, como o @VoiceOfUnreason mencionado em sua resposta, o acoplamento ainda está lá, mas não é explícito. Ainda é desnecessário conhecer a estrutura de dados do produto para salvá-lo em um banco de dados ou em um arquivo de log, pelo menos quando lidos novamente como um objeto.Storage
faz parte do domínio (assim como a interface do repositório) e cria uma API de persistência. Quando alterado, é melhor informar os clientes no tempo de compilação, porque eles precisam reagir de qualquer maneira para não serem interrompidos no tempo de execução.Existe uma alternativa aos padrões já mencionados. O padrão Memento é ótimo para encapsular o estado interno de um objeto de domínio. O objeto memento representa uma captura instantânea do estado público do objeto de domínio. O objeto de domínio sabe como criar esse estado público a partir do seu estado interno e vice-versa. Um repositório então trabalha apenas com a representação pública do estado. Com isso, a implementação interna é dissociada de quaisquer detalhes de persistência e apenas precisa manter o contrato público. Além disso, seu objeto de domínio não deve expor nenhum getter que de fato o tornaria um pouco anêmico.
Para mais informações sobre este tópico, recomendo o grande livro: "Padrões, Princípios e Práticas de Design Orientado a Domínios", de Scott Millett e Nick Tune
fonte