design patterns - trabajo - DDD-Dependencias entre modelo de dominio, servicios y repositorios
patron repositorio java (6)
Solo quería saber cómo otros han acodado su arquitectura. Digamos que tengo mis capas de la siguiente manera:
Capa de dominio
--Producto
--ProductService (¿Debería ir el diablillo a esta capa?)
--ProductoServicio
--ProductoRepositivo
Capa de Infraestructura
--ProductRepository (Imp de IProductRepository en mi dominio)
Ahora, cuando se crea un nuevo producto, tengo el requisito de asignar una identificación del producto llamando al método ProductService.GetNextProductId ().
Debido a que el servicio tiene una dependencia del repositorio, configuré el ctor ProductService con una interfaz de IProductRepository que se puede inyectar más adelante. algo como esto:
public class ProductService : IProductService
{
private IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
public long GetNextProductId()
{
return _repository.GetNextProductId();
}
}
Mi problema es que cuando uso el servicio en la Clase de producto, estoy haciendo referencia al Repositorio en el ctor al crear una instancia de una nueva clase ProductService. En DDD es un gran no, no tener tal referencia. Ni siquiera estoy seguro de si mi clase de dominio del producto se está configurando correctamente para llamar al servicio, ¿alguien puede avisar:
public class Product : Entity
{
private ProductService _svc;
private IProductRepository _repository;
public Product(string name, Address address) //It doesnt seem right to put parm for IProductRepository in the ctor?
: base(_svc.GetNextProductId) // This is where i pass the id
{
// where to create an instance of IProductRepository?
}
}
¿Cómo puedo resolver elegantemente este problema de diseño? Estoy abierto a sugerencias de DDD con experiencia
EDITAR:
Gracias por tus comentarios. También dudé si el servicio debería ser llamado desde la Clase de Producto. No he usado un patrón de fábrica (todavía) ya que la construcción del objeto todavía es simple. ¿No siento que justifique un método de fábrica todavía?
Estoy confundido ... Dejando a un lado el ProductId si mi clase de Producto necesitaba algún otro dato de un Servicio, por ejemplo, GetSystemDateTime () (lo sé, mal ejemplo, pero tratando de demostrar una llamada que no sea de DB) ¿dónde se llamará a este método de servicio?
Los servicios en DDD son volcados lógicos donde la lógica no es natrual al objeto de dominio, ¿verdad? Entonces, ¿cómo se pegan juntos?
¿Por qué necesita la identificación del producto cuando crea el producto en la memoria? Por lo general, la identificación del producto se establece cuando crea el producto en su repositorio.
Echa un vistazo al siguiente código:
var id1 = _repository.GetNextProductId (); var id2 = _repository.GetNextProductId ();
¿Devolverá dos identificadores de producto diferentes?
Si la respuesta es sí, entonces es segura (pero todavía incómoda); Si la respuesta es no, entonces tendrás un gran problema;
Así es como estructuraría tu problema. Creo que esta es también la forma recomendada de hacerlo.
public class ProductService : IProductService // Application Service class, used by outside components like UI, WCF, HTTP Services, other Bounded Contexts, etc.
{
private readonly IProductRepository _prodRepository;
private readonly IStoreRepository _storeRepository;
public ProductService(IProductRepository prodRepository, IStoreRepository storeRepository) // Injected dependencies DI
{
if(prodRepository == null) throw new NullArgumentException("Prod Repo is required."); // guard
if(storeRepository == null) throw new NullArgumentException("Store Repo is required."); // guard
_prodRepository = prodRepository;
_storeRepository = storeRepository;
}
public void AddProductToStore(string name, Address address, StoreId storeId) //An exposed API method related to Product that is a part of your Application Service. Address and StoreId are value objects.
{
Store store = _storeRepository.GetBy(storeId);
IProductIdGenerator productIdGenerator = new ProductIdGenerator(_prodRepository);
Product product = Product.MakeNew(name, address, productIdGenerator);
}
... // Rest of API
}
public class Product : Entity
{
public static MakeNew(string name, Address address, IProductIdGenerator productIdGenerator) // Factory to make construction behaviour more explicit
{
return new Product(name, address, productIdGenerator);
}
protected Product(string name, Address address, IProductIdGenerator productIdGenerator)
: base(productIdGenerator.GetNextProductId())
{
Name = name;
Address = address;
}
... // Rest of Product methods, properties and fields
}
public class ProductIdGenerator : IProductIdGenerator
{
private IProductRepository _repository;
public ProductIdGenerator(IProductRepository repository)
{
_repository = repository;
}
public long GetNextProductId()
{
return _repository.GetNextProductId();
}
}
public interface IProductIdGenerator
{
long GetNextProductId();
}
Básicamente, ProductService es parte de su Servicio de Aplicación, es decir, el punto de entrada y salida de todo lo que necesita para usar su dominio o cruzar su límite. Es responsable de delegar cada caso de uso a los componentes apropiados que pueden manejarlo y de coordinar entre todos estos componentes si se requieren muchos para cumplir con el caso de uso.
El producto es su AggregateRoot y una entidad en su dominio. Es responsable de dictar el contrato de UbiquitousLanguage que captura el dominio de su empresa. Entonces, en sí mismo, significa que su dominio tiene un concepto de Producto, que contiene Datos y Comportamiento, independientemente de los datos y comportamientos que exponga públicamente, debe ser un concepto de UbiquitousLanguage. Su campo no debe tener dependencias externas fuera del modelo de dominio, por lo que no hay servicios. Pero, sus métodos pueden tomar los Servicios de Dominio como parámetros para ayudarlo a realizar la lógica de comportamiento.
ProductIdGenerator es un ejemplo de dicho servicio de dominio. Los Servicios de dominio encapsulan la lógica de comportamiento que cruza fuera del propio límite de una Entidad. Entonces, si tiene una lógica que requiere otras raíces agregadas, o servicios externos como Repositorio, Sistema de archivos, Criptografía, etc. Básicamente, cualquier lógica que no pueda entrenar desde su Entidad, sin necesidad de nada más, puede necesitar un Servicio de Dominio. Si la lógica es englobar y parece que conceptualmente no pertenece realmente a un método en su Entidad, es una señal de que puede necesitar un caso de uso del Servicio de Aplicación completamente nuevo para él, o bien, ha perdido una Entidad en su diseño. También es posible utilizar el Servicio de dominio directamente desde el Servicio de aplicación, de manera no doble. Esto es un poco similar a los métodos de extensión C # en comparación con el método estático normal.
=========== Para responder a sus preguntas de edición ===============
También dudé si el servicio debería ser llamado desde la Clase de Producto.
Los servicios de dominio se pueden llamar desde la clase de producto si se pasan como una referencia temporal a través de un parámetro de método. Los servicios de aplicaciones nunca deben llamarse desde la clase de productos.
No he usado un patrón de fábrica (todavía) ya que la construcción del objeto todavía es simple. ¿No siento que justifique un método de fábrica todavía?
Depende de lo que espere que le lleve más tiempo, hacer una fábrica ahora, aunque no tenga lógica de construcción múltiple, o refactorizar más tarde cuando la tenga. Creo que no vale la pena para las entidades que no necesitan construirse de más de una manera. Como explica wikipedia , factory se utiliza para hacer que cada constructor haga más explícito y diferenciable. En mi ejemplo, la fábrica MakeNew explica a qué sirve esta construcción en particular de la Entidad: crear un nuevo producto. Podría tener más fábricas como MakeExisting, MakeSample, MakeDeprecated, etc. Cada una de estas fábricas crearía un Producto, pero con diferentes propósitos y de maneras ligeramente diferentes. Sin una fábrica, todos estos constructores se llamarían Producto () y sería difícil saber cuál es para qué y para qué. La desventaja es que es más difícil trabajar con Factory cuando extiendes tu entidad, la entidad secundaria no puede usar Factory para crear un hijo, por eso tiendo a hacer toda la lógica de construcción dentro de los constructores, y solo uso Factory Tener un nombre bonito para ellos.
Estoy confundido ... Dejando a un lado el ProductId si mi clase de Producto necesitaba algún otro dato de un Servicio, por ejemplo, GetSystemDateTime () (lo sé, mal ejemplo, pero tratando de demostrar una llamada que no sea de DB), ¿dónde se llamará a este método de servicio?
Digamos que pensaste que la implementación de la fecha era un detalle de la infraestructura. Debería crear una abstracción a su alrededor para usar en su aplicación. Comenzaría con una interfaz, tal vez algo como IDateTimeProvider. Esta interfaz tendría un método GetSystemDateTime ().
Sus Servicios de Aplicación serían libres de crear una instancia de un IDateTimeProvider y llamar a sus métodos en cualquier momento, de lo que podrían pasar el resultado a Agregados, Entidades, Servicios de Dominio o cualquier otra cosa que lo necesite.
Sus servicios de dominio serían libres de mantener una referencia a IDateTimeProvider como un campo de clase, pero no deberían crear la instancia en sí. O bien lo recibe a través de la inyección de dependencia, o lo solicita a través de Service Locator.
Finalmente, sus Entites y sus raíces agregadas y objetos de valor tendrían la libertad de llamar a GetSystemDateTime () y otros métodos de IDateTimeProvider, pero no directamente. Tendría que pasar por un doble despacho, donde le daría un Servicio de Dominio como parámetro de uno de sus métodos, y usaría ese Servicio de Dominio para consultar la información que desea, o realizar el comportamiento que necesita. También podría pasar de nuevo al Servicio de dominio, donde el servicio de dominio haría la consulta y la configuración.
Si considera que su IDateTimeProvider es en realidad un Servicio de Dominio, como parte del Lenguaje Ubicuo, entonces sus Entidades y Raíces Agregadas solo pueden invocar métodos directamente en él, no puede contener una referencia a él como un campo de clase, sino local. Las variables de los parámetros del método están bien.
Los servicios en DDD son volcados lógicos donde la lógica no es natrual al objeto de dominio, ¿verdad? Entonces, ¿cómo se pegan juntos?
Creo que toda mi respuesta ya lo ha dejado bastante claro. Básicamente, tienes 3 posibilidades para pegarlo todo (en lo que puedo pensar al menos ahora).
1) Un Servicio de Aplicación crea una instancia de un Servicio de Dominio, llama a un método y pasa los valores de retorno resultantes a otra cosa que lo necesitaba (repo, entidad, raíz agregada, objeto de valor, otro servicio de dominio, fábricas, etc.).
2) Un Servicio de Dominio es instanciado por el Dominio de la Aplicación y se pasa como un parámetro a un método de algo que lo utilizará. Lo que sea que lo use, no guarda una referencia permanente a él, es solo una variable local.
3) Un Servicio de Dominio es instanciado por el Dominio de la Aplicación y pasado como un parámetro a un método de algo que lo utilizará. Lo que sea que lo use, usa doble despacho para usar el Servicio de Dominio de una manera no dependiente. Esto significa que pasa al método del servicio de dominio una referencia a sí mismo, como en DomainService.DoSomething (this, name, Address).
Espero que esto ayude. Los comentarios son bienvenidos si hice algo incorrecto o que va en contra de las mejores prácticas de DDD.
De acuerdo con Marcelo, probablemente debería primero determinar si la ID del producto es realmente un concepto de modelo de dominio. Si los usuarios comerciales nunca usan o tienen alguna idea de la ID del producto y, por lo general, se refieren al producto por su nombre, número, SKU o una ID de producto natural formada por nombre + dimensiones, esto es lo que el modelo de dominio debe tener en cuenta.
Dicho esto, aquí es cómo estructuro mis proyectos DDD asumiendo que la ID del producto es un campo de número automático en la base de datos:
Project.Business (Modelo de dominio)
No hay referencias y por lo tanto, no hay dependencias sobre nada.
public class Product : Entity
{
private Product(string name, Address address)
{
//set values.
}
//Factory method, even for simple ctor is used for encapsulation so we don''t have
//to publically expose the constructor. What if we needed more than just a couple
//of value objects?
public static CreateNewProduct(string name, Address address)
{
return new Product(name, address);
}
public static GetAddress(string address, string city, string state, string zip) { }
}
public interface IProductRepository : IEnumerable<Product>
{
void Add(Product product);
//The following methods are extraneous, but included for completion sake.
int IndexOf(Product product);
Product this[int index] { get; set; }
}
Implementacion de proyecto
public SqlProductRepository : List<ProductDataModel>, IProductRepository
{
public SqlProductRepository(string sqlConnectionString) { }
public void Add(Product product)
{
//Get new Id and save the product to the db.
}
public int IndexOf(Product product)
{
//Get the index of the base class and convert to business object.
}
public Product this[int index]
{
get { //find instance based on index and return; }
set { //find product ID based on index and save the passed in Business object to the database under that ID. }
}
}
Project.ApplicationName (capa de presentación)
public class Application
{
IProductRepository repository = new SqlProductRepository(SqlConnectionString);
protected void Save_Click(object sender, EventArgs e)
{
Product newProduct = Product.CreateNewProduct(name, Product.GetAddress(address,city,state,zip));
repository.Add(newProduct);
}
}
Si es necesario, puede tener:
Project.Services (Capa de servicios de aplicación que usa DTO entre sí y la capa de presentación)
Hasta su último punto, los servicios en DDD son un lugar para poner lo que describo como lógica "torpe". Si tiene algún tipo de lógica o flujo de trabajo que depende de otras entidades, este es el tipo de lógica que normalmente no "encaja" dentro de un objeto de dominio en sí. Ejemplo: si tengo un método en mi objeto de negocio para realizar algún tipo de validación, la clase de servicio podría ejecutar este método (aún manteniendo la lógica de validación real relacionada con la entidad dentro de su clase)
Otro ejemplo realmente bueno que siempre menciono es el método de transferencia de fondos. No tendría una transferencia de objeto de cuenta de un objeto a otro, sino que tendría un servicio que toma la cuenta "to" y la cuenta "from". Luego, dentro del servicio, invocaría el método de retiro en su cuenta "de" y el método de depósito en su cuenta "a". Si intentara poner esto dentro de la entidad de la cuenta, se sentiría incómodo.
Un gran podcast que habla en profundidad sobre este mismo tema se puede encontrar here . David Laribee hace un muy buen trabajo explicando ahora solo el "cómo" pero el "por qué" de DDD.
Si entiendo su pregunta correctamente, usted declara que su clase de Producto está llamando a la clase ProductService. No debería. Debe hacer esto en una clase de fábrica que sea responsable de crear y configurar un Producto. El lugar donde llame a este método también puede depender de cuándo desea emitir el ProductId: tenemos un caso similar en el que necesitamos obtener un número de nuestro sistema de contabilidad heredado para un proyecto. Aplazar la obtención del número hasta que el proyecto persista para que no desperdicie ningún número o tenga huecos. Si se encuentra en una situación similar, es posible que desee emitir ProductId en un método de guardado de guardado en lugar de cuando se crea el objeto.
Además, ¿realmente crees que alguna vez tendrás más de un ProductService o ProductRepository? Si no, entonces no me molestaría con las interfaces.
Editado para añadir:
Recomiendo comenzar pequeño y mantenerlo simple comenzando con dos clases justas, Productos y Servicios de productos. ProductServices prestaría todos los servicios, incluidos los de fábrica y el repositorio, ya que puede considerarlos como servicios especializados.
Su modelo de dominio no debe tener una referencia a ProductService ni a IProductRepository. Si crea un nuevo producto, debe crearse a través de una fábrica; la fábrica puede usar ProductService para obtener una identificación del producto.
De hecho, envolvería ProductService con una interfaz adecuada, como IProductIdGeneratorService para que pueda inyectar esto en la fábrica utilizando su contenedor IoC.