首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >不可变的建造者和更新者

不可变的建造者和更新者
EN

Code Review用户
提问于 2019-08-23 15:30:18
回答 2查看 325关注 0票数 9

关于创建不可变的对象没有足够的问题..。那么为什么不用另一种方法再试一次呢?这一次,它是一个构建器,它将属性映射到构造函数参数。属性是通过With方法选择的,该方法还需要参数的新值。构造函数参数必须与属性匹配,因此主要用于遵循此模式的DTO。Build试图找到该构造函数并创建对象。

代码语言:javascript
复制
// I need this primarily for the left-join of optional parameters.
internal class IgnoreCase : IEquatable<string>
{
    public string Value { get; set; }
    public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
    public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
    public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
    public static explicit operator IgnoreCase(string value) => new IgnoreCase { Value = value };
}

public class ImmutableBuilder<T>
{
    private readonly IList<(MemberExpression Selector, object Value)> _selectors = new List<(MemberExpression Selector, object Value)>();

    public ImmutableBuilder<T> With<TProperty>(Expression<Func<T, TProperty>> selector, TProperty value)
    {
        _selectors.Add(((MemberExpression)selector.Body, value));
        return this;
    }

    public T Build()
    {
        var ctors =
            from ctor in typeof(T).GetConstructors()
            let parameters = ctor.GetParameters()
            // Join parameters and values by parameter order. 
            // The ctor requires them sorted but they might be initialized in any order.
            let requiredParameterValues =
                from parameter in parameters.Where(p => !p.IsOptional)
                join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name
                select selector.Value
            // Get optional parameters if any.
            let optionalParameterValues =
                from parameter in parameters.Where(p => p.IsOptional)
                join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name into s
                from selector in s.DefaultIfEmpty()
                select selector.Value
            // Make sure all required parameters are specified.
            where requiredParameterValues.Count() == parameters.Where(p => !p.IsOptional).Count()
            select (ctor, requiredParameterValues, optionalParameterValues);

        var theOne = ctors.Single();
        return (T)theOne.ctor.Invoke(theOne.requiredParameterValues.Concat(theOne.optionalParameterValues).ToArray());
    }
}

public static class Immutable<T>
{
    public static ImmutableBuilder<T> Builder => new ImmutableBuilder<T>();
}

如果我们已经有了一个不可变的类型,并且只想通过更改一些值来修改它,那么可以使用ImmutableHelper。它还提供了期待一个新值的With方法。然后将它和其他属性与构造函数匹配,并使用origianl值作为默认值,对于指定的属性使用新值。

代码语言:javascript
复制
public static class ImmutableHelper
{
    public static T With<T, TProperty>(this T obj, Expression<Func<T, TProperty>> selector, TProperty value)
    {
        var comparer = StringComparer.OrdinalIgnoreCase;

        var propertyName = ((MemberExpression)selector.Body).Member.Name;
        var properties = typeof(T).GetProperties();

        var propertyNames =
            properties
                .Select(x => x.Name)
                // A hash-set is convenient for name matching in the next step.
                .ToImmutableHashSet(comparer);

        // Find the constructor that matches property names.
        var ctors =
            from ctor in typeof(T).GetConstructors()
            where propertyNames.IsProperSupersetOf(ctor.GetParameters().Select(x => x.Name))
            select ctor;

        var theOne = ctors.Single(); // There can be only one match.        

        var parameters =
            from parameter in theOne.GetParameters()
            join property in properties on (IgnoreCase)parameter.Name equals (IgnoreCase)property.Name
            // They definitely match so no comparer is necessary.
            // Use either the new value or the current one.
            select property.Name == propertyName ? value : property.GetValue(obj);

        return (T)theOne.Invoke(parameters.ToArray());
    }
}

示例

这就是如何使用它:

代码语言:javascript
复制
void Main()
{
    var person = 
        Immutable<Person>
            .Builder
            .With(x => x.FirstName, "Jane")
            .With(x => x.LastName, null)
            //.With(x => x.NickName, "JD") // Optional
            .Build();

    person.With(x => x.LastName, "Doe").Dump();
}

public class Person
{
    public Person(string firstName, string lastName, string nickName = null)
    {
        FirstName = firstName;
        LastName = lastName;
        NickName = nickName;
    }

    // This ctor should confuse the API.
    public Person(string other) { }

    public string FirstName { get; }

    public string LastName { get; }

    public string NickName { get; }

    // This property should confuse the API too.
    public string FullName => $"{LastName}, {FirstName}";
}

你说呢?疯了吗?精神错乱?我很喜欢?让我们改进一下?

EN

回答 2

Code Review用户

回答已采纳

发布于 2019-08-23 17:36:26

在应该使用IList<>的地方使用ICollection<>。我很少遇到实际需要使用IList<>的场景。ICollection<>接口拥有列表的大多数方法,但是没有任何与索引相关的方法,而且您也不会使用这些方法。这没什么大不了的,但我认为这是很好的知识。

在搜索构造函数参数时,我认为除了参数名称之外,还应该匹配参数类型。这背后的原因是,这种带有名称的参数类型保证是唯一的,而参数名不能。例如(这不是一个很好的例子,但它可能会使您的代码崩溃)

代码语言:javascript
复制
class Foobar
{

    public Foobar(string a, int b)
    {
    }

    public Foobar(string a, double b)
    {
    }

}

我在Immutable<T>类中看到的一个问题是,每次调用它时,我都不会期望static属性返回一个新的引用。我一直试图在.NET框架中找到另一个例子,说明何时会发生这种情况,而且.我做不到,我会将它修改为一个名为CreateBuilder()之类的方法,这样,很明显,每次调用该方法时,我们都会使用一个新的构建器。

