quickly por patrón orientada guiado ejemplo driven dominio domain diseño diagrama ddd arquitectura repository domain-driven-design repository-pattern specifications

repository - por - domain driven design quickly



Patrón de especificación en diseño impulsado por dominio (4)

He estado buscando semanas en todo el Internet, leí docenas de artículos sobre DDD y especificaciones, pero siempre manejan casos simples y no toman en cuenta el rendimiento o violan el patrón de DDD.

Alguien me corregirá si me equivoco, pero me parece que el concepto de "Modelo de persistencia" no apareció hasta hace muy poco en el espacio de DDD (por cierto, ¿dónde lo leyeron?). No estoy seguro de que esté descrito en el libro azul original.

Personalmente no le veo muchas ventajas. Mi opinión es que tiene un modelo relacional persistente (generalmente) en su base de datos y un modelo de dominio en memoria en su aplicación. La brecha entre los dos está unida por una acción , no por un modelo. Esta acción puede ser realizada por un ORM. Todavía tengo que venderme por el hecho de que un "modelo de objetos de persistencia" realmente tiene sentido semánticamente, y mucho menos es obligatorio respetar los principios de DDD (*).

Ahora está el enfoque CQRS en el que tiene un Modelo de Lectura separado, pero este es un animal totalmente diferente y no vería las Specifications actúan sobre los objetos del Modelo de Lectura en lugar de Entidades como una violación de DDD en este caso. Después de todo, la especificación es un patrón muy general que nada en DDD restringe fundamentalmente a las Entidades.

(*) Editar: el creador de Automapper Jimmy Bogard parece encontrarlo demasiado complicado también. Ver ¿Cómo uso automapper para mapear relaciones de muchos a muchos?

He estado luchando por resolver un problema relacionado con DDD con Especificaciones y he leído mucho sobre DDD y especificaciones y repositorios.

Sin embargo, existe un problema si se intentan combinar los 3 elementos sin romper el diseño controlado por el dominio. Se reduce a cómo aplicar filtros teniendo en cuenta el rendimiento.

Primero algunos hechos obvios:

  1. Repositorios para obtener DataAccess / Infrastructure Layer
  2. Los modelos de dominio representan la lógica de negocios y van a la capa de dominio
  3. Los modelos de acceso a datos representan la capa de persistencia y van a la capa de persistencia / infraestructura / acceso de datos
  4. La lógica de negocios va a la capa de dominio
  5. Las especificaciones son lógica de negocios, por lo que también pertenecen a la capa de dominio.
  6. En todos estos ejemplos, se utiliza un ORM Framework y SQL Server dentro del Repositorio
  7. Los modelos de persistencia no pueden filtrarse en la capa de dominio

Hasta ahora, tan fácil. El problema surge cuando / si tratamos de aplicar Especificaciones al Repositorio y no rompemos el patrón DDD o tenemos problemas de rendimiento.

Las posibles formas de aplicar Especificaciones:

1) Forma clásica: especificaciones utilizando el modelo de dominio en la capa de dominio

Aplique el patrón de especificación tradicional, con un método IsSatisfiedBy , devolviendo un bool y especificaciones compuestas para combinar múltiples especificaciones.

Esto nos permite mantener las especificaciones en la capa de dominio, pero ...

  1. Tiene que trabajar con los Modelos de Dominio, mientras que el repositorio utiliza Modelos de Persistencia que representan la estructura de datos de la capa de persistencia. Este es fácil de solucionar con el uso de mapeadores como AutoMapper .
  2. Sin embargo, el problema no se puede resolver: todas las especificaciones deberían realizarse en la memoria. En una tabla / base de datos grande, esto significa un gran impacto si tiene que recorrer todas las entidades para filtrar la que cumpla con sus especificaciones.

2) Especificaciones utilizando el modelo de persistencia

Esto es similar a 1), pero utilizando modelos de persistencia en la especificación. Esto permite utilizar directamente la Especificación como parte de nuestro predicado .Where que se traducirá en una consulta (es decir, TSQL) y el filtrado se realizará en el almacenamiento de persistencia (es decir, SQL Server).

  1. Si bien esto da un buen rendimiento, claramente viola el patrón DDD. Nuestro modelo de persistencia se filtra en la capa de dominio, lo que hace que la capa de dominio dependa de la capa de persistencia en lugar de al revés.

