Autofixture: Criação de uma árvore de objetos onde os filhos fazem referência ao pai para o design DDD

Criado em 1 fev. 2014  ·  14Comentários  ·  Fonte: AutoFixture/AutoFixture

Olá.

Não quero ser capaz de criar uma árvore de objetos em que os filhos façam referência aos pais.
Por exemplo, um carro tem pneus e referências de pneu é carro.

Até agora, tenho usado OmitOnRecursionBehavior e atribuí na referência manualmente a demonstração:

`` `c #
var fixture = new Fixture ();
fixture.Behaviors.Add (new OmitOnRecursionBehavior ());
var car = fixture.Create();
foreach (var pneu no carro. Pneus)
{
pneu.Carro = carro;
}

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

O que eu também queria fazer:

`` `c #
classe privada CarCustomization: ICustomization
{
public void Customize (IFixture fixture)
{
fixture.Behaviors.Add (new OmitOnRecursionBehavior ());
fixture.Customizations.Add (new CarBuilder ());
}
}

classe privada CarBuilder: ISpecimenBuilder
{
criar objeto público (solicitação de objeto, contexto ISpecimenContext)
{
var t = solicitar como tipo;
if (t! = null && t == typeof (Car))
{
// Não funciona, o construtor depende de si mesmo
var car = context.Create();
foreach (var pneu no carro. Pneus)
{
pneu.Carro = carro;
}
}

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

Alguém pode me ajudar a resolver isso de forma organizada?

Comentários muito úteis

O código de amostra fornecido por @moodmosaic é idiomático, então esse deve ser o caminho a seguir como uma solução específica para um problema específico.

Quanto à adequação de usar um ORM para DDD, discordo. Os ORMs podem parecer poderosos, mas, na verdade, eles são muito restritivos. Parecem resolver um problema, mas criam muitos mais problemas.

Quando se trata de DDD, um dos padrões táticos mais importantes descritos no livro azul é o conceito de _Riz agregada_. Todos os ORMs que já vi violam abertamente esse padrão, porque você pode começar em qualquer tabela e começar a carregar linhas dessa tabela e, em seguida, percorrer várias 'propriedades de navegação' para carregar dados de tabelas associadas. Isso significa que não há raízes e, portanto, nenhuma propriedade clara dos dados.

Acrescente a isso que tudo é mutável e em breve você terá um sistema em suas mãos, o que é muito difícil de raciocinar.

Todos 14 comentários

Acho que você descobrirá que Tire não deve ter uma referência ao pai Car . Referências circulares são consideradas ruins (por autofixture, pelo menos).

No entanto, isso é algo que também preciso resolver. Estou trabalhando com modelos EF que não posso alterar e eles têm referências circulares que não posso remover. No momento, estou evitando a autofixação, mas prefiro me esforçar para resolver isso genericamente (se possível).

Fazer o Domain Driven Design às vezes é conveniente, pois a maior parte da lógica está na camada de domínio e alguns métodos no filho podem precisar da referência para implementações mais limpas.

Eric Evans diz em seu livro "Domain-Driven Design - Tackling Complexity in the Heart of Software":
As referências circulares existem logicamente em muitos domínios e às vezes também são necessárias no design, mas são difíceis de manter.

Além disso, com tecnologias de persistência populares como EF e Fluent NHibernate, isso é encorajado ou pelo menos mais fácil para os mapeamentos.

Encontrar uma solução conveniente, portanto, seria útil para mim e outras pessoas que estão fazendo DDD, EF ou NHibernate Fluent.

Acho que @ploeh e @moodmosaic deixaram claro no passado que não gostam de referências circulares e as evitam sempre que possível (corrija-me se estiver errado, não quero falar por você, mas isso é meu entendimento).

No entanto, ambos são conhecidos por aceitar contribuições para o AutoFixture que eles não consideram particularmente úteis, desde que não lhes cause problemas de manutenção e suporte (muito justo, eu acho).

Estou muito motivado para tentar fazer algo sobre esse problema (presumindo que haja algo genericamente útil que funcione).

Gosto de frameworks que incentivam um bom design e entendo o problema das referências circulares, mas às vezes são a coisa mais lógica a se fazer. Talvez seja encontrada uma solução que não incentive referências circulares, mas não prejudique a beleza da AutoFixtura.

Obrigado por sua resposta e sua atitude positiva: +1:

Sim, _se_ conseguirmos encontrar uma solução genérica decente, não seria o comportamento _padrão_ do AutoFixture.

Do CarBuilder acima (aquele que não funciona), parece que o que você realmente precisa é de Postprocessor<Car> . Você já tentou isso?

Em geral, o desejo de usar AutoFixture para classes ORM é um tema recorrente e, como @adamchester observa, não tenho problemas em fazer esse trabalho se puder ser feito de forma que não cause problemas de manutenção ou suporte.

Por outro lado, não tendo usado um ORM por mais de três anos, não tenho nenhum interesse pessoal em buscar esse recurso, então teria que depender de outra pessoa para assumir a liderança.

Quanto à necessidade de referências circulares, concordo que o conceito é real, mas isso não significa que seja uma boa ideia utilizá-lo no design de software. As referências circulares tornarão o código muito mais difícil de usar e raciocinar. Se fosse inevitável, teríamos que conviver com isso, mas IME você pode _sempre_ projetar uma API de uma forma que evite referências circulares; Não sei quando criei uma API com uma referência circular pela última vez, mas foi há muitos anos.

Mas e quanto ao ORMS, que os requer?

Bem, se você usa um ORM, não está mais trabalhando com código orientado a objetos; seu código será _relacional_, com fortes elementos procedurais. Não se deixe enganar pela sua escolha de linguagem (C #, etc.).

Talvez você ainda possa fazer DDD dessa forma, mas será um DDD relacional, não um DDD orientado a objetos. Você acabará com o que Martin Fowler chama de Script de Transação .

Obrigado pela sua resposta, é muito apreciado: +1:

Eu não ouvi falar do PostProcessormas eu tentei e não consegui fazer funcionar, talvez eu esteja faltando alguma coisa. Parece que se eu usar o PostProcessor, o tipo não foi inicializado com valores ao ser executado.

Aqui está o meu código:

`` `c #
classe interna DomainCustomization: ICustomization
{
public void Customize (IFixture fixture)
{
fixture.Behaviors.Add (new OmitOnRecursionBehavior ());
fixture.Customizations.Add (
novo pós-processador(((Fixture) fixture) .Motor, novo CarCommand ())
);
}
}

classe interna CarCommand: ISpecimenCommand
{
public void Execute (objeto espécime, contexto ISpecimenContext)
{
var car = espécime como Car;
if (carro! = nulo)
{
// Exceção de referência nula aqui, sem pneus, sem valores na configuração do carro
foreach (var pneu no carro. Pneus)
{
pneu.Carro = carro;
}
}
}
}
`` `

Martin Fowler é um dos meus heróis. Em uma de suas postagens, ele fala do Modelo de Domínio Anêmico, que é exatamente o que estou tentando evitar. Minhas entidades possuem a maior parte da lógica do aplicativo e não dos serviços (Transaction Scirpt). Como as entidades têm toda a lógica, podem precisar de alguns dados relacionados a elas.

Sempre tento terminar com a implementação da camada de persistência depois que o modelo de domínio estiver concluído, para que o banco de dados seja um reflexo de minhas entidades e não o contrário.

No entanto, os ORMS como EF / EF CodeFirst e NHibernate fazem com que você crie relações desnecessárias para suas entidades e referências mais circulares do que o necessário para seu design original, tornando seu design relacional.

Esses ORMS são muito poderosos e justificam essa "feiura".
Terei que conviver com essas tecnologias por enquanto e muitas outras, de forma que uma solução para esse problema possa ser útil para outras pessoas.

Aqui está um teste de aprovação onde o SUT é criado com seus _Tires_ preenchidos e cada Tire referência a seus Car :

`` `c #
[Facto]
public void Test ()
{
var fixture = new Fixture ();
fixture.Customize(c => c.Sem (x => x.Car));
fixture.Customizations.Add (
novo FilteringSpecimenBuilder (
novo Pós-processador (
new MethodInvoker (
novo ModestConstructorQuery ()),
novo CarFiller ()),
nova CarSpecification ()));

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

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

}

classe privada CarFiller: ISpecimenCommand
{
public void Execute (objeto espécime, contexto ISpecimenContext)
{
if (espécime == null)
lance novo ArgumentNullException ("specimen");
if (contexto == null)
lance novo 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));
    });
}

}

classe privada CarSpecification: IRequestSpecification
{
public bool IsSatisfiedBy (solicitação de objeto)
{
var requestType = request as Type;
if (requestType == null)
retorna falso;

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

}
`` `

Doce! Obrigado @moodmosaic , estava faltando algumas peças deste quebra-cabeça.
Belo código :)

Obrigado também a @adamchester e @ploeh por seus pensamentos.

Vou encerrar esse problema, isso é o suficiente para mim. Você pode reabrir isso se quiser criar uma solução mais genérica.

O código de amostra fornecido por @moodmosaic é idiomático, então esse deve ser o caminho a seguir como uma solução específica para um problema específico.

Quanto à adequação de usar um ORM para DDD, discordo. Os ORMs podem parecer poderosos, mas, na verdade, eles são muito restritivos. Parecem resolver um problema, mas criam muitos mais problemas.

Quando se trata de DDD, um dos padrões táticos mais importantes descritos no livro azul é o conceito de _Riz agregada_. Todos os ORMs que já vi violam abertamente esse padrão, porque você pode começar em qualquer tabela e começar a carregar linhas dessa tabela e, em seguida, percorrer várias 'propriedades de navegação' para carregar dados de tabelas associadas. Isso significa que não há raízes e, portanto, nenhuma propriedade clara dos dados.

Acrescente a isso que tudo é mutável e em breve você terá um sistema em suas mãos, o que é muito difícil de raciocinar.

Não é possível criar uma verdadeira raiz agregada com ORMs atuais. A raiz agregada deve ser a única capaz de alterar seus dados ...

ORMs criam restrições como: construtores sem parâmetros, setters para cada propriedade e suas propriedades de navegação públicas. Isso me irritou muito.

Com minhas abordagens atuais, não posso realmente confiar que os usuários de minha biblioteca alterem dados por meio de sua raiz agregada. Mesmo se a raiz agregada pudesse impor isso, não impediria atualizações diretas de sql no banco de dados ...

Ficar preso no SqlServer e nos bancos de dados relacionais não me ajudou a encontrar outra maneira. Talvez eu precise continuar procurando uma maneira melhor.

Mesmo assim, acho que os aplicativos que criei com DDD são mais flexíveis para alterar e manter. Uma parte disso também pode ser a ajuda de TDD e experiência.

Obrigado por sua opinião: +1:

Percebi que se você tiver uma chamada .Customize para o tipo que está sendo processado pelo Filler, isso faz com que o filler seja ignorado. por exemplo, fixture.Customize<Car>(c => c.Without(x => x.Doors))

Tentei combiná-los assim:

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

Portanto, a afirmação sobre a descrição falha. Ainda sendo gerado automaticamente, as personalizações são ignoradas. Com base no meu entendimento tenso das coisas, eu esperava que o espécime passado para o enchedor tivesse sido criado usando o Composer.

Editar , vejo agora que else x.SetValue(car, context.Resolve(x.PropertyType)) está gerando automaticamente as outras propriedades. Saúde

Aqui está uma versão mais 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);
            }
        }

Sua solução genérica foi ótima!
Eu o atualizei para usar Expressões de propriedade para obter mais código de tipo seguro e criei uma personalização genérica. Acabei não usando, mas achei que iria compartilhar, pois já havia escrito. :-)

Exemplo. CashRegister tem lista com recibos. O recibo tem uma referência anterior a CashRegister

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

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

Uso de Personalização:
new BackReferenceCustomization<CashRegister, Receipt>(cashRegister => cashRegister.Receipts, receipt => receipt.CashRegister)

Costumização:

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);
    }
}
Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

josh-degraw picture josh-degraw  ·  4Comentários

TroyHouston picture TroyHouston  ·  6Comentários

tomasaschan picture tomasaschan  ·  3Comentários

mjfreelancing picture mjfreelancing  ·  4Comentários

ecampidoglio picture ecampidoglio  ·  7Comentários