Autofixture: Создание дерева объектов, в котором дочерние элементы ссылаются на родительский элемент для разработки DDD

Созданный на 1 февр. 2014  ·  14Комментарии  ·  Источник: AutoFixture/AutoFixture

Всем привет.

Я не хочу создавать дерево объектов, в котором дети ссылаются на родителя.
Например, у автомобиля есть шины, а шина - это автомобиль.

До сих пор я использовал OmitOnRecursionBehavior и вручную присваивал ссылку, демонстрация:

`` С #
var fixture = new Fixture ();
fixture.Behaviors.Add (новый OmitOnRecursionBehavior ());
var car = fixture.Create();
foreach (var шина в машине. покрышки)
{
tyre.Car = автомобиль;
}

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

Что еще хотел сделать:

`` С #
частный класс CarCustomization: ICustomization
{
public void Customize (прибор IFixture)
{
fixture.Behaviors.Add (новый OmitOnRecursionBehavior ());
fixture.Customizations.Add (новый CarBuilder ());
}
}

частный класс CarBuilder: ISpecimenBuilder
{
общедоступный объект Create (запрос объекта, контекст ISpecimenContext)
{
var t = запрос как Тип;
если (t! = null && t == typeof (Автомобиль))
{
// Не получится, строитель зависит сам
var car = context.Create();
foreach (var шина в машине. покрышки)
{
tyre.Car = автомобиль;
}
}

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

Может ли кто-нибудь помочь мне решить эту проблему аккуратно?

Самый полезный комментарий

Образец кода, предоставленный @moodmosaic , идиоматичен, так что это должно быть

Что касается целесообразности использования ORM для DDD, я не согласен. ORM могут показаться мощными, но на самом деле они очень ограничивают. Кажется, что они решают проблему, но создают гораздо больше проблем.

Когда дело доходит до DDD, одним из наиболее важных тактических паттернов, описанных в синей книге, является концепция _Aggregate Root_. Все ORM, которые я когда-либо видел, явно нарушают этот шаблон, потому что вы можете начать с любой данной таблицы и начать загрузку строк этой таблицы, а затем пройтись по различным «свойствам навигации» для загрузки данных из связанных таблиц. Это означает, что нет корней и, следовательно, нет четкого владения данными.

Добавьте к этому, что все изменчиво, и вскоре вы получите систему, о которой очень трудно рассуждать.

Все 14 Комментарий

Я думаю, вы обнаружите, что Tire не должно иметь ссылки на родительский Car . Циркулярные ссылки считаются плохими (по крайней мере, по автофиксации).

Однако и это мне нужно решить. Я работаю с моделями EF, которые не могу изменить, и у них есть циклические ссылки, которые я не могу удалить. Прямо сейчас я избегаю автофиксации, но я бы предпочел приложить некоторые усилия для решения этой общей проблемы (если возможно).

Иногда это удобно, так как большая часть логики находится на уровне домена, а для некоторых дочерних методов может потребоваться ссылка для более чистых реализаций.

Эрик Эванс в своей книге «Доменно-ориентированный дизайн - преодоление сложности в самой основе программного обеспечения»:
Циклические ссылки логически существуют во многих областях и иногда также необходимы в дизайне, но их сложно поддерживать.

Кроме того, с популярными технологиями сохранения, такими как EF и Fluent NHibernate, это приветствуется или, по крайней мере, проще для сопоставлений.

Поэтому поиск удобного решения был бы полезен для меня и других, кто занимается DDD, EF или Fluent NHibernate.

Я думаю, что @ploeh и @moodmosaic в прошлом ясно давали понять, что им не нравятся круговые ссылки и они избегают их везде, где это возможно (поправьте меня, если я ошибаюсь, я не хочу говорить за вас, но это Мое понимание).

Однако известно, что оба они принимают вклады в AutoFixture, которые они не считают особенно полезными, при условии, что это не вызывает у них проблем с ремонтопригодностью и поддержкой (я думаю, очень справедливо).

Я очень мотивирован, чтобы попытаться что-то сделать с этой проблемой (при условии, что есть что-то, что в целом полезно, что будет работать).

Мне нравятся фреймворки, которые поощряют хороший дизайн, и я понимаю проблему с круговыми ссылками, но иногда это логично. Может быть, удастся найти решение, которое не поощряет циклические ссылки, но не вредит красоте AutoFixture.

Спасибо за ответ и положительный настрой: +1:

Да, _если_ нам удастся придумать достойное универсальное решение, это будет не поведение _default_ AutoFixture.

Из CarBuilder выше (тот, который не работает), похоже, что вам действительно нужен Postprocessor<Car> . Вы пробовали это?

В общем, желание использовать AutoFixture для классов ORM является повторяющейся темой, и, как отмечает @adamchester , у меня нет проблем с выполнением этой работы, если это можно сделать таким образом, чтобы это не приводило к проблемам с ремонтопригодностью или поддержкой.

С другой стороны, поскольку я не использовал ORM более трех лет, у меня нет личного интереса в реализации этой функции, поэтому мне придется полагаться на кого-то другого, кто возьмет на себя инициативу.

Что касается необходимости циклических ссылок, я согласен с тем, что концепция реальна, но это не значит, что использовать ее при разработке программного обеспечения - хорошая идея. Циркулярные ссылки сделают код намного более трудным в использовании и рассуждении. Если бы это было неизбежно, нам пришлось бы жить с этим, но IME вы можете _всегда_ разработать API таким образом, чтобы избежать циклических ссылок; Я не знаю, когда в последний раз создавал API с круговой ссылкой, но это было много лет назад.

А как же ORMS, которые в них нуждаются?

Что ж, если вы используете ORM, вы больше не работаете с объектно-ориентированным кодом; ваш код будет относительным, с четкими процедурными элементами. Не позволяйте вашему выбору языка (C # и т. Д.) Обмануть вас.

Возможно, вы все еще можете выполнять DDD таким образом, но это будет реляционный DDD, а не объектно-ориентированный DDD. В итоге вы получите то, что Мартин Фаулер называет сценарием транзакции .

Спасибо за ответ, очень признателен: +1:

Я не слышал о постпроцессорено я попробовал и не смог заставить его работать, возможно, я что-то упускаю. Кажется, что если я использую PostProcessor, тогда тип не был инициализирован значениями при запуске.

Вот мой код:

`` С #
внутренний класс DomainCustomization: ICustomization
{
public void Customize (прибор IFixture)
{
fixture.Behaviors.Add (новый OmitOnRecursionBehavior ());
fixture.Customizations.Add (
новый постпроцессор(((Fixture) fixture) .Engine, новая CarCommand ())
);
}
}

внутренний класс CarCommand: ISpecimenCommand
{
public void Execute (образец объекта, контекст ISpecimenContext)
{
var car = образец как Автомобиль;
если (машина! = ноль)
{
// Здесь исключение нулевой ссылки, без шин, без значений в настройке автомобиля
foreach (var шина в машине. покрышки)
{
tyre.Car = автомобиль;
}
}
}
}
`` ''

Мартин Фаулер - один из моих героев. В одном из своих постов он говорит о модели анемической области, которой я стараюсь избегать. Мои сущности имеют большую часть логики в приложении, а не в сервисах (Transaction Scirpt). Поскольку у сущностей есть вся логика, им могут потребоваться некоторые данные, связанные с ними.

Я всегда стараюсь завершить реализацию уровня сохраняемости после завершения модели предметной области, чтобы база данных отражала мои сущности, а не наоборот.

Однако ORMS, такие как EF / EF CodeFirst и NHibernate, заставляют вас создавать отношения, которые не нужны вашим объектам, и создавать больше циклических ссылок, чем требуется для вашего исходного дизайна, делая ваш дизайн реляционным.

Эти ORMS очень мощные и оправдывают эту «уродство».
Пока мне придется жить с этими технологиями и многими другими, чтобы решение этой проблемы могло быть полезно другим.

Вот проходной тест, в котором SUT создается с заполненными _Tires_, и каждый Tire ссылается на свой Car :

`` С #
[Факт]
public void Test ()
{
var fixture = new Fixture ();
приспособление.(c => c. Без (x => x.Car));
fixture.Customizations.Add (
новый FilteringSpecimenBuilder (
новый постпроцессор (
новый MethodInvoker (
новый ModestConstructorQuery ()),
новый CarFiller ()),
новая CarSpecification ()));

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

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

}

частный класс CarFiller: ISpecimenCommand
{
public void Execute (образец объекта, контекст ISpecimenContext)
{
если (образец == нуль)
throw new ArgumentNullException ("образец");
если (контекст == нуль)
выбросить новое исключение ArgumentNullException ("контекст");

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

}

частный класс CarSpecification: IRequestSpecification
{
public bool IsSatisfiedBy (запрос объекта)
{
var requestType = запрос как Тип;
если (requestType == null)
вернуть ложь;

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

}
`` ''

Милая! Спасибо @moodmosaic , мне не хватало нескольких кусочков этой головоломки.
Красивый код :)

Также спасибо @adamchester и @ploeh за ваши мысли.

Закрою этот выпуск, мне этого достаточно. Вы можете открыть его снова, если хотите создать более общее решение.

Образец кода, предоставленный @moodmosaic , идиоматичен, так что это должно быть

Что касается целесообразности использования ORM для DDD, я не согласен. ORM могут показаться мощными, но на самом деле они очень ограничивают. Кажется, что они решают проблему, но создают гораздо больше проблем.

Когда дело доходит до DDD, одним из наиболее важных тактических паттернов, описанных в синей книге, является концепция _Aggregate Root_. Все ORM, которые я когда-либо видел, явно нарушают этот шаблон, потому что вы можете начать с любой данной таблицы и начать загрузку строк этой таблицы, а затем пройтись по различным «свойствам навигации» для загрузки данных из связанных таблиц. Это означает, что нет корней и, следовательно, нет четкого владения данными.

Добавьте к этому, что все изменчиво, и вскоре вы получите систему, о которой очень трудно рассуждать.

Создать настоящий агрегированный корень с текущими ORM невозможно. Совокупный корень должен быть единственным, кто может изменять свои данные ...

ORM создают ограничения, такие как конструкторы без параметров, установщики для каждого свойства и общедоступные свойства навигации. Это меня очень разозлило.

С моими текущими подходами я не могу действительно доверять тому, что пользователи моей библиотеки изменяют данные через его агрегатный корень. Даже если бы агрегатный корень мог обеспечить это, это не предотвратило бы прямые обновления sql в базе данных ...

Застревание на SqlServer и реляционных базах данных не помогло мне найти другой путь. Может, мне нужно продолжать поиски лучшего пути.

Тем не менее, я считаю, что приложения, которые я создал с помощью DDD, более гибкие в изменении и поддержке. Частью этого также может быть помощь TDD и опыта.

Спасибо за мысли: +1:

Я заметил, что если у вас есть вызов .Customize для типа, обрабатываемого наполнителем, который заставляет его игнорировать. например, fixture.Customize<Car>(c => c.Without(x => x.Doors))

Я пробовал комбинировать их вот так:

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

Таким образом, утверждение об описании не выполняется. Он по-прежнему генерируется автоматически, настройки игнорируются. Основываясь на моем напряженном понимании вещей, я ожидал, что образец, переданный наполнителю, был создан с помощью Composer.

Изменить Теперь я вижу, что else x.SetValue(car, context.Resolve(x.PropertyType)) автоматически генерирует другие свойства. Ваше здоровье

Вот более обобщенная версия:

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

Ваше универсальное решение было отличным!
Я обновил его, чтобы использовать выражения свойств для более безопасного типа кода, и создал общую настройку. Я закончил тем, что не использовал его, но подумал, что поделюсь им, так как я уже написал его. :-)

Пример. В CashRegister есть список квитанций. Квитанция имеет обратную ссылку на CashRegister

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

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

Использование настройки:
new BackReferenceCustomization<CashRegister, Receipt>(cashRegister => cashRegister.Receipts, receipt => receipt.CashRegister)

Настройка:

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);
    }
}
Была ли эта страница полезной?
0 / 5 - 0 рейтинги