我认为Immutable<>类型是有误导性的。当看到这一点时,我希望能够使用它使某些可变类型不可变(不管这样做是怎么做的),但这不是它所做的。事实上,大多数代码都不依赖于T类型是不可变的,这使我认为一两次调整也可以使您的工具在可变类型上工作。从这个意义上说,声称它是一个ImmutableBuilder是错误的,它是一个构建器,可以处理不可变的类型,但也可以处理可变的类型。

根据注释,With方法在ImmutableHelper中创建一个带有更改参数的对象的副本,考虑到它不会修改不可变类型,这是正常的。我认为可以改进的是使用签名static T With<T, TProperty>(this T obj, IEnmerable<(Expression<Func<T, TProperty>> selector, TProperty value)>)的类似方法,这样如果您想修改对象中的多个字段,您就可以这样做,而不必每次都创建对象的副本。

票数 7
EN

Code Review用户

发布于 2019-08-23 20:31:40

(自我回答)

不使用构造函数的想法确实很疯狂,但是.因为有可能我也把它保留在新版本里了。我将该工具的名称更改为DtoUpdater。现在它可以收集多个属性的更新,这些属性最终必须是Commit编辑的。现在,参数不仅通过名称而且按类型与成员进行匹配,并且它选择参数最多的构造函数和匹配属性。我创建了这个助手来更新简单的DTO,它们通常只有一个构造函数来初始化所有属性,所以我认为它当前的复杂性对于大多数用例来说都足够了。

此版本也不再强制用户指定构造函数所需的所有值。我认为这个新功能使这个实用程序在某些情况下比使用构造器更有用.当然,如果允许default值的话。

代码语言:javascript
复制
public static class DtoUpdater
{
    public static DtoUpdater<T> For<T>() => new DtoUpdater<T>(default);

    public static DtoUpdater<T> Update<T>(this T obj) => new DtoUpdater<T>(obj);
}

public class DtoUpdater<T>
{
    private readonly T _obj;

    private readonly ICollection<(MemberInfo Member, object Value)> _updates = new List<(MemberInfo Member, object Value)>();

    public DtoUpdater(T obj) => _obj = obj;

    public DtoUpdater<T> With<TProperty>(Expression<Func<T, TProperty>> update, TProperty value)
    {
        _updates.Add((((MemberExpression)update.Body).Member, value));
        return this;
    }

    public T Commit()
    {
        var members =
            from member in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(m => m is PropertyInfo || m is FieldInfo)
            select (member.Name, Type: (member as PropertyInfo)?.PropertyType ?? (member as FieldInfo)?.FieldType);

        members = members.ToList();

        // Find the ctor that matches most properties.
        var ctors =
            from ctor in typeof(T).GetConstructors()
            let parameters = ctor.GetParameters()
            from parameter in parameters
            join member in members
                on
                new
                {
                    Name = parameter.Name.AsIgnoreCase(),
                    Type = parameter.ParameterType
                }
                equals
                new
                {
                    Name = member.Name.AsIgnoreCase(),
                    Type = member.Type
                }
            orderby parameters.Length descending
            select ctor;

        var theOne = ctors.First();

        // Join parameters and values by parameter order.
        // The ctor requires them sorted but they might be initialized in any order.
        var parameterValues =
            from parameter in theOne.GetParameters()
            join update in _updates on parameter.Name.AsIgnoreCase() equals update.Member.Name.AsIgnoreCase() into x
            from update in x.DefaultIfEmpty()
            select update.Value ?? GetMemberValueOrDefault(parameter.Name);

        return (T)theOne.Invoke(parameterValues.ToArray());
    }

    private object GetMemberValueOrDefault(string memberName)
    {
        if (_obj == null) return default;

        // There is for sure only one member with that name.
        switch (typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Single(m => m.Name.AsIgnoreCase().Equals(memberName)))
        {
            case PropertyInfo p: return p.GetValue(_obj);
            case FieldInfo f: return f.GetValue(_obj);
            default: return default; // Makes the compiler very happy.
        }
    }
}

public static class StringExtensions
{
    public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;

    private class IgnoreCase : IEquatable<string>
    {
        private IgnoreCase(string value) => Value = value;
        private string Value { get; }
        public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
        public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
        public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
        public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
    }
}

我将IgnoreCase助手隐藏在一个新的扩展后面:

代码语言:javascript
复制
public static class StringExtensions
{
    public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;

    private class IgnoreCase : IEquatable<string>
    {
        private IgnoreCase(string value) => Value = value;
        private string Value { get; }
        public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
        public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
        public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
        public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
    }
}

现在可以这样使用新的API:

代码语言:javascript
复制
public class DtoBuilderTest
{
    [Fact]
    public void Can_create_and_update_object()
    {
        var person =
            DtoUpdater
                .For<Person>()
                .With(x => x.FirstName, "Jane")
                .With(x => x.LastName, null)
                //.With(x => x.NickName, "JD") // Optional
                .Commit();

        Assert.Equal("Jane", person.FirstName);
        Assert.Null(person.LastName);
        Assert.Null(person.NickName);

        person =
            person
                .Update()
                .With(x => x.LastName, "Doe")
                .With(x => x.NickName, "JD")
                .Commit();

        Assert.Equal("Jane", person.FirstName);
        Assert.Equal("Doe", person.LastName);
        Assert.Equal("JD", person.NickName);
    }

    private class Person
    {
        public Person(string firstName, string lastName, string nickName = null)
        {
            FirstName = firstName;
            LastName = lastName;
            NickName = nickName;
        }

        // This ctor should confuse the API.
        public Person(string other) { }

        public string FirstName { get; }

        public string LastName { get; }

        public string NickName { get; set; }

        // This property should confuse the API too.
        public string FullName => $"{LastName}, {FirstName}";
    }
}
票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/226694

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档