Autofixture: Creación de un árbol de objetos donde los niños hacen referencia al padre para el diseño de DDD

Creado en 1 feb. 2014  ·  14Comentarios  ·  Fuente: AutoFixture/AutoFixture

Hola.

No quiero poder crear un árbol de objetos donde los niños hagan referencia al padre.
Por ejemplo, un automóvil tiene neumáticos y un neumático hace referencia a su automóvil.

Hasta ahora he estado usando OmitOnRecursionBehavior y asigné la referencia manualmente, demostración:

`` c #
var fixture = new Fixture ();
fixture.Behaviors.Add (new OmitOnRecursionBehavior ());
var car = fixture.Create();
foreach (var tire en car.Tires)
{
tire.Car = coche;
}

What I wanted to do:

``` c#
fixture.Customize<Car>(composer => composer
.With(car => car.Tires, fixture.Build<Tire>().With(tire => tire.Car, car)
.CreateMany().ToList()));

Lo que también quería hacer:

`` c #
CarCustomization de clase privada: ICustomization
{
Personalizar vacío público (accesorio IFixture)
{
fixture.Behaviors.Add (new OmitOnRecursionBehavior ());
fixture.Customizations.Add (new CarBuilder ());
}
}

CarBuilder de clase privada: ISpecimenBuilder
{
Crear objeto público (solicitud de objeto, contexto ISpecimenContext)
{
var t = solicitud como tipo;
if (t! = null && t == typeof (Coche))
{
// No funcionará, el constructor depende de sí mismo
var car = context.Create();
foreach (var tire en car.Tires)
{
tire.Car = coche;
}
}

    return new NoSpecimen(request);
}

}

Here are the Car/Tire classes:

``` c#
public class Car
{
    public string Name { get; set; }
    public string Description { get; set; }
    public int Doors { get; set; }
    public List<Tire> Tires { get; set; }
}

