使用构建器模式与异步任务

23

我目前使用建造者模式来构建我的MVC视图模型。

var viewModel = builder
                  .WithCarousel(),
                  .WithFeaturedItems(3),
                  .Build()

我遇到的问题是在需要调用异步方法时。这意味着我的构建方法必须返回 Task<HomeViewModelBuilder> 而不是 HomeViewModelBuilder。这会阻止我链接构建方法,因为我必须使用await等待它们。

示例方法

public async Task<HomeViewModelBuilder> WithCarousel()
{   
    var carouselItems = await _service.GetAsync();
    _viewModel.Carousel = carouselItems;
    return this;
}

现在我必须使用 await 来调用构建器方法。

await builder.WithCarousel();
await builder.WithFeaturedItems(3);

有人尝试过在构建模式中使用异步方法吗?如果有,是否可能链式调用这些方法或者将await延迟到build方法中执行。


ContinueWith? - GSerg
编写一个 Task<HomeViewModelBuilder> 的扩展方法,以便您可以进行链式调用? - Sriram Sakthivel
1
你能否将异步方法调用重定位到 Build() 方法中吗?这样 With...() 只会修改构建器的状态,记住它需要调用该方法。 - Nico Schertler
很抱歉,关于这些方法内部的具体操作,信息不足。请提供一些完整的示例来展示该实现的功能,然后我会尝试解决问题。 - Sriram Sakthivel
@SriramSakthivel 请查看更新 - Colin Bacon
显示剩余2条评论
5个回答

19

我实际上以前没有做过这个,但这里有一种替代Sriram解决方案的方法。

思路是捕获构建器对象中的任务,而不是任务结果。然后Build方法等待它们完成并返回构建的对象。

public sealed class HomeViewModelBuilder
{
  // Example async
  private Task<Carousel> _carouselTask = Task.FromResult<Carousel>(null);
  public HomeViewModelBuilder WithCarousel()
  {
    _carouselTask = _service.GetAsync();
    return this;
  }

  // Example sync
  private int _featuredItems;
  public HomeViewModelBuilder WithFeaturedItems(int featuredItems)
  {
    _featuredItems = featuredItems;
    return this;
  }

  public async Task<HomeViewModel> BuildAsync()
  {
    return new HomeViewModel(await _carouselTask, _featuredItems);
  }
}

使用方法:

var viewModel = await builder
    .WithCarousel(),
    .WithFeaturedItems(3),
    .BuildAsync();

这个构建器模式适用于任何数量的异步或同步方法,例如:

public sealed class HomeViewModelBuilder
{
  private Task<Carousel> _carouselTask = Task.FromResult<Carousel>(null);
  public HomeViewModelBuilder WithCarousel()
  {
    _carouselTask = _service.GetAsync();
    return this;
  }

  private Task<int> _featuredItemsTask;
  public HomeViewModelBuilder WithFeaturedItems(int featuredItems)
  {
    _featuredItemsTask = _featuredService.GetAsync(featuredItems);
    return this;
  }

  public async Task<HomeViewModel> BuildAsync()
  {
    return new HomeViewModel(await _carouselTask, await _featuredItemsTask);
  }
}

使用方法仍然相同。


谢谢您提供的解决方案。唯一的缺点是,如果有两种具有异步调用的构建器方法,则构建器将必须具有某种检查机制。 - Colin Bacon
2
@ColinBacon:我不确定你的意思;使用相同的模式,您可以拥有任意数量的异步构建器调用。 - Stephen Cleary
抱歉,我的表述可能不太清楚。如果 WithCarouselWithFeaturedItems 都有异步调用,那么在 BuildAsync 中该如何处理呢?因为我可能不会同时使用这两种方法。 - Colin Bacon
1
@ColinBacon:更新了一个示例,其中两个调用都使用了任务。 - Stephen Cleary
建造者模式中的多个"With*"方法不会并行运行吗?如果代码不是线程安全的话,这可能会导致问题 - 例如,注入的 EF DataContext 是同一个实例。 - Dan
@Dan:是的,它们将同时运行。 - Stephen Cleary

6

就像我在评论中所说的,你可以为 HomeViewModelBuilder 编写扩展方法,以及 Task<HomeViewModelBuilder> 并将其链接。

public static class HomeViewModelBuilderExtension
{
    public static Task<HomeViewModelBuilder> WithCarousel(this HomeViewModelBuilder antecedent)
    {
        return WithCarousel(Task.FromResult(antecedent));
    }

    public static async Task<HomeViewModelBuilder> WithCarousel(this Task<HomeViewModelBuilder> antecedent)
    {
        var builder = await antecedent;
        var carouselItems = await builder.Service.GetAsync();
        builder.ViewModel.Carousel = carouselItems;
        return builder;
    }

