【问题标题】:How to use generics to handle type-specific configurations in an Action<IBuilder<T>>?如何使用泛型来处理 Action<IBuilder<T>> 中的特定类型配置?
【发布时间】:2016-03-09 15:41:36
【问题描述】:

我正在尝试实现一个复杂的构建器来帮助我的测试环境。为此,我重构了我的代码以拥有一个方法:

public TestContext Add<T>(Action<IBuilder<T>> configurator) where T : class, new()
{
    IBuilder<T> builder = CreateBuilderOf<T>();
    configurator(builder);
    T item = builder.GetItem();
    RepositoryOf<T>().Insert(item);
    SetCurrent(item);
    return this;
}

当我需要在调用方法时指定配置时出现问题:

TestContext.Instance.Add<Person>(personBuilder => ((PersonBuilder)personBuilder).Name("SMITH"));

我需要能够使用配置器中的类型特定方法,这些方法由具体的构建器实现,例如:

public PersonBuilder : IBuilder<Person>
{
  private Person Item;

  public PersonBuilder() { Item = new Person(); }

  public Name(string mame) { Item.Name = name; }

  public Person GetItem() { return Item; }
}

显然,不允许将Action&lt;PersonBuilder&gt; 作为Action&lt;IBuilder&lt;Person&gt;&gt; 传递,即使PersonBuilder 实现了IBuilder&lt;Person&gt;,因此是演员表。

我非常愿意:

  • 不需要在 lambda 中进行转换,而是在它的开头,例如
    (PersonBuilder personBuilder) =&gt; personBuilder.Name("SMITH"),但这归结为Action&lt;PersonBuilder&gt; 的一个实例,因此同样无效;
  • 在 Add 的参数中使用诸如 BuildSimplePerson(PersonBuilder builder) 之类的函数:Add&lt;Person&gt;(BuildSimplePerson)

我想我可以通过两个BuildSimplePersons 实现进行类型转换,例如:

private void BuildSimplePerson(IBuilder<Person> builder)
{
  BuildSimplePerson(builder as PersonBuilder);
}
private void BuildSimplePerson(PersonBuilder builder)
{
  builder.Name("SMITH");
}

但这并不是一个真正优雅的解决方案。

我还意识到将Action&lt;PersonBuilder&gt; 作为Action&lt;IBuilder&lt;Person&gt;&gt; 传递是不正确的,因为我们不知道该函数的参数是否真的是PersonBuilderIBuilder&lt;Person&gt; 的任何其他实现。

我怎样才能做得更好?

【问题讨论】:

  • 您的问题源于一个简单的事实,即您将IBuilder&lt;Person&gt;PersonBuilder 视为相同,而实际上它们并非如此。您的问题假定 CreateBuilderOf&lt;T&gt; 返回一个 PersonBuilder 但它可以返回任何实现 IBuilder&lt;Person&gt; 在这种情况下您的演员无论如何都会失败。
  • @DanielHilgarth 是的,我意识到这一点。我重构了我的问题及其标题,以显示我想在现实中了解的内容:我怎样才能保留通用的 Add 方法并且仍然有一种特定于类型的配置的方法?
  • @Eregrith:您是在问是否可以为Person 提供一个特殊的Add 方法?
  • @DanielHilgarth 我认为不是。很抱歉,如果我不太清楚,这有点难以解释。我想只保留一个如上所述的通用 Add 方法,它将接收作为参数的 lambda(或一般的操作)来配置此类 T 项。问题是可用的配置内容和方法应该取决于T(人名,合同号...)

标签: c# templates lambda delegates


【解决方案1】:

正如我的评论已经指出的那样,问题在于您当前的代码假定 CreateBuilderOf&lt;T&gt; 返回一个 PersonBuilder 但它实际上可以返回任何实现 IBuilder&lt;Person&gt; 的东西,在这种情况下您的演员将失败。

您的代码看起来是通用的,但实际上并非如此。您总是希望在具体类 (PersonBuilder) 上工作,而不是在通用接口 IBuilder&lt;Person&gt; 上工作。

我的理解是,您需要一个通用的Add&lt;T&gt; 方法以避免在其中为每种类型重复该代码。

这是我的方法:

public TestContext Add<T>(IBuilder<T> builder) where T : class, new()
{
    T item = builder.GetItem();
    RepositoryOf<T>().Insert(item);
    SetCurrent(item);
    return this;
}

你可以这样称呼它:

TestContext.Instance.Add<Person>(CreatePersonBuilder().Name("SMITH"));

显然,您需要为每种要添加的类型提供CreateXBuilder 方法。但是,我认为您至少已经隐含地拥有了这一点,因为我认为您的 CreateBuilderOf&lt;T&gt; 方法无论如何都是一个巨大的 switch 语句。

如果您不想创建此类方法,另一种获取构建器的方法是通用方法,如下所示:

CreateBuilder<PersonBuilder>()

但实际上,这实际上只不过是一个new PersonBuilder(),所以你实际上可以简单地使用

TestContext.Instance.Add<Person>(new PersonBuilder().Name("SMITH"));

Configure 方法非常相似:

TestContext.Instance.Configure<Person>(id, p => new PersonBuilder(p).Name("SMITH"));

这将传递 ID,Configure 方法将使用它来查找对象,然后将其传递给回调。所以Configure 的第二个参数不是Action&lt;IBuilder&lt;T&gt;&gt;,而是Action&lt;T&gt;

与您现有的代码相比,这种方法还有另一个优势:
您现有的代码不仅假定PersonBuilder 将是用于IBuilder&lt;Person&gt; 的实现。不,您的代码还假定它有一个没有参数的构造函数和一个接受Person 的构造函数。编译器无法验证这些假设。
使用我上面显示的代码,构建器实现可以毫无问题地采用其他参数,并且编译器将验证一切正常。

【讨论】:

  • 我去看看,谢谢。我的 CreateBuilderOf 不是开关,它是 Dictionary 上的 Activator.CreateInstanceOf,这并没有 那么好
  • 我的上下文助手中的另一种方法出现了另一个问题:Configure&lt;T&gt;(object id, Action&lt;IBuilder&lt;T&gt;&gt; configurator) 它带回了具有给定 id 的 T 实例以进一步编辑对象。在该方法中,我需要创建一个 IBuilder 给定对象作为构造函数参数
  • 看起来不错,谢谢。如果我没有撞到其他墙,我会接受你的回答
  • 我看到了这种方法的另一个优势:我可以拥有用于通用类似配置的构建器,它将BuildSimplePerson(IBuilder&lt;Person&gt;) 替换为new SimplePersonBuilder(),这很好,而且更面向模式。
  • 当然。你可以像这样封装标准配置
猜你喜欢
  • 2010-09-15
  • 1970-01-01
  • 2012-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-15
  • 1970-01-01
相关资源
最近更新 更多