public class Tire
{
    public int Size { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Car Car { get; set; }
}

¿Alguien puede ayudarme a resolver esto cuidadosamente?

Comentario más útil

El código de muestra proporcionado por @moodmosaic es idiomático, por lo que debería ser el camino a seguir como una solución específica para un problema específico.

Con respecto a la conveniencia de usar un ORM para DDD, no estoy de acuerdo. Los ORM pueden parecer poderosos, pero de hecho, son muy restrictivos. Parece que resuelven un problema, pero crean muchos más problemas.

Cuando se trata de DDD, uno de los patrones tácticos más importantes que se describen en el libro azul es el concepto de _ raíz agregada_. Todos los ORM que he visto violan descaradamente este patrón, porque puede comenzar en cualquier tabla dada y comenzar a cargar filas de esa tabla, y luego recorrer varias 'propiedades de navegación' para cargar datos de tablas asociadas. Esto significa que no hay raíces y, por lo tanto, no hay una propiedad clara de los datos.

Agregue a esto que todo es mutable, y pronto tendrá un sistema en su mano, sobre el cual es muy difícil razonar.

Todos 14 comentarios

Creo que encontrará que Tire no debería tener una referencia al padre Car . Las referencias circulares se consideran malas (por autofijación, al menos).

Sin embargo, esto es algo que también necesito resolver. Estoy trabajando con modelos EF que no puedo cambiar y tienen referencias circulares que no puedo eliminar. En este momento, estoy evitando la autofijación, pero prefiero esforzarme en resolver esto de forma genérica (si es posible).

Hacer un diseño controlado por dominio a veces es conveniente ya que la mayor parte de la lógica está en la capa de dominio y algún método en el niño puede necesitar la referencia para implementaciones más limpias.

Eric Evans dice en su libro "Domain-Driven Design - Tackling Complexity in the Heart of Software":
Las referencias circulares lógicamente existen en muchos dominios y, a veces, también son necesarias en el diseño, pero son difíciles de mantener.

Además, con tecnologías de persistencia populares como EF y Fluent NHibernate, se recomienda o al menos más fácil para las asignaciones.

Por lo tanto, encontrar una solución conveniente sería útil para mí y para otras personas que realizan DDD, EF o Fluent NHibernate.

Creo que @ploeh y @moodmosaic han dejado claro en el pasado que no les gustan las referencias circulares y las evitan siempre que sea posible (corríjanme si me equivoco, no quiero hablar por ustedes, pero esto es mi entendimiento).

Sin embargo, se sabe que ambos aceptan contribuciones a AutoFixture que no encuentran particularmente útiles, siempre que no les cause problemas de mantenimiento y soporte (muy justo, creo).

Estoy muy motivado para intentar hacer algo al respecto (suponiendo que haya algo que sea genéricamente útil que funcione).

Me gustan los frameworks que fomentan el buen diseño y entiendo el problema de las referencias circulares, pero a veces son lo lógico. Quizás se pueda encontrar una solución que no fomente las referencias circulares pero que no dañe la belleza de AutoFixture.

Gracias por tu respuesta y tu actitud positiva: +1:

Sí, _si_ logramos encontrar una solución genérica decente, no sería el comportamiento _predeterminado_ de AutoFixture.

Desde el CarBuilder anterior (el que no funciona), parece que lo que realmente necesita es un Postprocessor<Car> . ¿Has probado eso?

En general, el deseo de usar AutoFixture para clases ORM es un tema recurrente y, como señala @adamchester , no tengo ningún problema en hacer que esto funcione si se puede hacer de tal manera que no cause problemas de mantenimiento o soporte.

Por otro lado, al no haber usado un ORM durante más de tres años, no tengo ningún interés personal en seguir esta función, por lo que tendría que depender de que alguien más tome la iniciativa.

En cuanto a la necesidad de referencias circulares, estoy de acuerdo en que el concepto es real, pero eso no significa que sea una buena idea utilizarlo en el diseño de software. Las referencias circulares harán que el código sea mucho más difícil de usar y razonar. Si fuera inevitable, entonces tendríamos que vivir con ello, pero IME usted puede _siempre_ diseñar una API de una manera que evite referencias circulares; No sé cuándo creé por última vez una API con una referencia circular, pero fue hace muchos años.

Pero, ¿qué pasa con los ORMS, que los requieren?

Bueno, si usa un ORM, ya no está trabajando con código orientado a objetos; su código será _relacional_, con fuertes elementos de procedimiento. No se deje engañar por la elección del idioma (C #, etc.).

Quizás todavía pueda hacer DDD de esta manera, pero será un DDD relacional, no un DDD orientado a objetos. Terminará con lo que Martin Fowler llama un script de transacción .

Gracias por tu respuesta, se agradece mucho: +1:

No he oído hablar del Postprocesadorpero lo intenté y no pude hacerlo funcionar, tal vez me esté perdiendo algo. Parece que si uso el PostProcessor, el tipo no se ha inicializado con valores cuando se ejecuta.

Aquí está mi código:

`` c #
clase interna DomainCustomization: ICustomization
{
Personalizar vacío público (accesorio IFixture)
{
fixture.Behaviors.Add (new OmitOnRecursionBehavior ());
fixture.Customizations.Add (
nuevo Postprocesador(((Fixture) fixture) .Engine, nuevo CarCommand ())
);
}
}

clase interna CarCommand: ISpecimenCommand
{
public void Execute (muestra de objeto, contexto ISpecimenContext)
{
var car = espécimen como Car;
si (coche! = nulo)
{
// Excepción de referencia nula aquí, sin neumáticos, sin valores en la configuración del automóvil
foreach (var tire en car.Tires)
{
tire.Car = coche;
}
}
}
}
''

Martin Fowler es uno de mis héroes. En una de sus publicaciones, habla del modelo de dominio anémico, que es exactamente lo que estoy tratando de evitar. Mis entidades tienen la mayor parte de la lógica en la aplicación y no en los servicios (Transaction Scirpt). Dado que las entidades tienen toda la lógica, es posible que necesiten algunos datos relacionados con ellas.

Siempre trato de terminar implementando la capa de persistencia después de que se completa el modelo de dominio para que la base de datos sea un reflejo de mis entidades y no al revés.

Sin embargo, los ORMS como EF / EF CodeFirst y NHibernate te hacen crear relaciones que no son necesarias para tus entidades y más referencias circulares que las que necesita tu diseño original, haciendo que tu diseño sea relacional.

Estos ORMS son muy poderosos y justifican esta "fealdad".
Tendré que vivir con estas tecnologías por ahora y con muchas otras, por lo que una solución a este problema podría ser útil para otros.

Aquí hay una prueba de aprobación donde se crea el SUT con sus _Neumáticos_ llenos y cada Tire referencia a su Car :

`` c #
[Hecho]
Prueba de vacío público ()
{
var fixture = new Fixture ();
accesorio Personalizar(c => c. Sin (x => x.Car));
fixture.Customizations.Add (
new FilteringSpecimenBuilder (
nuevo Postprocesador (
nuevo MethodInvoker (
nuevo ModestConstructorQuery ()),
nuevo CarFiller ()),
new CarSpecification ()));

var car = fixture.Create<Car>();

Assert.NotEmpty(car.Tires);
Array.ForEach(car.Tires.ToArray(), x => Assert.NotNull(x.Car));

}

CarFiller de clase privada: ISpecimenCommand
{
public void Execute (muestra de objeto, contexto ISpecimenContext)
{
si (espécimen == nulo)
lanzar una nueva ArgumentNullException ("espécimen");
si (contexto == nulo)
lanzar una nueva ArgumentNullException ("contexto");

    var car = specimen as Car;
    if (car == null)
        throw new ArgumentException(
            "The specimen must be an instance of Car.",
            "specimen");

    Array.ForEach(car.GetType().GetProperties(), x =>
    {
        if (x.Name == "Tires")
        {
            var tires =
                ((IEnumerable<object>)context
                    .Resolve(new MultipleRequest(typeof(Tire))))
                        .Cast<Tire>().ToArray();
            Array.ForEach(tires, tire => tire.Car = car);
            x.SetValue(car, tires.ToList());
        }
        else x.SetValue(car, context.Resolve(x.PropertyType));
    });
}

}

Clase privada CarSpecification: IRequestSpecification
{
public bool IsSatisfiedBy (solicitud de objeto)
{
var requestType = solicitud como tipo;
si (requestType == null)
falso retorno;

    return typeof(Car).IsAssignableFrom(requestType);
}

}
''

¡Dulce! Gracias @moodmosaic , me faltaban algunas piezas de este rompecabezas.
Hermoso código :)

También gracias a @adamchester y @ploeh por sus pensamientos.

Cerraré este tema, esto es suficiente para mí. Puede volver a abrir esto si desea crear una solución más genérica.

El código de muestra proporcionado por @moodmosaic es idiomático, por lo que debería ser el camino a seguir como una solución específica para un problema específico.

Con respecto a la conveniencia de usar un ORM para DDD, no estoy de acuerdo. Los ORM pueden parecer poderosos, pero de hecho, son muy restrictivos. Parece que resuelven un problema, pero crean muchos más problemas.

Cuando se trata de DDD, uno de los patrones tácticos más importantes que se describen en el libro azul es el concepto de _ raíz agregada_. Todos los ORM que he visto violan descaradamente este patrón, porque puede comenzar en cualquier tabla dada y comenzar a cargar filas de esa tabla, y luego recorrer varias 'propiedades de navegación' para cargar datos de tablas asociadas. Esto significa que no hay raíces y, por lo tanto, no hay una propiedad clara de los datos.

Agregue a esto que todo es mutable, y pronto tendrá un sistema en su mano, sobre el cual es muy difícil razonar.

No es posible crear una verdadera raíz agregada con los ORM actuales. La raíz agregada debería ser la única capaz de cambiar sus datos ...

Los ORM crean restricciones como: constructores sin parámetros, establecedores para cada propiedad y sus propiedades de navegación públicas. Esto me ha molestado mucho.

Con mis enfoques actuales, realmente no puedo confiar en que los usuarios de mi biblioteca cambien los datos a través de su raíz agregada. Incluso si Aggregate Root pudiera hacer cumplir esto, no evitaría las actualizaciones directas de sql en la base de datos ...

Estar atascado en SqlServer y bases de datos relacionales no me ha ayudado a encontrar otra forma. Quizás necesito seguir buscando una mejor manera.

Aun así, siento que las aplicaciones que he creado con DDD son más flexibles para cambiar y mantener. Una parte de eso también puede ser la ayuda de TDD y la experiencia.

Gracias por tus pensamientos: +1:

Me di cuenta de que si tiene una llamada .Customize para el tipo que está procesando el Relleno, eso hace que se ignore el relleno. por ejemplo, fixture.Customize<Car>(c => c.Without(x => x.Doors))

Intenté combinar estos así:

var fixture = new Fixture();
            fixture.Customize<Tire>(c => c.Without(x => x.Car));
            ///fixture.Customize<Car>(c => c.Without(x => x.Doors));
            fixture.Customizations.Add(
                new FilteringSpecimenBuilder(
                    new Postprocessor(
                        SpecimenBuilderNodeFactory.CreateComposer<Car>().WithAutoProperties(!fixture.OmitAutoProperties)
                        .With(x=>x.Description, "Wee")
                        ,
                        new CarFiller()),
                    new CarSpecification()));


