Как установить вложенное свойство с автофиксацией (только для чтения)? Что-то вроде этого:
var result =
fixture.Build<X>()
.With(x => x.First.Second.Third, "value")
.Create();
Как установить вложенное свойство с автофиксацией (только для чтения)? Что-то вроде этого:
var result =
fixture.Build<X>()
.With(x => x.First.Second.Third, "value")
.Create();
Если я правильно понимаю вопрос, я предполагаю, что у нас есть такие классы:
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
— линзы.
Lens
-Traversal
-Setter
-Fold
, потому что вам не разрешено множественное наследование. Так совпало, что пару недель назад я задумался о том, как здесь могут помочь методы интерфейса по умолчанию (Я назвал ваш Compose
_
, потому что хотел, чтобы синтаксис был кратким.) Вы можете напрямую выражать подтипы, а разрешение перегрузки выбирает правильный тип возвращаемого значения для l1._(l2)
. Однако не доволен ITraversal
.
- person Benjamin Hodgson♦; 01.04.2018