3) Me gusta 2), pero las especificaciones forman parte de la capa de persistencia

  1. Esto no funciona, porque la capa de dominio necesita hacer referencia a las especificaciones. Todavía dependería de la capa de persistencia.
  2. Tendríamos lógica de negocios dentro de la capa de persistencia. Lo que también viola el patrón DDD.

4) Me gusta 3, pero use el resumen de las especificaciones como interfaces.

Tendríamos interfaces de Especificación en nuestra capa de Dominio, nuestras implementaciones concretas de las Especificaciones en la Capa de Persistencia. Ahora nuestra capa de dominio solo interactuaría con las interfaces y no dependería de la capa de persistencia.

  1. Esto todavía viola el # 2 de 3). Tendríamos lógica empresarial en la capa de persistencia, lo cual es malo.

5) Convierta el árbol de expresiones del modelo de dominio en un modelo de persistencia

Esto ciertamente resuelve el problema, pero es una tarea no trivial pero mantendría las Especificaciones dentro de nuestra Capa de Dominio sin dejar de beneficiarse de la optimización de SQL, porque las Especificaciones se convierten en parte de la cláusula de los Repositorios donde se traducen a TSQL.

Intenté seguir este enfoque y hay varios problemas (desde el lado de la implementación):

  1. Necesitaríamos conocer la Configuración del Asignador (si usamos uno) o mantener nuestro propio sistema de mapeo. Esto se puede hacer parcialmente (leyendo la configuración del Mapeador) con AutoMapper, pero existen problemas adicionales
  2. Es aceptable para una donde una propiedad del modelo A se asigna a una propiedad del modelo B. Se vuelve más difícil si los tipos son diferentes (es decir, debido a los tipos de persistencia, por ejemplo, las enumeraciones se guardan como cadenas o pares clave / valor en otra tabla y Necesitamos hacer conversiones dentro del resolutor.
  3. Se complica bastante si varios campos se asignan en un campo de destino. Creo que esto no es un problema para el modelo de dominio -> Mapeos de modelo de persistencia

** 6) Generador de consultas como API **

El último es hacer algún tipo de API de consulta que se pasa a la especificación y desde la cual la capa Repositorio / Persistencia generaría un Árbol de Expresión para pasar a la cláusula .Where y que usa una Interfaz para declarar todos los campos filtrables.

Hice algunos intentos en esa dirección también, pero no estaba muy contento con los resultados. Algo como

public interface IQuery<T> { IQuery<T> Where(Expression<Func<T, T>> predicate); } public interface IQueryFilter<TFilter> { TFilter And(TFilter other); TFilter Or(TFilter other); TFilter Not(TFilter other); } public interface IQueryField<TSource, IQueryFilter> { IQueryFilter Equal(TSource other); IQueryFilter GreaterThan(TSource other); IQueryFilter Greater(TSource other); IQueryFilter LesserThan(TSource other); IQueryFilter Lesser(TSource other); } public interface IPersonQueryFilter : IQueryFilter<IPersonQueryFilter> { IQueryField<int, IPersonQueryFilter> ID { get; } IQueryField<string, IPersonQueryFilter> Name { get; } IQueryField<int, IPersonQueryFilter> Age { get; } }

y en la especificación IQuery<IPersonQueryFilter> query una consulta IQuery<IPersonQueryFilter> query al constructor de especificaciones y luego le aplicaremos las especificaciones cuando la IQuery<IPersonQueryFilter> query .

IQuery<IGridQueryFilter> query = null; query.Where(f => f.Name.Equal("Bob") );

No me gusta mucho este enfoque, ya que hace que el manejo de especificaciones complejas sea algo difícil (como y / o si se encadena) y no me gusta la forma en que funcionaría Y / O / No, especialmente creando árboles de expresión a partir de esta "API". .

He estado buscando semanas en todo el Internet, leí docenas de artículos sobre DDD y especificaciones, pero siempre manejan casos simples y no toman en cuenta el rendimiento o violan el patrón de DDD.

¿Cómo solucionas esto en una aplicación del mundo real sin hacer filtrado de memoria o perder la persistencia en la capa de dominio?