            var car = fixture.Create<Car>();

            Assert.Equal("Wee", car.Description);
            Assert.NotEmpty(car.Tires);
            Array.ForEach(car.Tires.ToArray(), x => Assert.NotNull(x.Car));
            Array.ForEach(car.Tires.ToArray(), x => Assert.True(x.Car == car));

Por tanto, la afirmación de la descripción falla. Aún siendo generado automáticamente, las personalizaciones se ignoran. Basado en mi comprensión de las cosas, hubiera esperado que la muestra pasada al relleno se hubiera creado usando el Compositor.

Editar Veo ahora que else x.SetValue(car, context.Resolve(x.PropertyType)) genera automáticamente las otras propiedades. Salud

Aquí hay una versión más genérica:

private class CollectionBackReferenceFiller<T, TElement> : ISpecimenCommand
            where T : class
        {
            private readonly string _collectionProperty;
            private readonly Action<TElement, T> _backReferenceSetter;

            public CollectionBackReferenceFiller(string collectionProperty, Action<TElement, T> backReferenceSetter)
            {
                _collectionProperty = collectionProperty;
                _backReferenceSetter = backReferenceSetter;
            }

            public void Execute(object specimen, ISpecimenContext context)
            {
                if (specimen == null)
                    throw new ArgumentNullException("specimen");
                if (context == null)
                    throw new ArgumentNullException("context");

                var typedSpecimen = specimen as T;
                if (typedSpecimen == null)
                    throw new ArgumentException(
                        "The specimen must be an instance of " + typeof(T).Name,
                        "specimen");

                Array.ForEach(typedSpecimen.GetType().GetProperties(), x =>
                {
                    if (x.Name == _collectionProperty)
                    {
                        var elements =
                            ((IEnumerable<object>)context
                                .Resolve(new MultipleRequest(typeof(TElement))))
                                    .Cast<TElement>().ToArray();
                        Array.ForEach(elements, e => _backReferenceSetter(e, typedSpecimen));

                        x.SetValue(typedSpecimen, elements.ToList());
                    }
                });
            }
        }

