FluentBuilder для immutable класса
В общем по следам этого вопроса решил написать примерчик, но к сожалению не осилил ?. Может кто из вас поможет дописать ?.
Имеем такой класс человека
class Person
{
public Person(string firstName, string lastName,
Address address, Employment employment)
{
FirstName = firstName;
LastName = lastName;
Address = address;
Employment = employment;
}
public string FirstName { get; }
public string LastName { get; }
public Address Address { get; }
public Employment Employment { get; }
public override string ToString()
{
return $"{FirstName} {LastName}: "
+ Environment.NewLine +
$"{Address}"
+ Environment.NewLine +
$"{Employment}";
}
}
с сопутствующими классами
class Address
{
public string City { get; set; }
public int PostCode { get; set; }
public override string ToString()
{
return $"Адрес: {City}-{PostCode} ";
}
}
class Employment
{
public string CompanyName { get; set; }
public string Position { get; set; }
public int AnnualIncome { get; set; }
public override string ToString()
{
return $"Работа: {CompanyName}, должность: {Position}, зарплата: {AnnualIncome}";
}
}
Самостоятельно удалось написать только такой строитель
class PersonBuilder
{
//аккумулятор названий свойств и их значений
private readonly Dictionary<string, object> _propertiesToBuild;
public PersonBuilder()
{
_propertiesToBuild = new Dictionary<string, object>();
}
//запоминание в словаре названия свойства и его значения
public PersonBuilder Set<T>(Expression<Func<Person, T>> expression, T value)
{
var propertyName = ((MemberExpression)expression.Body).Member.Name;
_propertiesToBuild.Add(propertyName, value);
return this;
}
public T Include<T>(Expression<Func<Person, T>> expression)
{
var propertyName = ((MemberExpression)expression.Body).Member.Name;
if (propertyName == nameof(Person.Address))
{
var result = new Address();
return (T)(object)result;
}
else
{
var result = new Employment();
return (T)(object)result;
}
}
//создание экземпляра на основе значений свойств из словаря
public Person Build()
{
return new Person
(
firstName: GetPropertyValue<string>(nameof(Person.FirstName), "Неизвестно"),
lastName: GetPropertyValue<string>(nameof(Person.LastName), "Неизвестно"),
address: new Address(),
employment: new Employment()
);
}
private T GetPropertyValue<T>(string propertyName, T defaultValue)
{
return _propertiesToBuild.TryGetValue(propertyName, out var value) ? (T)value : defaultValue;
}
}
И пока это работает так
var person = new PersonBuilder()
.Set(p => p.FirstName, "Андрей")
.Set(p => p.LastName, "Иванов")
.Build();
или так
Address address = new PersonBuilder()
.Set(p => p.FirstName, "Андрей")
.Set(p => p.LastName, "Иванов")
.Include(p => p.Address);
ну или так
Employment employment = new PersonBuilder()
.Set(p => p.FirstName, "Андрей")
.Set(p => p.LastName, "Иванов")
.Include(p => p.Employment);
А хотелось бы в результате как-то так
Person person = new PersonBuilder()
.Set(p => p.FirstName, "Андрей")
.Set(p => p.LastName, "Иванов")
.Include(p => p.Address)
.Set(a => a.City, "Москва")
.Include(p => p)
.Include(p => p.Employment)
.Set(e => e.Position, "инженер")
.Include(p => p)
.Build();
P.S. получившейся в итоге вариант здесь, спасибо @Vasek
Ответы (4 шт):
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
public Employment Employment { get; set; }
}
class Address
{
public string City { get; set; }
public int PostCode { get; set; }
}
class Employment
{
public string CompanyName { get; set; }
public string Position { get; set; }
public int AnnualIncome { get; set; }
}
class Builder<T>
{
public Builder Root { get; }
public Dictionary<string, object> Props { get; }
public Builder(Builder root)
{
Root = root;
Props = new Dictionary<string, object>();
}
public Builder<T> Set<TProp>(Expression<Func<T, TProp>> expression, TProp value)
{
var propertyName = ((MemberExpression)expression.Body).Member.Name;
Props.Add(propertyName, value);
return this;
}
public virtual Builder<TProp> Include<TProp>(Expression<Func<Person, TProp>> expression)
{
return Root.Include(expression);
}
public TProp GetPropertyValue<TProp>(string propertyName, TProp defaultValue)
{
return Props.TryGetValue(propertyName, out var value) ? (TProp)value : defaultValue;
}
public virtual Person Build()
{
return Root.Build();
}
}
class Builder : Builder<Person>
{
private Builder<Address> _addressBuilder;
private Builder<Employment> _employmentBuilder;
public Builder()
: base(null)
{
_addressBuilder = new Builder<Address>(this);
_employmentBuilder = new Builder<Employment>(this);
}
public override Builder<TProp> Include<TProp>(Expression<Func<Person, TProp>> expression)
{
if (expression.Body is MemberExpression memberExpression) {
var propName = memberExpression.Member.Name;
return propName switch {
nameof(Person.Address) => _addressBuilder as Builder<TProp>,
nameof(Person.Employment) => _employmentBuilder as Builder<TProp>,
_ => throw new Exception("lolo")
};
}
if (expression.Body is ParameterExpression parameterExpression && parameterExpression.Type == typeof(Person)) {
return this as Builder<TProp>;
}
throw new Exception("lolo");
}
public override Person Build()
{
return new Person() {
FirstName = GetPropertyValue<string>(nameof(Person.FirstName), "!FirstName!"),
LastName = GetPropertyValue<string>(nameof(Person.LastName), "!LastName!"),
Address = new Address() {
City = _addressBuilder.GetPropertyValue<string>(nameof(Address.City), "!City!"),
PostCode = _addressBuilder.GetPropertyValue<int>(nameof(Address.PostCode), -1),
},
Employment = new Employment() {
CompanyName = _employmentBuilder.GetPropertyValue<string>(nameof(Employment.CompanyName), "!CompanyName!"),
Position = _employmentBuilder.GetPropertyValue<string>(nameof(Employment.Position), "!Position!"),
AnnualIncome = _employmentBuilder.GetPropertyValue<int>(nameof(Employment.AnnualIncome), -1),
}
};
}
}
static void Main(string[] args)
{
var builder = new Builder()
.Set(p => p.FirstName, "1")
.Include(p => p.Address)
.Set(a => a.City, "3")
.Set(a => a.PostCode, 123)
.Include(p => p.Employment)
.Set(e => e.AnnualIncome, 5)
.Set(e => e.Position, "6")
.Include(p => p)
.Set(p => p.LastName, "2")
;
var person = builder.Build();
}
Ps: Придумал такое. Исходил из того что надо как можно проще. Я в выраженях не очень силен поэтому может здесь что не совсем правильно, но идея думаю понятна будет
Как насчет такого решения?
UPD: Нашел баг. Если один и тот же тип будет встречаться в дереве несколько раз, оно не будет работать. Например если у Person будет Address WorkAddress и Address HomeAddress. Подумаю, как это можно разрулить.
Сюда стоит допилить защитного программирования, конструктор например надо лишний спрятать, и дергать его рефлексией. Может быть еще что не учел. Но вроде-как работает.
Немного изменил данные
class Person
{
public string FirstName { get; }
public string LastName { get; }
public Address Address { get; }
public Employment Employment { get; }
public override string ToString()
{
return $"{FirstName} {LastName}: "
+ Environment.NewLine +
$"{Address}"
+ Environment.NewLine +
$"{Employment}";
}
}
class Address
{
public string City { get; }
public int PostCode { get; }
public override string ToString()
{
return $"Адрес: {City}-{PostCode} ";
}
}
class Employment
{
public string CompanyName { get; }
public string Position { get; }
public int AnnualIncome { get; }
public override string ToString()
{
return $"Работа: {CompanyName}, должность: {Position}, зарплата: {AnnualIncome}";
}
}
И вот что получилось
class FluentBuilder<T>
{
private readonly Dictionary<Type, Dictionary<string, object>> _container;
private Dictionary<string, object> _properties
{
get
{
if (!_container.TryGetValue(typeof(T), out var properties))
{
properties = new Dictionary<string, object>();
_container[typeof(T)] = properties;
}
return properties;
}
}
public FluentBuilder()
=> _container = new Dictionary<Type, Dictionary<string, object>>();
public FluentBuilder(Dictionary<Type, Dictionary<string, object>> props)
=> _container = props;
public FluentBuilder<Tprop> As<Tprop>() where Tprop : class
{
return new FluentBuilder<Tprop>(_container);
}
public FluentBuilder<T> Set<Tprop>(Expression<Func<T, Tprop>> expression, Tprop value)
{
var propertyName = ((MemberExpression)expression.Body).Member.Name;
_properties.Add(propertyName, value);
return this;
}
public FluentBuilder<Tprop> Include<Tprop>(Expression<Func<T, Tprop>> expression) where Tprop : class
{
var propertyName = ((MemberExpression)expression.Body).Member.Name;
_properties.Add(propertyName, _properties);
return As<Tprop>();
}
public T Build()
{
// для отладки, можно убрать
foreach (Type t in _container.Keys)
foreach (string s in _container[t].Keys)
Console.WriteLine($"[{t.Name}] {s}");
return (T)GetInstance(typeof(T));
}
private FieldInfo GetBackingField(Type type, string propertyName)
=> type.GetField($"<{propertyName}>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new NullReferenceException($"Auto properties only allowed. Property: {type.Name}.{propertyName}");
private object GetInstance(Type type)
{
object result = Activator.CreateInstance(type);
Dictionary<string, object> props = _container[type];
foreach (PropertyInfo p in type.GetProperties())
if (props.TryGetValue(p.Name, out object v))
{
GetBackingField(result.GetType(), p.Name).SetValue(result, v == props ? GetInstance(p.PropertyType) : v);
}
return result;
}
}
Использование
Person person = new FluentBuilder<Person>()
.Set(p => p.FirstName, "Андрей")
.Set(p => p.LastName, "Иванов")
.Include(p => p.Address)
.Set(a => a.City, "Москва")
.As<Person>()
.Include(p => p.Employment)
.Set(e => e.Position, "инженер")
.As<Person>()
.Build();
Console.WriteLine(person);
Вывод в консоль
Андрей Иванов:
Адрес: Москва-0
Работа: , должность: инженер, зарплата: 0
А хотелось бы в результате как-то так...
Я бы покритиковал сам постановку задачи.
В чем смысл PersonBuilder? Это какая-то искусственная сущность, по сути мутабельный двойник иммутабельного класса, который нужно дублировать для каждого такого класса. Не лучше ли строить объекты унифицированным способом?
.Set(p => p.FirstName, "Андрей")- использование лямбда-выражения ни с того, ни с сего, хотя анонимная функция тут вообще не вызывается. Почему не.Set("FirstName", "Андрей")?.Include(p => p)- вообще не понятно, что такое. Как включить самого себя? По логике, происходит переход к родительскому объекту, так что лучше бы создать отдельный метод "ToParent"
Я бы сделал это так. Допустим, мы принимаем соглашение, что у иммутабельного класса имена get-only свойств начинаются с большой буквы. Параметры конструктора соответствуют по типу его свойствам, а имя параметра начинается путем замены первой буквы имени свойства на такую же маленькую букву. Создадим такой класс:
public class ImmutableEntity
{
static Dictionary<Type, Delegate[]> gettersCache = new Dictionary<Type, Delegate[]>();
static Dictionary<Type, Delegate> constrCache = new Dictionary<Type, Delegate>();
ImmutableEntity parent;//родительский объект, если этот объект - значение свойства другого объекта
string parentProperty;//свойство родительского объекта
Delegate[] getters;
Delegate constr;
protected ImmutableEntity()
{
if (gettersCache.ContainsKey(this.GetType()))
{
constr = constrCache[this.GetType()];
getters = gettersCache[this.GetType()];
}
else
{
PropertyInfo p;
//получаем первый конструктор, имеющий параметры
ConstructorInfo ci = this.GetType().GetConstructors().
Where(x => x.GetParameters().Length > 0).First();
ParameterInfo[] p_arr;
p_arr = ci.GetParameters();
//получаем массив делегатов для получения значений свойств
ParameterExpression[] expressions = new ParameterExpression[p_arr.Length];
this.getters = new Delegate[p_arr.Length];
for (int i = 0; i < p_arr.Length; i++)
{
//получаем имя свойства
string propname = p_arr[i].Name;
propname = Char.ToUpper(propname[0]).ToString() + propname.Substring(1);
//создаем делегат
p = this.GetType().GetProperty(
propname, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance
);
MethodInfo mb = p.GetGetMethod();
Type delegateType = typeof(Func<,>).MakeGenericType(this.GetType(), p.PropertyType);
getters[i] = mb.CreateDelegate(delegateType);
//создаем выражение для параметра
expressions[i] = Expression.Parameter(p.PropertyType);
}
//создаем делегат для вызова конструктора
LambdaExpression expr = Expression.Lambda(Expression.New(ci, expressions), expressions);
constr = expr.Compile();
//сохраняем делегаты, чтобы не пересоздавать для каждого объекта одного типа
gettersCache[this.GetType()] = getters;
constrCache[this.GetType()] = constr;
}
}
public ImmutableEntity Set(string prop, object val)
{
//получаем имя параметра из имени свойства
string pname = prop;
pname = Char.ToLower(pname[0]).ToString() + pname.Substring(1);
//получаем первый конструктор, имеющий параметры
ConstructorInfo ci = this.GetType().GetConstructors().
Where(x => x.GetParameters().Length > 0).First();
//формируем массив значений параметров
ParameterInfo[] p_arr;
p_arr = ci.GetParameters();
object[] pars = new object[p_arr.Length];
for (int i = 0; i < p_arr.Length; i++)
{
if (p_arr[i].Name == pname)
{
//свойство, которое изменилось
pars[i] = val;
}
else
{
pars[i] = this.getters[i].DynamicInvoke(this);
}
}
//создаем измененный объект
ImmutableEntity ret=(ImmutableEntity)constr.DynamicInvoke(pars);
ret.parent = this.parent;
ret.parentProperty = this.parentProperty;
return ret;
}
public static object GetDefault(Type t)
{
if (t.IsValueType) return Activator.CreateInstance(t);
else return null;
}
public ImmutableEntity Include(string prop)
{
//получаем имя параметра из имени свойства
string pname = prop;
pname = Char.ToLower(pname[0]).ToString() + pname.Substring(1);
//получаем первый конструктор, имеющий параметры
ConstructorInfo ci = this.GetType().GetConstructors().
Where(x => x.GetParameters().Length > 0).First();
//формируем массив значений параметров
ParameterInfo[] p_arr;
p_arr = ci.GetParameters();
object[] pars = new object[p_arr.Length];
ImmutableEntity included =null;
for (int i = 0; i < p_arr.Length; i++)
{
if (p_arr[i].Name == pname)
{
//свойство для включения
included = this.getters[i].DynamicInvoke(this) as ImmutableEntity;
if (included == null)
{
included = New(p_arr[i].ParameterType);
included.parentProperty = prop;
}
pars[i] = included;
}
else
{
pars[i] = this.getters[i].DynamicInvoke(this);
}
}
//включаем объект в родительский
included.parent = (ImmutableEntity)constr.DynamicInvoke(pars);
return included;
}
public ImmutableEntity ToParent()
{
if (this.parent == null) return null;
return this.parent.Set(this.parentProperty, this);
}
public static T New<T>() where T:ImmutableEntity
{
return (T)New(typeof(T));
}
public static ImmutableEntity New(Type t)
{
//создаем пустой объект указанного типа
ConstructorInfo ci = t.GetConstructors().
Where(x => x.GetParameters().Length > 0).First();
ParameterInfo[] p_arr = ci.GetParameters();
object[] pars = new object[p_arr.Length];
for (int i = 0; i < p_arr.Length; i++)
{
pars[i] = GetDefault(p_arr[i].ParameterType);
}
return (ImmutableEntity)ci.Invoke(pars);
}
}
Его суть в использовании выражений для вызова конструктора объекта и получения значений свойств. Так можно строить любой объект, который удовлетворяет нашим соглашениям.
Сделаем иммутабельные классы производными от него:
public class Person: ImmutableEntity
{
public Person(string firstName, string lastName,
Address address, Employment employment)
{
FirstName = firstName;
LastName = lastName;
Address = address;
Employment = employment;
}
public string FirstName { get; }
public string LastName { get; }
public Address Address { get; }
public Employment Employment { get; }
public override string ToString()
{
return $"{FirstName} {LastName}: "
+ Environment.NewLine +
$"{Address}"
+ Environment.NewLine +
$"{Employment}";
}
}
public class Address : ImmutableEntity
{
public Address(string city, int postCode)
{
this.City = city;
this.PostCode = postCode;
}
public string City { get; }
public int PostCode { get; }
public override string ToString()
{
return $"Адрес: {City}-{PostCode} ";
}
}
public class Employment : ImmutableEntity
{
public Employment(string companyName, string position,int annualIncome)
{
this.CompanyName = companyName;
this.Position = position;
this.AnnualIncome = annualIncome;
}
public string CompanyName { get; }
public string Position { get; }
public int AnnualIncome { get; }
public override string ToString()
{
return $"Работа: {CompanyName}, должность: {Position}, зарплата: {AnnualIncome}";
}
}
Тогда использовать можно так:
var person = (Person)ImmutableEntity.New<Person>().
Set(nameof(Person.FirstName), "Ivan").
Set(nameof(Person.LastName), "Ivanov").
Include(nameof(Person.Address)).
Set(nameof(Address.City), "Chelyabinsk").
ToParent().
Include(nameof(Person.Employment)).
Set(nameof(Employment.Position), "Engineer").
ToParent();
В качестве пропаганды всего нового и хорошего напомню, что immutable + builder из коробки поддерживается 9-ой версией C#.
Добавляем в .csproj <LangVersion>9.0</LangVersion>, и ваш пример можно магически записать следующим образом:
record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Address Address { get; init; }
public Employment Employment { get; init; }
public override string ToString() =>
$"{FirstName} {LastName}: \n{Address}\n{Employment}";
}
record Address
{
public string City { get; init; }
public int PostCode { get; init; }
public override string ToString() =>
$"Адрес: {City}-{PostCode}";
}
record Employment
{
public string CompanyName { get; init; }
public string Position { get; init; }
public int AnnualIncome { get; init; }
public override string ToString() =>
$"Работа: {CompanyName}, должность: {Position}, зарплата: {AnnualIncome}";
}
А код, использующий всё это, запишется так:
var person =
new Person()
with { FirstName = "Андрей" }
with { LastName = "Иванов" }
with { Address = new Address()
with { City = "Мюнхен" }
with { PostCode = 80000 } }
with { Employment = new Employment()
with { CompanyName = "Google" }
with { Position = "CEO" }
with { AnnualIncome = 100500 } };
Всё!