Una implementación típica de un repositorio DDD no se ve muy OO, por ejemplo, un 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 de infraestructura:
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);
}
}
Dicha interfaz espera que Product
a sea un modelo anémico, al menos con getters.
Por otro lado, OOP dice que un Product
objeto debe saber cómo salvarse.
package com.example.domain;
public class Product {
private String name;
private Double price;
void save() {
// save the product
// ???
}
}
La cuestión es que, cuando Product
sabe cómo salvarse, significa que el código de infraestructura no está separado del código de dominio.
Tal vez podamos delegar el guardado a otro 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 de infraestructura:
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"));
}
}
¿Cuál es el mejor enfoque para lograr esto? ¿Es posible implementar un repositorio orientado a objetos?
Respuestas:
Tu escribiste
y en un comentario
Este es un malentendido común.
Product
es un objeto de dominio, por lo que debe ser responsable de las operaciones de dominio que involucran un solo objeto de producto, ni menos, ni más, por lo que definitivamente no es para todas las operaciones. Por lo general, la persistencia no se ve como una operación de dominio. Todo lo contrario, en aplicaciones empresariales, no es raro tratar de lograr la ignorancia de persistencia en el modelo de dominio (al menos hasta cierto punto), y mantener la mecánica de persistencia en una clase de repositorio separada es una solución popular para esto. "DDD" es una técnica que apunta a este tipo de aplicaciones.Entonces, ¿qué podría ser una operación de dominio sensible para a
Product
? Esto depende en realidad del contexto de dominio del sistema de aplicación. Si el sistema es pequeño y solo admite operaciones CRUD exclusivamente, entonces, de hecho,Product
puede permanecer bastante "anémico" como en su ejemplo. Para este tipo de aplicaciones, puede ser discutible si vale la pena poner las operaciones de la base de datos en una clase de repositorio separada, o usar DDD.Sin embargo, tan pronto como su aplicación admita operaciones comerciales reales, como comprar o vender productos, mantenerlos en existencia y administrarlos, o calcular impuestos para ellos, es bastante común que comience a descubrir operaciones que pueden ubicarse de manera sensata en una
Product
clase. Por ejemplo, podría haber una operaciónCalcTotalPrice(int noOfItems)
que calcule el precio de `n artículos de un determinado producto cuando se tienen en cuenta los descuentos por volumen.En resumen, cuando diseñas clases, debes pensar en tu contexto, en cuál de los cinco mundos de Joel Spolsky te encuentras y si el sistema contiene suficiente lógica de dominio, por lo que DDD será beneficioso. Si la respuesta es sí, es bastante improbable que termine con un modelo anémico solo porque mantiene la mecánica de persistencia fuera de las clases de dominio.
fuente
Account.transfer(amount)
debe persistir la transferencia. Cómo lo hace es responsabilidad del objeto, no de alguna entidad externa. ¡Mostrar el objeto por otro lado suele ser una operación de dominio! Los requisitos generalmente describen con gran detalle cómo deberían verse las cosas. Es parte del lenguaje entre los miembros del proyecto, negocios u otros.Account.transfer
para generalmente involucra dos objetos de cuenta y una unidad de objeto de trabajo. La operación persistente transaccional podría ser parte de esta última (junto con las llamadas a repositorios relacionados), por lo que queda fuera del método de "transferencia". De esa manera,Account
puede permanecer ignorante de la persistencia. No digo que esto sea necesariamente mejor que su supuesta solución, pero la suya también es solo uno de los varios enfoques posibles.La práctica triunfa sobre la teoría.
La experiencia nos enseña que Product.Save () genera muchos problemas. Para sortear esos problemas, inventamos el patrón de repositorio.
Seguro que rompe la regla de OOP de ocultar los datos del producto. Pero funciona bien.
Es mucho más difícil hacer un conjunto de reglas consistentes que cubran todo lo que es hacer algunas buenas reglas generales que tengan excepciones.
fuente
Es útil tener en cuenta que no se pretende que haya tensión entre estas dos ideas: los objetos de valor, los agregados, los repositorios son una serie de patrones utilizados, lo que algunos consideran que es POO bien hecho.
No tan. Los objetos encapsulan sus propias estructuras de datos. Su representación en memoria de un Producto es responsable de exhibir comportamientos del producto (cualesquiera que sean); pero el almacenamiento persistente está allí (detrás del repositorio) y tiene su propio trabajo que hacer.
Es necesario que haya alguna forma de copiar datos entre la representación en memoria de la base de datos y su recuerdo persistente. En el límite , las cosas tienden a ponerse bastante primitivas.
Básicamente, las bases de datos de solo escritura no son particularmente útiles, y sus equivalentes en memoria no son más útiles que el tipo "persistente". No tiene sentido poner información en un
Product
objeto si nunca vas a sacar esa información. No utilizará necesariamente "captadores": no está tratando de compartir la estructura de datos del producto y ciertamente no debería compartir el acceso mutable a la representación interna del Producto.Eso ciertamente funciona: su almacenamiento persistente se convierte efectivamente en una devolución de llamada. Probablemente haría la interfaz más simple:
No se va a acoplar entre la representación en la memoria y el mecanismo de almacenamiento, ya que la información tiene que llegar desde aquí hasta allí (y viceversa). Cambiar la información que se va a compartir tendrá un impacto en ambos extremos de la conversación. Así que bien podríamos hacer eso explícito donde podamos.
Este enfoque, pasar datos a través de devoluciones de llamada, jugó un papel importante en el desarrollo de simulacros en TDD .
Tenga en cuenta que pasar la información a la devolución de llamada tiene las mismas restricciones que devolver la información de una consulta: no debe pasar copias mutables de sus estructuras de datos.
Este enfoque es un poco contrario a lo que Evans describió en el Libro Azul, donde la devolución de datos a través de una consulta era la forma normal de hacer las cosas, y los objetos de dominio fueron diseñados específicamente para evitar la mezcla de "preocupaciones de persistencia".
Una cosa a tener en cuenta: el Libro Azul se escribió hace quince años, cuando Java 1.4 vagaba por la tierra. En particular, el libro es anterior a los genéricos de Java : tenemos muchas más técnicas disponibles ahora que cuando Evans estaba desarrollando sus ideas.
fuente
Storage
interfaz de la misma manera que lo hizo, luego consideré un alto acoplamiento y lo cambié. Pero tienes razón, de todos modos hay un acoplamiento inevitable, así que ¿por qué no hacerlo más explícito?Muy buenas observaciones, estoy completamente de acuerdo contigo en ellas. Aquí hay una charla mía (corrección: solo diapositivas) sobre exactamente este tema: Diseño orientado a dominio orientado a objetos .
Respuesta corta: no. No debe haber un objeto en su aplicación que sea puramente técnico y que no tenga relevancia de dominio. Eso es como implementar el marco de registro en una aplicación de contabilidad.
Su
Storage
ejemplo de interfaz es excelente, suponiendo queStorage
se considere un marco externo, incluso si lo escribe.Además,
save()
en un objeto solo debe permitirse si es parte del dominio (el "idioma"). Por ejemplo, no se me debería pedir que "guarde" explícitamente unaAccount
vez que llametransfer(amount)
. Con razón debería esperar que la función comercialtransfer()
persista en mi transferencia.En general, creo que las ideas de DDD son buenas. Uso de un lenguaje ubicuo, ejercicio del dominio con conversación, contextos limitados, etc. Sin embargo, los bloques de construcción necesitan una revisión seria para ser compatibles con la orientación a objetos. Vea el mazo vinculado para más detalles.
fuente
AccountNumber
debe saber que se puede representar como aTextField
. Si otros (como una "vista") sabrían esto, que es el acoplamiento que no debería existir, ya que dicho elemento tendría que saber quéAccountNumber
consiste, es decir, las partes internas.Evite difundir el conocimiento de los campos innecesariamente. Mientras más cosas sepa sobre un campo individual, más difícil será agregar o quitar un campo:
Aquí el producto no tiene idea si está guardando en un archivo de registro o una base de datos o ambos. Aquí el método de guardar no tiene idea si tiene 4 o 40 campos. Eso está vagamente acoplado. Eso es bueno.
Por supuesto, este es solo un ejemplo de cómo puede lograr este objetivo. Si no le gusta construir y analizar una cadena para usar como DTO, también puede usar una colección.
LinkedHashMap
es un antiguo favorito mío ya que conserva el orden y toString () se ve bien en un archivo de registro.Como sea que lo haga, no difunda el conocimiento de los campos. Esta es una forma de acoplamiento que la gente a menudo ignora hasta que es demasiado tarde. Quiero que pocas cosas sepan estáticamente cuántos campos tiene mi objeto como sea posible. De esa manera, agregar un campo no implica muchas ediciones en muchos lugares.
fuente
Map
, tú propones unString
o unList
. Pero, como @VoiceOfUnreason mencionó en su respuesta, el acoplamiento sigue ahí, pero no es explícito. Todavía no es necesario conocer la estructura de datos del producto para guardarlo tanto en una base de datos como en un archivo de registro, al menos cuando se vuelve a leer como un objeto.Storage
es una parte del dominio (al igual que la interfaz del repositorio) y hace una API de persistencia. Cuando se cambia, es mejor informar a los clientes en tiempo de compilación, ya que de todos modos tienen que reaccionar para no romperse en el tiempo de ejecución.Hay una alternativa a los patrones ya mencionados. El patrón Memento es ideal para encapsular el estado interno de un objeto de dominio. El objeto de recuerdo representa una instantánea del estado público del objeto de dominio. El objeto de dominio sabe cómo crear este estado público a partir de su estado interno y viceversa. Un repositorio solo funciona con la representación pública del estado. Con eso, la implementación interna se desacopla de los detalles de persistencia y solo tiene que mantener el contrato público. Además, su objeto de dominio no tiene que exponer ningún captador que realmente lo haga un poco anémico.
Para más información sobre este tema, recomiendo el gran libro: "Patrones, principios y prácticas del diseño impulsado por dominio" de Scott Millett y Nick Tune
fuente