Как создать вложенное свойство с автофиксацией

Как установить вложенное свойство с автофиксацией (только для чтения)? Что-то вроде этого:

var result =
    fixture.Build<X>()
    .With(x => x.First.Second.Third, "value")
    .Create();

person user963935    schedule 19.06.2015    source источник
comment
Если свойство доступно только для чтения, не означает ли это, что вы все равно не сможете (не должны) установить его?   -  person Dylan Corriveau    schedule 19.06.2015
comment
Я могу сделать это, как это установлено в конструкторе.   -  person user963935    schedule 19.06.2015
comment
Если он доступен только для чтения, вы не можете присваивать ему значения, но вот как обращаться с аргументами конструктора: stackoverflow.com/q/ 28350054/126014   -  person Mark Seemann    schedule 19.06.2015


Ответы (1)


Если я правильно понимаю вопрос, я предполагаю, что у нас есть такие классы:

public class X
{
    public X(One first, string foo)
    {
        First = first;
        Foo = foo;
    }

    public One First { get; }

    public string Foo { get; }
}

public class One
{
    public One(Two second, int bar)
    {
        Second = second;
        Bar = bar;
    }

    public Two Second { get; }

    public int Bar { get; }
}

public class Two
{
    public Two(string third, bool baz)
    {
        Third = third;
        Baz = baz;
    }

    public string Third { get; }

    public bool Baz { get; }
}

В частности, я добавил свойства Foo, Bar и Baz в каждый из этих классов, чтобы подчеркнуть, что, хотя кому-то может быть интересно установить для x.First.Second.Third определенное значение, все равно будет интересно, чтобы все остальные свойства заполнялись AutoFixture.

Как правило, как только вы начинаете работать с неизменяемыми значениями, такие языки, как C#, начинают раскрывать свои ограничения. Хотя это возможно, это идет вразрез с сутью языка.

У написания кода с неизменяемыми данными есть много других преимуществ, но в C# это становится утомительным. Это одна из причин, по которой я окончательно отказался от C# и перешел на F# и Haskell. Хотя это небольшое отступление, я упомянул об этом, чтобы явно сообщить, что я думаю, что использование свойств, доступных только для чтения, является прекрасным дизайнерским решением, но оно сопряжено с некоторыми известными проблемами.

Как правило, при работе с неизменяемыми значениями, особенно при тестировании, рекомендуется добавьте методы копирования и обновления к каждому неизменяемому классу, начиная с X:

public X WithFirst(One newFirst)
{
    return new X(newFirst, this.Foo);
}

On One:

public One WithSecond(Two newSecond)
{
    return new One(newSecond, this.Bar);
}

и Two:

public Two WithThird(string newThird)
{
    return new Two(newThird, this.Baz);
}

Это позволяет вам использовать метод расширения Get Fixture для создания значения X с конкретным значением First.Second.Third, но при этом все остальные значения свободно заполняются AutoFixture.

Проходит следующий тест:

[Fact]
public void BuildWithThird()
{
    var fixture = new Fixture();

    var actual =
        fixture.Get((X x, One first, Two second) =>
            x.WithFirst(first.WithSecond(second.WithThird("ploeh"))));

    Assert.Equal("ploeh", actual.First.Second.Third);
    Assert.NotNull(actual.Foo);
    Assert.NotEqual(default(int), actual.First.Bar);
    Assert.NotEqual(default(bool), actual.First.Second.Baz);
}

Здесь используется перегрузка Fixture.Get, которая принимает делегата с тремя входными значениями. Все эти значения заполняются AutoFixture, а затем вы можете вложить методы копирования и обновления, используя x, first и second.

Утверждения показывают, что не только actual.First.Second.Third имеет ожидаемое значение, но и все остальные свойства также заполнены.

Линзы

Вам может показаться излишним запрашивать у AutoFixture значения first и second, поскольку x уже должно их содержать. Вместо этого вы можете захотеть иметь возможность просто «достучаться» до First.Second.Third без необходимости иметь дело со всеми этими промежуточными значениями.

Это возможно с помощью линз.

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

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

Короче говоря, вы можете определить линзу в C# следующим образом:

public class Lens<T, V>
{
    public Lens(Func<T, V> getter, Func<V, T, T> setter)
    {
        Getter = getter;
        Setter = setter;
    }

    internal Func<T, V> Getter { get; }