    public static Task<HomeViewModelBuilder> WithFeaturedItems(this HomeViewModelBuilder antecedent, int number)
    {
        return WithFeaturedItems(Task.FromResult(antecedent), number);
    }

    public static async Task<HomeViewModelBuilder> WithFeaturedItems(this Task<HomeViewModelBuilder> antecedent, int number)
    {
        var builder = await antecedent;
        builder.ViewModel.FeaturedItems = number;
        return builder;
    }
}

我们将添加一些方法以实现单个操作,以便您可以将其与HomeViewModelBuilderTask<HomeViewModelBuilder>链接在一起。否则,您将无法调用builder.WithCarousel()
然后像这样使用:
private static void Main()
{
    HomeViewModelBuilder builder = new HomeViewModelBuilder();
    var task = builder
        .WithCarousel()
        .WithFeaturedItems(3);        
}

3

使用构建器模式,您可以创建一种构建对象的策略。它在调用build方法之前不会构造对象。如果填充对象的逻辑在build方法中,则可以一起调用所有异步方法。

请参见下面的构建器示例代码。这只是概念的演示,因此您可能希望对其进行改进。

    public class Builder
    {
        private bool hasCarousel = false;
        private int featuredItems = 0;

        public Builder WithCarousel()
        {
            hasCarousel = true;
            return this;
        }

        public Builder WithFeaturedItems(int featured)
        {
            featuredItems = featured;
            return this;
        }

        public BuiltObject Build()
        {
            if (hasCarousel)
            {
                // do carousel related calls
            }
            if (featuredItems > 0)
            {
                // do featured items related calls.
            }

            // build and return the actual object.
        }
    }

谢谢,问题在于根据我选择的构建方法,我可能不需要异步方法。 - Colin Bacon
请参见上面的编辑中的示例代码。然后您可以在build方法中首先调用异步方法,然后再执行其他需要完成的任务。 - CarlHembrough
3
这段代码让我很费解。Builder 构造函数是否本应该成为一个异步的 Build 方法? - svick
@ColinBacon 这有什么问题吗?你可以让 Build() 返回一个 Task,即使你不需要执行任何异步操作,这个 Task 也会被立即完成(你可以轻松地使用 async-await 实现这一点)。 - svick
谢谢svick,你在代码示例中发现了一个错误。build方法被声明为类的构造函数,而实际上应该是类的build方法。我已经更新了它。 - CarlHembrough

2
关于async的问题是它具有连锁效应。它倾向于在您的代码中传播,以保持其async "始终异步"
如果您想要使用构建器模式(或任何其他流畅模式,如LINQ),同时保持其async,则需要为您想要进行的每个可能调用都有一个async重载。否则,您将无法使用它们,或者会错误地使用它们(例如使用"同步覆盖异步")。 async-await相对较新,但我相信随着时间的推移,您几乎可以为任何事情提供异步选项。

1
谢谢。您能详细说明“您需要为每个可能的调用都拥有一个异步重载”吗? - Colin Bacon
使用选择答案,我认为这个答案没有关联性。 "Async all the way"仅意味着应该从请求到响应都是异步的,甚至这也有争议。根据我的经验,这实际上意味着要选择你的方法并坚持下去。从a点到z点,尽量不要混合同步和异步方法。这绝不意味着您构建器上的所有构建方法都必须是异步的,那是荒谬的。正如所选答案所示,你所做的就是使用同步方法启动任务,然后构建程序会解开结果,这仍然符合异步精神。 - Sinaesthetic

1
已接受的答案非常有帮助,但我认为在某些情况下(至少在我的情况下),等待构建器本身比等待构建对象更有帮助:
public sealed class HomeViewModelBuilder
{
  
 private List<Task<HomeViewModelBuilder>> taskList= new List<Task<HomeViewModelBuilder>>();

 public HomeViewModelBuilder WithCarousel(){
   taskList.add(WithCarouselInternal());
   return this;
 }

 private async Task<HomeViewModelBuilder> WithCarouselInternal()
  {
    var result = await _service.GetAsync();
    // do something with the result
    return this;
  }

 public HomeViewModelBuilder WithSomthingElse(){
   taskList.add(WithSomethingElseInternal());
   return this;
 }

 (...)


  public async Task<HomeViewModel> BuildAsync()
  {
   await Task.WhenAll(taskList);
   // On that point we can now be sure that all builds are finished
   return new HomeViewModel(...);
  }
}

我希望这个更通用,因为你只需要保持任务列表最新,并且可以简单地调用相同的构建方法多次(你不能覆盖对构建方法的先前调用,因为所有任务都将出现在列表中,因此构建肯定会等待任务完成)。

如果你需要从上一步骤中的结果用于下一步骤,那么这种方法是行不通的。 - Rafael Herscovici

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接