.net - persistencia - Diseño impulsado por el dominio: cómo acceder al elemento secundario de la raíz de agregado
microservicios en net (3)
Si tengo una clase de orden an como raíz agregada y 1000 elementos de línea.
¿Cómo cargo solo uno de los 1000 artículos de línea? Por lo que yo entiendo, solo se puede acceder a una línea de pedido a través de la clase de Orden y tiene una identidad "local". ¿Seguiría creando un método de repositorio en el OrderRepository como "GetLineItemById"?
Edite para comentar la respuesta: actualmente no creo que sea razonable tener hijos inmutables. ¿Qué pasa si tengo una clase de Cliente con varias direcciones, contratos e incluso más colecciones de niños? Una gran entidad en la que quiero realizar métodos CRUD.
Quisiera
public class Customer
{
public IEnumerable<Address> Addresses { get; private set; }
public IEnumerable<Contracts> Contracts { get; private set; }
...
}
¿Tendría que hacer algo como esto si un usuario corrige la calle de una dirección o una propiedad de un contrato?
public class Customer
{
public void SetStreetOfAddress(Address address, street){}
public void SetStreetNumberOfAddress(Address address, streetNumber){}
}
La clase de cliente estaría llena de métodos de manipulación infantil entonces. Así que preferiría hacer
addressInstance.Street = "someStreet";
Creo que estoy malinterpretando todo el concepto ... :)
Cuando necesita acceder a la entidad hija por Id, hace que la entidad hija sea una raíz agregada. No hay nada malo con las raíces agregadas que tienen otras raíces agregadas como niños, o incluso con niños con una referencia al padre. Un repositorio separado para la entidad hijo está bien. Cuando las raíces agregadas tienen raíces agregadas, debemos tener en cuenta el concepto de "contextos delimitados" para evitar que se junten partes demasiado grandes del dominio y dificultar el cambio del código. Cuando esto sucede, la razón es que la mayoría de las veces las raíces agregadas se anidan en profundidad. Esto no debería ser un problema en su caso, el anidamiento de los elementos de línea en un orden parece muy razonable.
Para responder a la pregunta si debe anidar las líneas de pedido, ahora tengo la razón por la que desea cargar las líneas de pedido por identificación, y la venta de 1000 artículos por pedido parece que la aplicación se venderá mucho.
Cuando anida las líneas de pedido en el pedido y espera que las órdenes tengan muchas líneas de pedido, puede ver varias opciones de asignación / almacenamiento en caché / carga de consultas para hacer que los pedidos grandes funcionen como lo necesita la aplicación. La respuesta sobre cómo cargar las líneas de pedido que necesita de la manera más rápida depende del contexto en el que la utilice.
1) No hay nada de malo en acceder a los niños de una raíz agregada a través de propiedades simples de solo lectura o métodos get.
Lo importante es asegurarse de que todas las interacciones con los niños estén mediadas por la raíz agregada, de modo que haya un lugar único y predecible para garantizar invariantes.
Entonces Order.LineItems
está bien, siempre y cuando devuelva una colección inmutable de objetos (públicamente) inmutables. Del Order.LineItems[id]
modo, Order.LineItems[id]
. Para ver un ejemplo, consulte la fuente del ejemplo de ddd canónico aprobado por Evans , donde la clase de Cargo
raíz agregada expone a varios de sus hijos, pero las características de los niños son inmutables.
2) Las raíces agregadas pueden contener referencias a otras raíces agregadas, simplemente no pueden cambiarse entre sí.
Si tiene "el libro azul" ( Diseño Car.Engine
dominio ), consulte el ejemplo en la página 127, que muestra cómo puede tener Car.Engine
, donde tanto el Car
como el Engine
son raíces agregadas, pero un motor no es parte de un agregado del auto y no puede hacer cambios a un motor usando cualquiera de los métodos de Car
(o viceversa).
3) En el diseño impulsado por dominio, no es necesario que todas las clases agreguen raíces o hijos de agregados. Solo necesita raíces agregadas para encapsular interacciones complejas entre un grupo de clases cohesivo. La clase de Customer
que ha propuesto parece que no debería ser una raíz agregada, solo una clase regular que contiene referencias a agregados de Contract
y Address
.
Cuando dice cargar en "¿Cómo cargo solo uno de los 1000 artículos de línea?" ¿quieres decir "cargar desde la base de datos"? En otras palabras, ¿cómo puedo cargar solo una entidad secundaria de una raíz agregada desde la base de datos?
Esto es un poco complejo, pero puede hacer que sus repositorios devuelvan una derivación de la raíz agregada, cuyos campos están cargados de forma diferida. P.ej
namespace Domain
{
public class LineItem
{
public int Id { get; set; }
// stuff
}
public class Order
{
public int Id { get; set; }
protected ReadOnlyCollection<LineItem> LineItemsField;
public ReadOnlyCollection<LineItem> LineItems { get; protected set; }
}
public interface IOrderRepository
{
Order Get(int id);
}
}
namespace Repositories
{
// Concrete order repository
public class OrderRepository : IOrderRepository
{
public Order Get(int id)
{
Func<IEnumerable<LineItem>> getAllFunc = () =>
{
Collection<LineItem> coll;
// { logic to build all objects from database }
return coll;
};
Func<int, LineItem> getSingleFunc = idParam =>
{
LineItem ent;
// { logic to build object with ''id'' from database }
return ent;
};
// ** return internal lazy-loading derived type **
return new LazyLoadedOrder(getAllFunc, getSingleFunc);
}
}
// lazy-loading internal derivative of Order, that sets LineItemsField
// to a ReadOnlyCollection constructed with a lazy-loading list.
internal class LazyLoadedOrder : Order
{
public LazyLoadedOrder(
Func<IEnumerable<LineItem>> getAllFunc,
Func<int, LineItem> getSingleFunc)
{
LineItemsField =
new ReadOnlyCollection<LineItem>(
new LazyLoadedReadOnlyLineItemList(getAllFunc, getSingleFunc));
}
}
// lazy-loading backing store for LazyLoadedOrder.LineItems
internal class LazyLoadedReadOnlyLineItemList : IList<LineItem>
{
private readonly Func<IEnumerable<LineItem>> _getAllFunc;
private readonly Func<int, LineItem> _getSingleFunc;
public LazyLoadedReadOnlyLineItemList(
Func<IEnumerable<LineItem>> getAllFunc,
Func<int, LineItem> getSingleFunc)
{
_getAllFunc = getAllFunc;
_getSingleFunc = getSingleFunc;
}
private List<LineItem> _backingStore;
private List<LineItem> GetBackingStore()
{
if (_backingStore == null)
_backingStore = _getAllFunc().ToList(); // ** lazy-load all **
return _backingStore;
}
public LineItem this[int index]
{
get
{
if (_backingStore == null) // bypass GetBackingStore
return _getSingleFunc(index); // ** lazy-load only one from DB **
return _backingStore[index];
}
set { throw new NotSupportedException(); }
}
// "getter" implementations that use lazy-loading
public IEnumerator<LineItem> GetEnumerator() { return GetBackingStore().GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
public bool Contains(LineItem item) { return GetBackingStore().Contains(item); }
public void CopyTo(LineItem[] array, int arrayIndex) { GetBackingStore().CopyTo(array, arrayIndex); }
public int Count { get { return GetBackingStore().Count; } }
public bool IsReadOnly { get { return true; } }
public int IndexOf(LineItem item) { return GetBackingStore().IndexOf(item); }
// "setter" implementations are not supported on readonly collection
public void Add(LineItem item) { throw new NotSupportedException("Read-Only"); }
public void Clear() { throw new NotSupportedException("Read-Only"); }
public bool Remove(LineItem item) { throw new NotSupportedException("Read-Only"); }
public void Insert(int index, LineItem item) { throw new NotSupportedException("Read-Only"); }
public void RemoveAt(int index) { throw new NotSupportedException("Read-Only"); }
}
}
Las personas que llaman a OrderRepository.Get(int)
recibirían algo que es efectivamente solo un objeto Order, pero en realidad es un LazyLoadedOrder. Por supuesto, para hacer esto, sus raíces agregadas deben proporcionar un miembro virtual o dos, y estar diseñadas alrededor de esos puntos de extensión.
Editar para direccionar actualizaciones de preguntas
En el caso de una dirección, la trataría como un objeto de valor, es decir, composiciones de datos inmutables que se tratan juntas como un único valor.
public class Address
{
public Address(string street, string city)
{
Street = street;
City = city;
}
public string Street {get; private set;}
public string City {get; private set;}
}
Luego, para modificar el agregado, crea una nueva instancia de Dirección. Esto es análogo al comportamiento de DateTime. También puede agregar métodos de métodos a la dirección como SetStreet(string)
pero estos deberían devolver nuevas instancias de la dirección, al igual que los métodos de DateTime devuelven nuevas instancias de DateTime.
En su caso, los objetos de valor de Dirección inmutables deben combinarse con algún tipo de observación de la colección de Direcciones. Una técnica directa y limpia es rastrear los valores de dirección agregados y eliminados en colecciones separadas.
public class Customer
{
public IEnumerable<Address> Addresses { get; private set; }
// backed by Collection<Address>
public IEnumerable<Address> AddedAddresses { get; private set; }
// backed by Collection<Address>
public IEnumerable<Address> RemovedAddresses { get; private set; }
public void AddAddress(Address address)
{
// validation, security, etc
AddedAddresses.Add(address);
}
public void RemoveAddress(Address address)
{
// validation, security, etc
RemovedAddresses.Add(address);
}
// call this to "update" an address
public void Replace(Address remove, Address add)
{
RemovedAddresses.Add(remove);
AddedAddresses.Add(add);
}
}
Alternativamente, puede respaldar Direcciones con un ObservableCollection<Address>
.
Esta es realmente una solución DDD pura, pero mencionaste NHibernate. No soy un experto en NHibernate, pero imagino que tendrá que agregar un código para que NHibernate sepa dónde se almacenan los cambios en las direcciones.