Autofixture: Erstellen eines Objektbaums, in dem Kinder auf Eltern für DDD-Design verweisen

Erstellt am 1. Feb. 2014  ·  14Kommentare  ·  Quelle: AutoFixture/AutoFixture

Hi.

Ich möchte nicht in der Lage sein, einen Objektbaum zu erstellen, in dem Kinder auf Eltern verweisen.
Zum Beispiel hat ein Auto Reifen und ein Reifen verweist auf sein Auto.

Bisher habe ich OmitOnRecursionBehavior verwendet und die Referenz manuell zugewiesen, Demo:

``` c#
var Fixture = new Fixture();
Fixture.Behaviors.Add (neues OmitOnRecursionBehavior());
var Auto = Fixture.Create();
foreach (var Reifen im Auto.Reifen)
{
Reifen.Auto = Auto;
}

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()));

Was ich auch machen wollte:

``` c#
Privatklasse CarCustomization : ICustomization
{
public void Customize (IFixture Fixture)
{
Fixture.Behaviors.Add (neues OmitOnRecursionBehavior());
Fixture.Customizations.Add (neuer CarBuilder());
}
}

Privatklasse CarBuilder: ISpecimenBuilder
{
öffentliches Objekt Create(Objektanforderung, ISpecimenContext-Kontext)
{
var t = Anfrage als Typ;
if (t != null && t == typeof(Auto))
{
// Funktioniert nicht, Bauherr hängt von sich selbst ab
var car = context.Create();
foreach (var Reifen im Auto.Reifen)
{
Reifen.Auto = Auto;
}
}

    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; }
}

Kann mir jemand helfen das sauber zu lösen?

Hilfreichster Kommentar

Der von @moodmosaic bereitgestellte richtige Weg sein sollte.

Bezüglich der Angemessenheit der Verwendung eines ORM für DDD bin ich anderer Meinung. ORMs mögen mächtig erscheinen, aber in Wirklichkeit sind sie sehr einschränkend. Sie scheinen ein Problem zu lösen, schaffen aber noch viele weitere Probleme.

Wenn es um DDD geht, ist eines der wichtigsten taktischen Muster, das im Bluebook beschrieben wird, das Konzept einer _Aggregate Root_. Alle ORMs, die ich je gesehen habe, verstoßen eklatant gegen dieses Muster, da Sie bei einer beliebigen Tabelle beginnen und Zeilen dieser Tabelle laden und dann verschiedene "Navigationseigenschaften" durchlaufen können, um Daten aus zugeordneten Tabellen zu laden. Dies bedeutet, dass es keine Wurzeln und damit kein klares Eigentum an Daten gibt.

Hinzu kommt, dass alles veränderbar ist, und Sie haben bald ein System an der Hand, über das nur sehr schwer nachzudenken ist.

Alle 14 Kommentare

Ich denke, Sie werden feststellen, dass Tire keinen Verweis auf das übergeordnete Element Car . Zirkuläre Referenzen gelten als schlecht (zumindest durch Autofixture).

Dies ist jedoch auch etwas, das ich lösen muss. Ich arbeite mit EF-Modellen, die ich nicht ändern kann, und sie haben Zirkelverweise, die ich nicht entfernen kann. Im Moment vermeide ich Autofixture, aber ich würde mir viel lieber Mühe geben, dies generisch zu lösen (wenn möglich).

Das domänengesteuerte Design ist manchmal praktisch, da sich die meiste Logik in der Domänenschicht befindet und einige Methoden auf untergeordneten Elementen möglicherweise die Referenz für sauberere Implementierungen benötigen.

Eric Evans sagt in seinem Buch "Domain-Driven Design - Tackling Complexity in the Heart of Software":
Zirkelverweise gibt es logischerweise in vielen Bereichen und sind manchmal auch im Design notwendig, aber sie sind schwierig zu pflegen.

Auch mit beliebten Persistenztechnologien wie EF und Fluent NHibernate wird dies für die Zuordnungen gefördert oder zumindest einfacher.

Daher wäre es hilfreich für mich und andere, die DDD, EF oder Fluent NHibernate betreiben, eine geeignete Lösung zu finden.

