Autofixture: Création d'une arborescence d'objets où les enfants font référence au parent pour la conception DDD

Créé le 1 févr. 2014  ·  14Commentaires  ·  Source: AutoFixture/AutoFixture

Salut.

Je ne veux pas pouvoir créer une arborescence d'objets où les enfants font référence au parent.
Par exemple, une voiture a des pneus et un pneu fait référence à sa voiture.

Jusqu'à présent, j'ai utilisé OmitOnRecursionBehavior et attribué la référence manuellement, démo :

``` c#
var fixture = new Fixture();
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
var voiture = luminaire.Créer();
foreach (pneu var dans la voiture.Pneus)
{
pneu.Voiture = voiture;
}

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

Ce que je voulais aussi faire :

``` c#
classe privée CarCustomization : ICustomization
{
vide public Personnaliser (fixation IFixture)
{
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
fixture.Customizations.Add(new CarBuilder());
}
}

classe privée CarBuilder : ISpecimenBuilder
{
objet public Créer (demande d'objet, contexte ISpecimenContext)
{
var t = demande en tant que type ;
if (t != null && t == typeof(Voiture))
{
//Ne fonctionnera pas, le constructeur dépend de lui-même
var voiture = contexte.Créer();
foreach (pneu var dans la voiture.Pneus)
{
pneu.Voiture = voiture;
}
}

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

Quelqu'un peut-il m'aider à résoudre cela proprement?

Commentaire le plus utile

L'exemple de code fourni par @moodmosaic est idiomatique, ce devrait donc être la voie à suivre en tant que solution spécifique à un problème spécifique.

En ce qui concerne l'opportunité d'utiliser un ORM pour DDD, je ne suis pas d'accord. Les ORM peuvent sembler puissants, mais en fait, ils sont très contraignants. Ils semblent résoudre un problème, mais créent beaucoup plus de problèmes.

En ce qui concerne le DDD, l'un des modèles tactiques les plus importants décrits dans le livre bleu est le concept d'une _racine agrégée_. Tous les ORM que j'ai jamais vus violent de manière flagrante ce modèle, car vous pouvez commencer à partir de n'importe quelle table et commencer à charger des lignes de cette table, puis parcourir diverses « propriétés de navigation » pour charger les données des tables associées. Cela signifie qu'il n'y a pas de racines et donc pas de propriété claire des données.

Ajoutez à cela que tout est mutable, et vous aurez bientôt un système sous la main, ce qui est très difficile à raisonner.

Tous les 14 commentaires

Je pense que vous constaterez que Tire ne devrait pas avoir de référence au parent Car . Les références circulaires sont considérées comme mauvaises (par autofixation, au moins).

Cependant, c'est quelque chose que je dois également résoudre. Je travaille avec des modèles EF que je ne peux pas modifier et ils ont des références circulaires que je ne peux pas supprimer. Pour le moment, j'évite la fixation automatique, mais je préférerais de beaucoup faire des efforts pour résoudre ce problème de manière générique (si possible).

Faire de la conception axée sur le domaine est parfois pratique car la plupart des logiques se trouvent dans la couche de domaine et certaines méthodes sur l'enfant peuvent avoir besoin de la référence pour des implémentations plus propres.

Eric Evans dit dans son livre " Domain-Driven Design - Tackling Complexity in the Heart of Software" :
Les références circulaires existent logiquement dans de nombreux domaines et sont parfois nécessaires en conception également, mais elles sont délicates à maintenir.

De plus, avec les technologies de persistance populaires comme EF et Fluent NHibernate, il est encouragé ou du moins plus facile pour les mappages.

Trouver une solution pratique serait donc utile pour moi et pour les autres utilisateurs de DDD, EF ou Fluent NHibernate.

Je pense que @ploeh et @moodmosaic ont clairement indiqué dans le passé qu'ils n'aiment pas les références circulaires et les évitent dans la mesure du possible (corrigez-moi si je me trompe, je ne veux pas parler pour vous, mais c'est ma compréhension).

Cependant, les deux sont connus pour accepter des contributions à AutoFixture qu'ils ne trouvent pas particulièrement utiles, à condition que cela ne leur cause pas de problèmes de maintenabilité et de support (très juste, je pense).

Je suis très motivé pour essayer de faire quelque chose à propos de ce problème (en supposant qu'il y ait quelque chose de génériquement utile qui fonctionnera).

J'aime les frameworks qui encouragent un bon design et je comprends le problème des références circulaires mais parfois c'est la chose logique à faire. Peut-être qu'une solution peut être trouvée qui n'encourage pas les références circulaires mais ne nuit pas à la beauté d'AutoFixture.

Merci pour votre réponse et votre attitude positive :+1:

Oui, _si_ nous parvenons à proposer une solution générique décente, ce ne serait pas le comportement _par défaut_ d'AutoFixture.

D'après le CarBuilder ci-dessus (celui qui ne fonctionne pas), il semble que vous ayez vraiment besoin d'un Postprocessor<Car> . Avez-vous essayé cela?

En général, le désir d'utiliser AutoFixture pour les classes ORM est un thème récurrent, et comme le note @adamchester , je n'ai aucun problème à faire fonctionner cela si cela peut être fait de manière à ne pas causer de problèmes de maintenance ou de support.

D'un autre côté, n'ayant pas utilisé d'ORM depuis plus de trois ans, je n'ai aucun intérêt personnel à poursuivre cette fonctionnalité, il faudrait donc que quelqu'un d'autre en prenne la direction.

Concernant le besoin de références circulaires, je suis d'accord pour dire que le concept est réel, mais cela ne veut pas dire que c'est une bonne idée de l'utiliser dans la conception de logiciels. Les références circulaires rendront le code beaucoup plus difficile à utiliser et à raisonner. Si c'était inévitable, alors nous devrions vivre avec, mais IME vous pouvez _toujours_ concevoir une API d'une manière qui évite les références circulaires ; Je ne sais pas quand j'ai créé pour la dernière fois une API avec une référence circulaire, mais c'était il y a de nombreuses années.

Mais qu'en est-il des ORMS, qui en ont besoin ?

Eh bien, si vous utilisez un ORM, vous ne travaillez plus avec du code orienté objet ; votre code sera _relationnel_, avec des éléments procéduraux forts. Ne laissez pas votre choix de langue (C#, etc.) vous tromper.

Peut-être que vous pouvez toujours faire du DDD de cette manière, mais ce sera du DDD relationnel, pas du DDD orienté objet. Vous vous retrouverez avec ce que Martin Fowler appelle un script de transaction .

Merci pour votre réponse, c'est très apprécié :+1:

Je n'ai pas entendu parler du PostProcessormais je l'ai essayé et je n'ai pas pu le faire fonctionner, peut-être que j'ai raté quelque chose. Il semble que si j'utilise le PostProcessor, le type n'a pas été initialisé avec des valeurs lors de son exécution.

Voici mon code :

``` c#
classe interne DomainCustomization : ICustomization
{
vide public Personnaliser (fixation IFixture)
{
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
fixture.Customizations.Add(
nouveau postprocesseur(((Fixture)fixture).Moteur, nouveau CarCommand())
);
}
}

classe interne CarCommand : ISpecimenCommand
{
public void Execute (spécimen d'objet, contexte ISpecimenContext)
{
var car = spécimen en tant que voiture ;
si (voiture != null)
{
//Exeption de référence nulle ici, pas de pneus, pas de valeurs dans le réglage de la voiture
foreach (pneu var dans la voiture.Pneus)
{
pneu.Voiture = voiture;
}
}
}
}
```

Martin Fowler est l'un de mes héros. Dans l'un de ses articles, il parle du modèle de domaine anémique, qui est exactement ce que j'essaie d'éviter. Mes entités ont la plupart de la logique dans l'application et non des services (Transaction Scirpt). Étant donné que les entités ont toute la logique, elles peuvent avoir besoin de données les concernant.

J'essaie toujours de terminer par l'implémentation de la couche de persistance une fois le modèle de domaine terminé afin que la base de données soit le reflet de mes entités et non l'inverse.

Cependant, les ORMS comme EF/EF CodeFirst et NHibernate vous permettent de créer des relations non nécessaires pour vos entités et des références plus circulaires que celles nécessaires à votre conception d'origine, ce qui rend votre conception relationnelle.

Ces ORMS sont très puissants et justifient cette « laideur ».
Je vais devoir vivre avec ces technologies pour le moment et bien d'autres donc une solution à ce problème pourrait être utile à d'autres.

Voici un test de réussite où le SUT est créé avec ses _Pneus_ remplis et chaque Tire référence à ses Car :

``` c#
[Fait]
Test du vide public()
{
var fixture = new Fixture();
luminaire.Personnaliser(c => c.Sans(x => x.Voiture));
fixture.Customizations.Add(
nouveau FilteringSpecimenBuilder(
nouveau postprocesseur(
new MethodInvoker(
nouveau ModestConstructorQuery()),
nouveau CarFiller()),
nouvelle CarSpecification()));

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

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

}

classe privée CarFiller : ISpecimenCommand
{
public void Execute (spécimen d'objet, contexte ISpecimenContext)
{
if (spécimen == null)
throw new ArgumentNullException("spécimen");
si (contexte == nul)
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));
    });
}

}

classe privée CarSpecification : IRequestSpecification
{
public bool IsSatisfiedBy (demande d'objet)
{
var requestType = demande en tant que type ;
if (requestType == null)
renvoie faux ;

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

}
```

Doux! Merci @moodmosaic , il me manquait quelques pièces de ce puzzle.
Beau code :)

Merci également à @adamchester et @ploeh pour vos réflexions.

Je clos ce sujet, cela me suffit. Vous pouvez le rouvrir si vous souhaitez créer une solution plus générique.

L'exemple de code fourni par @moodmosaic est idiomatique, ce devrait donc être la voie à suivre en tant que solution spécifique à un problème spécifique.

En ce qui concerne l'opportunité d'utiliser un ORM pour DDD, je ne suis pas d'accord. Les ORM peuvent sembler puissants, mais en fait, ils sont très contraignants. Ils semblent résoudre un problème, mais créent beaucoup plus de problèmes.

En ce qui concerne le DDD, l'un des modèles tactiques les plus importants décrits dans le livre bleu est le concept d'une _racine agrégée_. Tous les ORM que j'ai jamais vus violent de manière flagrante ce modèle, car vous pouvez commencer à partir de n'importe quelle table et commencer à charger des lignes de cette table, puis parcourir diverses « propriétés de navigation » pour charger les données des tables associées. Cela signifie qu'il n'y a pas de racines et donc pas de propriété claire des données.

Ajoutez à cela que tout est mutable, et vous aurez bientôt un système sous la main, ce qui est très difficile à raisonner.

Il n'est pas possible de créer une véritable racine d'agrégat avec les ORM actuels. La racine agrégée devrait être la seule capable de modifier ses données...

Les ORM créent des contraintes telles que : des constructeurs sans paramètre, des setters pour chaque propriété et ses propriétés de navigation publiques. Cela m'a beaucoup ennuyé.

Avec mes approches actuelles, je ne peux pas vraiment faire confiance aux utilisateurs de ma bibliothèque pour modifier les données via sa racine agrégée. Même si la racine agrégée pouvait appliquer cela, cela n'empêcherait pas les mises à jour directes de SQL dans la base de données...

Être bloqué sur SqlServer et les bases de données relationnelles ne m'a pas aidé à trouver un autre moyen. Peut-être que je dois continuer à chercher un meilleur moyen.

Malgré tout, je pense que les applications que j'ai créées avec DDD sont plus flexibles à modifier et à maintenir. Une partie de cela peut également être l'aide du TDD et de l'expérience.

Merci pour vos idées :+1:

J'ai remarqué que si vous avez un appel .Customize pour le type en cours de traitement par le filler, le filler est ignoré. par exemple fixture.Customize<Car>(c => c.Without(x => x.Doors))

J'ai essayé de combiner ceux-ci comme ceci:

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

Ainsi, l'assertion sur la description échoue. Il est toujours généré automatiquement, les personnalisations sont ignorées. Sur la base de ma compréhension tendue des choses, je me serais attendu à ce que le spécimen transmis au remplisseur ait été créé à l'aide du compositeur.

Modifier Je vois maintenant que le else x.SetValue(car, context.Resolve(x.PropertyType)) génère automatiquement les autres propriétés. À votre santé

Voici une version plus générique :

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

Votre solution générique était géniale !
Je l'ai mis à jour pour utiliser des expressions de propriété pour un code plus sûr et j'ai créé une personnalisation générique. J'ai fini par ne pas l'utiliser, mais j'ai pensé le partager puisque je l'avais déjà écrit. :-)

Exemple. CashRegister a une liste avec des reçus. Le reçu a une référence arrière à CashRegister

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

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

Utilisation de la personnalisation :
new BackReferenceCustomization<CashRegister, Receipt>(cashRegister => cashRegister.Receipts, receipt => receipt.CashRegister)

Personnalisation :

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);
    }
}
Cette page vous a été utile?
0 / 5 - 0 notes