        private class TypeSpecification<T> : IRequestSpecification
        {
            public bool IsSatisfiedBy(object request)
            {
                var requestType = request as Type;
                if (requestType == null)
                    return false;

                return typeof(T).IsAssignableFrom(requestType);
            }
        }

¡Tu solución genérica fue genial!
Lo actualicé para usar Expresiones de propiedad para obtener más código seguro de tipos y creé una Personalización genérica. Terminé sin usarlo, pero pensé en compartirlo ya que ya lo había escrito. :-)

Ejemplo. CashRegister tiene una lista con recibos. El recibo tiene una referencia posterior a CashRegister

public class CashRegister : EntityBase
{
    public List<Receipt> Receipts { get; set; }
}

public class Receipt : EntityBase
{
    public CashRegister CashRegister { get; set; }
}

Uso de personalización:
new BackReferenceCustomization<CashRegister, Receipt>(cashRegister => cashRegister.Receipts, receipt => receipt.CashRegister)

Personalización:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AutoFixture;
using AutoFixture.Kernel;

public class BackReferenceCustomization<TTypeWithList, TTypeWithBackReference> : ICustomization where TTypeWithList : class
{
    private readonly Expression<Func<TTypeWithList, List<TTypeWithBackReference>>> _collectionPropertyExpression;
    private readonly Expression<Func<TTypeWithBackReference, TTypeWithList>> _backReferencePropertyExpression;