Ich denke, @ploeh und @moodmosaic haben in der Vergangenheit deutlich gemacht, dass sie keine zirkulären Bezüge mögen und sie nach Möglichkeit vermeiden (korrigiert mich, wenn ich falsch

Es ist jedoch bekannt, dass beide Beiträge zu AutoFixture akzeptieren, die sie nicht besonders nützlich finden, vorausgesetzt, dass dies keine Probleme mit der Wartbarkeit und dem Support verursacht (sehr fair, denke ich).

Ich bin sehr motiviert, zu versuchen, etwas gegen dieses Problem zu unternehmen (vorausgesetzt, es gibt etwas, das allgemein nützlich ist und funktioniert).

Ich mag Frameworks, die gutes Design fördern, und ich verstehe das Problem mit Zirkelverweisen, aber manchmal sind sie die logische Lösung. Vielleicht lässt sich eine Lösung finden, die Zirkelbezüge nicht fördert, aber der Schönheit von AutoFixture nicht schadet.

Vielen Dank für Ihre Antwort und Ihre positive Einstellung :+1:

Ja, _wenn_ wir es schaffen, eine anständige generische Lösung zu entwickeln, wäre dies nicht das _Standardverhalten_ von AutoFixture.

Von den CarBuilder oben (derjenige, der nicht funktioniert) sieht es so aus, als ob Sie wirklich ein Postprocessor<Car> brauchen würden. Hast du das probiert?

Im Allgemeinen ist der Wunsch, AutoFixture für ORM-Klassen zu verwenden, ein wiederkehrendes Thema, und wie @adamchester bemerkt, habe ich kein Problem damit, dass dies funktioniert, wenn es so durchgeführt werden kann, dass es keine Wartbarkeits- oder Supportprobleme verursacht.

Auf der anderen Seite, da ich seit mehr als drei Jahren kein ORM verwendet habe, habe ich kein persönliches Interesse daran, dieses Feature zu verfolgen, also müsste es sich darauf verlassen, dass jemand anderes die Führung übernimmt.

Was die Notwendigkeit von Zirkelverweisen angeht, stimme ich zu, dass das Konzept real ist, aber das bedeutet nicht, dass es eine gute Idee ist, es im Softwaredesign zu verwenden. Zirkuläre Verweise erschweren die Verwendung von Code und erschweren das Nachdenken. Wenn es unvermeidlich wäre, müssten wir damit leben, aber mit IME können Sie _immer_ eine API so entwerfen, dass Zirkelverweise vermieden werden; Ich weiß nicht, wann ich das letzte Mal eine API mit einem Zirkelbezug erstellt habe, aber das ist viele Jahre her.

Aber was ist mit ORMS, die sie benötigen?

Nun, wenn Sie ein ORM verwenden, arbeiten Sie nicht mehr mit objektorientiertem Code; Ihr Code wird _relational_ sein, mit starken prozeduralen Elementen. Lassen Sie sich von Ihrer Sprachwahl (C# usw.) nicht täuschen.

Vielleicht können Sie DDD immer noch auf diese Weise ausführen, aber es wird ein relationales DDD sein, kein objektorientiertes DDD. Am Ende erhalten Sie das, was Martin Fowler ein Transaktionsskript nennt.

Vielen Dank für Ihre Antwort, es freut uns sehr :+1:

Ich habe noch nichts vom PostProcessor gehörtaber ich habe es ausprobiert und konnte es nicht zum Laufen bringen, vielleicht übersehe ich etwas. Es scheint, als ob ich den PostProcessor verwende, dann wurde der Typ bei der Ausführung nicht mit Werten initialisiert.

Hier ist mein Code:

``` c#
interne Klasse DomainCustomization : ICustomization
{
public void Customize (IFixture Fixture)
{
Fixture.Behaviors.Add (neues OmitOnRecursionBehavior());
Fixture.Customizations.Add(
neuer Postprozessor(((Fixture)fixture).Motor, neues CarCommand())
);
}
}

interne Klasse CarCommand : ISpecimenCommand
{
public void Execute(Objektmuster, ISpecimenContext-Kontext)
{
var Auto = Exemplar als Auto;
if (Auto != null)
{
//Null-Referenz-Ausnahme hier, keine Reifen, keine Werte im Auto-Sett
foreach (var Reifen im Auto.Reifen)
{
Reifen.Auto = Auto;
}
}
}
}
```

Martin Fowler ist einer meiner Helden. In einem seiner Beiträge spricht er vom anämischen Domänenmodell, das genau das ist, was ich versuche zu vermeiden. Meine Entitäten haben den größten Teil der Logik in der Anwendung und nicht in Diensten (Transaction Script). Da die Entitäten über die gesamte Logik verfügen, benötigen sie möglicherweise einige Daten, die sich auf sie beziehen.

Ich versuche immer, mit der Implementierung der Persistenzschicht zu enden, nachdem das Domänenmodell abgeschlossen ist, damit die Datenbank meine Entitäten widerspiegelt und nicht umgekehrt.

Mit ORMS wie EF/EF CodeFirst und NHibernate können Sie jedoch Beziehungen erstellen, die für Ihre Entitäten nicht erforderlich sind, und mehr zirkuläre Referenzen als für Ihr ursprüngliches Design benötigt, wodurch Ihr Design relational wird.

Diese ORMS sind sehr mächtig und rechtfertigen diese "Hässlichkeit".
Ich werde vorerst mit diesen und vielen anderen Technologien leben müssen, damit eine Lösung für dieses Problem für andere hilfreich sein könnte.

Hier ist ein bestandener Test, bei dem das SUT mit gefüllten _Tires_ erstellt wird und jedes Tire auf seine Car verweist:

``` c#
[Tatsache]
öffentlicher ungültiger Test()
{
var Fixture = new Fixture();
Befestigung.Anpassen(c => c.Ohne(x => x.Auto));
Fixture.Customizations.Add(
neuer FilteringSpecimenBuilder(
neuer Postprozessor (
new MethodInvoker(
neuer ModestConstructorQuery()),
neuer CarFiller()),
neue CarSpecification()));

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

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

}

Privatklasse CarFiller : ISpecimenCommand
{
public void Execute(Objektmuster, ISpecimenContext-Kontext)
{
if (Muster == null)
throw new ArgumentNullException("exemplar");
if (Kontext == null)
throw new ArgumentNullException("context");

    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));
    });
}

}

Privatklasse CarSpecification: IRequestSpecification
{
public bool IsSatisfiedBy(Objektanfrage)
{
var requestType = Anfrage als Typ;
if (requestType == null)
falsch zurückgeben;

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

}
```

Süss! Danke @moodmosaic , mir haben ein paar Puzzleteile gefehlt.
Schöner Code :)

Danke auch an @adamchester und @ploeh für eure Gedanken.

Ich schließe dieses Thema, das reicht mir. Sie können dies erneut öffnen, wenn Sie eine allgemeinere Lösung erstellen möchten.

Der von @moodmosaic bereitgestellte richtige Weg sein sollte.

Bezüglich der Angemessenheit der Verwendung eines ORM für DDD bin ich anderer Meinung. ORMs mögen mächtig erscheinen, aber in Wirklichkeit sind sie sehr einschränkend. Sie scheinen ein Problem zu lösen, schaffen aber noch viele weitere Probleme.

Wenn es um DDD geht, ist eines der wichtigsten taktischen Muster, das im Bluebook beschrieben wird, das Konzept einer _Aggregate Root_. Alle ORMs, die ich je gesehen habe, verstoßen eklatant gegen dieses Muster, da Sie bei einer beliebigen Tabelle beginnen und Zeilen dieser Tabelle laden und dann verschiedene "Navigationseigenschaften" durchlaufen können, um Daten aus zugeordneten Tabellen zu laden. Dies bedeutet, dass es keine Wurzeln und damit kein klares Eigentum an Daten gibt.

Hinzu kommt, dass alles veränderbar ist, und Sie haben bald ein System an der Hand, über das nur sehr schwer nachzudenken ist.

Das Erstellen einer echten Aggregate Root mit aktuellen ORMs ist nicht möglich. Der Aggregate Root sollte der einzige sein, der seine Daten ändern kann...

ORMs erstellen Einschränkungen wie: parameterlose Konstruktoren, Setter für jede Eigenschaft und ihre öffentlichen Navigationseigenschaften. Das hat mich sehr genervt.

Bei meinen aktuellen Ansätzen kann ich nicht wirklich darauf vertrauen, dass Benutzer meiner Bibliothek Daten über das Aggregate Root ändern. Selbst wenn das Aggregate Root dies erzwingen könnte, würde es direkte SQL-Updates in der Datenbank nicht verhindern...

Auf SqlServer und relationalen Datenbanken hängen zu bleiben, hat mir nicht geholfen, einen anderen Weg zu finden. Vielleicht muss ich weiter nach einem besseren Weg suchen.

Trotzdem habe ich das Gefühl, dass Anwendungen, die ich mit DDD erstellt habe, flexibler zu ändern und zu warten sind. Ein Teil davon kann auch die Hilfe von TDD und Erfahrung sein.

Danke für deine Meinung :+1:

Mir ist aufgefallen, dass bei einem .Customize-Aufruf für den vom Füller verarbeiteten Typ der Füller ignoriert wird. zB fixture.Customize<Car>(c => c.Without(x => x.Doors))

Ich habe versucht, diese so zu kombinieren:

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));