¿Existen marcos que solucionen los problemas anteriores con una de las dos formas (el generador de consultas, como la sintaxis de los árboles de expresiones o un traductor del árbol de expresiones)?


Creo que el patrón de especificación no está diseñado para los criterios de consulta. En realidad, el concepto completo de DDD tampoco lo es. Considere CQRS si hay una gran cantidad de requisitos de consulta.

El patrón de especificación ayuda a desarrollar un lenguaje ubicuo, creo que es como una especie de DSL. Declara qué hacer en lugar de cómo hacerlo. Por ejemplo, en un contexto de pedido, los pedidos se consideran vencidos si se colocaron pero no se pagaron en un plazo de 30 minutos. Con el patrón de especificación, su equipo puede hablar con un término corto pero único: Especificación de orden de vencimiento. Imagina la discusión a continuación:

caso 1

Business people: I want to find out all overdue orders and ... Developer: I can do that, it is easy to find all satisfying orders with an overdue order specification and..

caso -2

Business people: I want to find out all orders which were placed before 30 minutes and still unpaid... Developer: I can do that, it is easy to filter order from tbl_order where placed_at is less that 30minutes before sysdate....

¿Cuál prefieres?

Por lo general, necesitamos un controlador DSL para analizar el dsl, en este caso, puede estar en el adaptador de persistencia, traduce la especificación a un criterio de consulta. Esta dependencia (infrastrructure.persistence => domain) no viola el principio de la arquitectura.

class OrderMonitorApplication { public void alarm() { // The specification pattern keeps the overdue order ubiquitous language in domain List<Order> overdueOrders = orderRepository.findBy(new OverdueSpecification()); for (Order order: overdueOrders) { //notify admin } } } class HibernateOrderRepository implements orderRepository { public List<Order> findBy(OrderSpecification spec) { criteria.le("whenPlaced", spec.placedBefore())//returns sysdate - 30 criteria.eq("status", spec.status());//returns WAIT_PAYMENT return ... } }


Llego tarde a la fiesta, bug aquí están mis 2 centavos ...

También me costó implementar el patrón de especificación exactamente por las mismas razones que describiste anteriormente. Si abandona el requisito de un modelo separado (Persistencia / Dominio), su problema se simplifica enormemente. podría agregar otro método a la especificación para generar el árbol de expresiones para el ORM:

public interface ISpecification<T> { bool IsSpecifiedBy(T item); Expression<Func<T, bool>> GetPredicate() }

Hay un post de Valdmir Khorikov que describe cómo hacer eso en detalle.

Sin embargo, realmente no me gusta tener un solo modelo . Al igual que usted, creo que el modelo Peristence debe mantenerse en la capa de infraestructura para no contaminar su dominio debido a las limitaciones de ORM.

Finalmente, se me ocurrió una solución que usaba un visitante para traducir el modelo de dominio a un árbol de expresiones del modelo de persistencia.

Hace poco escribí una serie de posts donde explico.

El resultado final a utilizar se vuelve muy simple en realidad, deberá hacer que la especificación sea Visitable ...

public interface IProductSpecification { bool IsSpecifiedBy(Product item); TResult Accept(IProductSpecificationVisitor<TResult> visitor); }

Cree un SpecificationVisitor para traducir la especificación a una expresión:

public class ProductEFExpressionVisitor : IProductSpecificationVisitor<Expression<Func<EFProduct, bool>>> { public Expression<Func<EFProduct, bool>>Visit (ProductMatchesCategory spec) { var categoryName = spec.Category.CategoryName; return ef => ef.Category == categoryName; } //other specification-specific visit methods }

Hay solo algunas modificaciones que deben hacerse si desea crear una especificación genérica. Todo está detallado en los mensajes mencionados anteriormente.


Una vez implementé la especificación pero ...