    public BackReferenceCustomization(
        Expression<Func<TTypeWithList, List<TTypeWithBackReference>>> collectionPropertyExpression,
        Expression<Func<TTypeWithBackReference, TTypeWithList>> backReferencePropertyExpression)
    {
        _collectionPropertyExpression = collectionPropertyExpression;
        _backReferencePropertyExpression = backReferencePropertyExpression;
    }

    public void Customize(IFixture fixture)
    {
        fixture.Customize<TTypeWithBackReference>(c => c.Without(_backReferencePropertyExpression));
        fixture.Customizations.Add(
            new FilteringSpecimenBuilder(
                new Postprocessor(
                    new MethodInvoker(new ModestConstructorQuery()),
                    new CollectionBackReferenceFiller<TTypeWithList, TTypeWithBackReference>(_collectionPropertyExpression, _backReferencePropertyExpression)
                ),
                new TypeSpecification<TTypeWithList>()
            )
        );
    }
}

public class CollectionBackReferenceFiller<TTypeWithList, TTypeWithBackReference> : ISpecimenCommand where TTypeWithList : class
{
    private readonly Expression<Func<TTypeWithList, List<TTypeWithBackReference>>> _collectionPropertyExpression;
    private readonly Expression<Func<TTypeWithBackReference, TTypeWithList>> _backReferencePropertyExpression;

    public CollectionBackReferenceFiller(Expression<Func<TTypeWithList, List<TTypeWithBackReference>>> collectionPropertyExpression, Expression<Func<TTypeWithBackReference, TTypeWithList>> backReferencePropertyExpression)
    {
        _collectionPropertyExpression = collectionPropertyExpression;
        _backReferencePropertyExpression = backReferencePropertyExpression;
    }

    public void Execute(object specimen, ISpecimenContext context)
    {
        if (specimen == null)
            throw new ArgumentNullException(nameof(specimen));
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        if (!(specimen is TTypeWithList typedSpecimen))
            throw new ArgumentException(
                "The specimen must be an instance of " + typeof(TTypeWithList).Name,
                nameof(specimen));

        var elements =
            ((IEnumerable<object>)context
                .Resolve(new MultipleRequest(typeof(TTypeWithBackReference))))
            .Cast<TTypeWithBackReference>().ToList();

        var collectionProperty = (PropertyInfo)((MemberExpression)_collectionPropertyExpression.Body).Member;
        collectionProperty.SetValue(typedSpecimen, elements);

        var backReferenceProperty = (PropertyInfo)((MemberExpression)_backReferencePropertyExpression.Body).Member;

        foreach (var element in elements)
        {
            backReferenceProperty.SetValue(element, typedSpecimen);
        }

        var otherProperties = typedSpecimen.GetType().GetProperties().Where(p => !p.Equals(collectionProperty) && p.SetMethod != null);
        foreach (var property in otherProperties)
        {
            property.SetValue(typedSpecimen, context.Resolve(property.PropertyType));
        }
    }
}

public class TypeSpecification<T> : IRequestSpecification
{
    public bool IsSatisfiedBy(object request)
    {
        var requestType = request as Type;
        if (requestType == null)
            return false;

        return typeof(T).IsAssignableFrom(requestType);
    }
}
¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

JoshKeegan picture JoshKeegan  ·  6Comentarios

ecampidoglio picture ecampidoglio  ·  7Comentarios

malylemire1 picture malylemire1  ·  7Comentarios

zvirja picture zvirja  ·  8Comentarios

mjfreelancing picture mjfreelancing  ·  4Comentarios