Die Bestätigung der Beschreibung schlägt also fehl. Es wird immer noch automatisch generiert, die Anpassungen werden ignoriert. Aufgrund meines angespannten Verständnisses der Dinge hätte ich erwartet, dass das an den Füller übergebene Exemplar mit dem Composer erstellt wurde.

Bearbeiten Ich sehe jetzt, dass else x.SetValue(car, context.Resolve(x.PropertyType)) die anderen Eigenschaften automatisch generiert. Danke schön

Heres eine generischere Version:

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);
            }
        }

Ihre generische Lösung war großartig!
Ich habe es aktualisiert, um Eigenschaftsausdrücke für mehr typsicheren Code zu verwenden, und eine generische Anpassung erstellt. Am Ende habe ich es nicht benutzt, aber ich dachte, ich würde es teilen, da ich es bereits geschrieben hatte. :-)

Beispiel. CashRegister hat eine Liste mit Quittungen. Quittung hat einen Rückverweis auf CashRegister

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

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

Verwendung der Anpassung:
new BackReferenceCustomization<CashRegister, Receipt>(cashRegister => cashRegister.Receipts, receipt => receipt.CashRegister)

Anpassung:

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);
    }
}
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

JoshKeegan picture JoshKeegan  ·  6Kommentare

ploeh picture ploeh  ·  7Kommentare

mjfreelancing picture mjfreelancing  ·  4Kommentare

Ephasme picture Ephasme  ·  3Kommentare

Ridermansb picture Ridermansb  ·  4Kommentare