  1. Estaba basado en LINQ y IQueryable.
  2. Utilizó un único repositorio unificado (pero para mí no es malo y creo que es la razón principal para usar la Especificación).
  3. Usó un modelo único para el dominio y las necesidades persistentes (lo que creo que es malo).

Repositorio:

public interface IRepository<TEntity> where TEntity : Entity, IAggregateRoot { TEntity Get<TKey>(TKey id); TEntity TryGet<TKey>(TKey id); void DeleteByKey<TKey>(TKey id); void Delete(TEntity entity); void Delete(IEnumerable<TEntity> entities); IEnumerable<TEntity> List(FilterSpecification<TEntity> specification); TEntity Single(FilterSpecification<TEntity> specification); TEntity First(FilterSpecification<TEntity> specification); TResult Compute<TResult>(ComputationSpecification<TEntity, TResult> specification); IEnumerable<TEntity> ListAll(); //and some other methods }

Especificación del filtro:

public abstract class FilterSpecification<TAggregateRoot> where TAggregateRoot : Entity, IAggregateRoot { public abstract IQueryable<TAggregateRoot> Filter(IQueryable<TAggregateRoot> aggregateRoots); public static FilterSpecification<TAggregateRoot> CreateByPredicate(Expression<Func<TAggregateRoot, bool>> predicate) { return new PredicateFilterSpecification<TAggregateRoot>(predicate); } public static FilterSpecification<TAggregateRoot> operator &(FilterSpecification<TAggregateRoot> op1, FilterSpecification<TAggregateRoot> op2) { return new CompositeFilterSpecification<TAggregateRoot>(op1, op2); } public static FilterSpecification<TAggregateRoot> CreateDummy() { return new DummyFilterSpecification<TAggregateRoot>(); } } public class CompositeFilterSpecification<TAggregateRoot> : FilterSpecification<TAggregateRoot> where TAggregateRoot : Entity, IAggregateRoot { private readonly FilterSpecification<TAggregateRoot> _firstOperand; private readonly FilterSpecification<TAggregateRoot> _secondOperand; public CompositeFilterSpecification(FilterSpecification<TAggregateRoot> firstOperand, FilterSpecification<TAggregateRoot> secondOperand) { _firstOperand = firstOperand; _secondOperand = secondOperand; } public override IQueryable<TAggregateRoot> Filter(IQueryable<TAggregateRoot> aggregateRoots) { var operand1Results = _firstOperand.Filter(aggregateRoots); return _secondOperand.Filter(operand1Results); } } public class PredicateFilterSpecification<TAggregateRoot> : FilterSpecification<TAggregateRoot> where TAggregateRoot : Entity, IAggregateRoot { private readonly Expression<Func<TAggregateRoot, bool>> _predicate; public PredicateFilterSpecification(Expression<Func<TAggregateRoot, bool>> predicate) { _predicate = predicate; } public override IQueryable<TAggregateRoot> Filter(IQueryable<TAggregateRoot> aggregateRoots) { return aggregateRoots.Where(_predicate); } }

Otro tipo de especificación:

public abstract class ComputationSpecification<TAggregateRoot, TResult> where TAggregateRoot : Entity, IAggregateRoot { public abstract TResult Compute(IQueryable<TAggregateRoot> aggregateRoots); public static CompositeComputationSpecification<TAggregateRoot, TResult> operator &(FilterSpecification<TAggregateRoot> op1, ComputationSpecification<TAggregateRoot, TResult> op2) { return new CompositeComputationSpecification<TAggregateRoot, TResult>(op1, op2); } }

y usos:

OrderRepository.Compute(new MaxInvoiceNumberComputationSpecification()) + 1 PlaceRepository.Single(FilterSpecification<Place>.CreateByPredicate(p => p.Name == placeName)); UnitRepository.Compute(new UnitsAreAvailableForPickingFilterSpecification() & new CheckStockContainsEnoughUnitsOfGivenProductComputatonSpecification(count, product));

Implementaciones personalizadas pueden parecer

public class CheckUnitsOfGivenProductExistOnPlaceComputationSpecification : ComputationSpecification<Unit, bool> { private readonly Product _product; private readonly Place _place; public CheckUnitsOfGivenProductExistOnPlaceComputationSpecification( Place place, Product product) { _place = place; _product = product; } public override bool Compute(IQueryable<Unit> aggregateRoots) { return aggregateRoots.Any(unit => unit.Product == _product && unit.Place == _place); } }

Finalmente, me veo forzado a decir que la implementación de Specficiation simple se ajusta mal según DDD. Has hecho una gran investigación en esta área y es poco probable que alguien proponga algo nuevo :). Además, eche un vistazo a http://www.sapiensworks.com/blog/ blog.