    internal Func<V, T, T> Setter { get; }
}

Объектив — это пара функций. Getter возвращает значение свойства с учетом «полного» объекта. Setter — это функция, которая принимает значение и старый объект и возвращает новый объект со свойством, измененным на значение.

Вы можете определить набор функций, которые работают с линзами:

public static class Lens
{
    public static V Get<T, V>(this Lens<T, V> lens, T item)
    {
        return lens.Getter(item);
    }

    public static T Set<T, V>(this Lens<T, V> lens, T item, V value)
    {
        return lens.Setter(value, item);
    }

    public static Lens<T, V> Compose<T, U, V>(
        this Lens<T, U> lens1,
        Lens<U, V> lens2)
    {
        return new Lens<T, V>(
            x => lens2.Get(lens1.Get(x)),
            (v, x) => lens1.Set(x, lens2.Set(lens1.Get(x), v)));
    }
}

Set и Get просто позволяют получить значение свойства или установить для свойства определенное значение. Здесь интересна функция Compose, которая позволяет составить линзу от T до U с линзой от U до V.

Это работает лучше всего, если у вас есть статические линзы, определенные для каждого класса, например, для X:

public static Lens<X, One> FirstLens =
    new Lens<X, One>(x => x.First, (f, x) => x.WithFirst(f));

One:

public static Lens<One, Two> SecondLens =
    new Lens<One, Two>(o => o.Second, (s, o) => o.WithSecond(s));

Two:

public static Lens<Two, string> ThirdLens =
    new Lens<Two, string>(t => t.Third, (s, t) => t.WithThird(s));

Это шаблонный код, но он прост, как только вы его освоите. Даже в Haskell это шаблон, но его можно автоматизировать с помощью Template Haskell.

Это позволяет вам написать тест, используя составную линзу:

[Fact]
public void BuildWithLenses()
{
    var fixture = new Fixture();

    var actual = fixture.Get((X x) =>
        X.FirstLens.Compose(One.SecondLens).Compose(Two.ThirdLens).Set(x, "ploeh"));

    Assert.Equal("ploeh", actual.First.Second.Third);
    Assert.NotNull(actual.Foo);
    Assert.NotEqual(default(int), actual.First.Bar);
    Assert.NotEqual(default(bool), actual.First.Second.Baz);
}

Вы берете X.FirstLens, то есть линзу от X до One, и сначала соединяете ее с One.SecondLens, то есть линзой от One до Two. Результат пока объектив от X до Two.

Так как это свободный интерфейс, вы можете продолжить и составить этот объектив с помощью Two.ThirdLens, что является объектив от Two до string. Последняя составленная линза — это линза от X до string.

Затем вы можете использовать метод расширения Set, чтобы установить этот объектив от x до "ploeh". Утверждения такие же, как и выше, и тест по-прежнему проходит.

Состав объектива выглядит многословным, но в основном это артефакт ограниченной поддержки C# пользовательских операторов. В Haskell аналогичная композиция будет буквально выглядеть как first.second.third, где first, second и third — линзы.

person Mark Seemann    schedule 26.03.2018
comment
Я люблю линзы так же сильно, как и любой другой парень, но считаю, что они недостаточно сильны в C#. Вы не можете выразить иерархию подтипов Lens-Traversal-Setter-Fold, потому что вам не разрешено множественное наследование. Так совпало, что пару недель назад я задумался о том, как здесь могут помочь методы интерфейса по умолчанию (Я назвал ваш Compose _, потому что хотел, чтобы синтаксис был кратким.) Вы можете напрямую выражать подтипы, а разрешение перегрузки выбирает правильный тип возвращаемого значения для l1._(l2). Однако не доволен ITraversal. - person Benjamin Hodgson♦; 01.04.2018
comment
Я подумал о том, чтобы поместить это в библиотеку после выхода C#8 — это дополнит мою общую библиотеку программирования красиво, обеспечивая более богатый словарный запас, чтобы говорить о перезаписываемых типах. Однако, как вы заметили, вам нужно вручную писать шаблонные линзы для каждого свойства в вашей системе, что, по IMO, слишком утомительно, чтобы сделать любую такую ​​библиотеку действительно пригодной для использования в производстве. (Я оставляю вопрос о том, как это исправить...) Также без этого клиентский код содержит в два раза больше параметров типа - person Benjamin Hodgson♦; 01.04.2018