这个MVC代码可以使用设计模式重构吗

本文关键字:设计模式 重构 可以使 MVC 代码 这个 | 更新日期: 2023-09-27 18:27:10

我的ASP.NET MVC 3站点上到处都有这样的控制器代码:

[HttpPost]
public ActionResult Save(PostViewModel viewModel)
{
   // VM -> Domain Mapping. Definetely belongs here. Happy with this.
   var post = Mapper.Map<PostViewModel, Post>(viewModel);
   // Saving. Again, fine. Controllers job to update model.
   _postRepository.Save(post);
   // No. Noooo..caching, thread spawning, something about a user?? Why....
   Task.Factory.StartNew(() => {
       _cache.RefreshSomeCache(post);
       _cache2.RefreshSomeOtherCache(post2);
       _userRepository.GiveUserPoints(post.User);
       _someotherRepo.AuditThisHappened();
   });
   // This should be the 3rd line in this method.
   return RedirectToAction("Index");
}

基本上,我指的是线程块中的代码。所有的事情都需要发生,但用户不需要等待(对于后台线程来说,这是一个很好的例子,对吧?)。

需要明确的是,我在整个网站上都使用缓存(常规ASP.NET数据缓存),其中大多数都有"不过期"缓存策略,所以我会在需要时手动收回它(如上所述)。

用户部分基本上是为用户做某事(比如Stack)提供代表。

因此,让我们回顾一下:我们有缓存、用户信誉处理、审计,所有这些都是一体的。不是真的属于一个地方,是吗?因此,当前代码的问题,以及试图找出如何将其移除的问题。

我想重构它的原因有几个:

  1. 难以进行单元测试。多线程和单元测试并不是很好
  2. 可读性。它很难阅读。一团糟
  3. SRP。控制器做/知道太多

我通过将生成线程的代码包装到一个接口中,并只是嘲笑/假装出来,解决了1)。

但我想做一些模式,我的代码可以是这样的:

[HttpPost]
public ActionResult Save(PostViewModel viewModel)
{
   // Map.
   var post = Mapper.Map<PostViewModel, Post>(viewModel);
   // Save.
   _postRepository.Save(post);
   // Tell someone about this.
   _eventManager.RaiseEvent(post);
   // Redirect.
   return RedirectToAction("Index");
}

基本上,把反应的责任推给"其他人",而不是控制者。

我听说/读过关于任务、命令、事件等的内容,但还没有看到在ASP.NET MVC空间中实现的内容。

最初的想法会告诉我要创建某种"活动经理"。但后来我想,这会去哪里?在域中?那么,它如何处理与缓存的交互,这是一个基础设施问题。然后是线程,这也是一个基础设施问题。如果我想做的是同步的,而不是异步的?是什么决定的?

我不想把所有这些逻辑都堆积在其他地方。理想情况下,它应该被重新纳入可管理和有意义的组成部分,而不是转移责任,如果这有意义的话。

有什么建议吗?

这个MVC代码可以使用设计模式重构吗

最初的想法会告诉我要创建某种"活动经理"。但后来我想,这会去哪里?在域中?

这是我解决问题的方法。我认为活动管理器是基础设施。但实际事件属于该领域。

那么,它如何处理与缓存的交互,这是一个基础设施问题。然后是线程,这也是一个基础设施问题。如果我想做的是同步的,而不是异步的?是什么决定的?

异步很好,但会使事务处理变得复杂。如果您使用IoC容器,那么您已经有了一个定义良好的范围和一个可以在事件传播过程中使用的事务。

imho如果订阅者知道它的事件处理需要时间,那么就由它来调度/线程它的任务。

建议的解决方案:

使用IoC容器发布事件。我会让存储库发布事件(PostUpdatedEntityUpdated,取决于您想对事件做什么),而不是控制器(以减少代码重复)。

我为autofac做了一个IoC实现,它允许你:

DomainEventDispatcher.Current.Dispatch(new EntityUpdated(post));

订阅:

public class CacheService : IAutoSubscriberOf<EntityUpdated>
{
    public void Handle(EntityUpdated domainEvent) {};
}

https://github.com/sogeti-se/Sogeti.Pattern/wiki/Domain-events

典型用法

  1. 实现IServiceResolver(用于您的容器)
  2. 分配:ServiceResolver.Assign(new yourResolver(yourContainer))
  3. 按此处所述使用

您可以在这个特定问题上使用面向方面的编程。在.NET世界中常用的产品是PostSharp。

其想法是在方法上方添加一个属性。该属性将告知应该执行哪些特定操作(在您的情况下,刷新缓存、增加点数等)以及何时执行(例如,在退出方法时)。

你也可以将它们分离成不同的属性,这样你就可以做出不同类型的组合。

也许post对象应该更新自己,并被传递一个IRepository(它本身被传递给了控制器。)

//in controller:
var post = Mapper.Map<PostViewModel, Post>(viewModel);
post.Update(_postRepository);
//inside Post.cs:
public Update(IRepository rep){
//update db with the repo    
//give points
}
MvcContrib中有一个消息总线实现,作为PortableArea功能的一部分。它的主要目的是允许独立实现的功能触发和监听事件,这听起来很像你想要的。

我不确定这是否是最好的选择,因为MvcContrib的状态有点没有记录和粗略。有些零件正在积极维护,而另一些则已过时。

另一个需要考虑的选项是ZeroMQ,但对于您的需求来说,它可能有些过头了。

如果您正在处理缓存破坏或审计(异步服务可能更好),您可能需要考虑实现服务总线系统,如NServiceBus。

使用消息总线,事件消息可以异步发布到任意数量的事件处理程序(例如缓存处理程序),因此您的应用程序可以启动消息,然后继续快速提供同步页面。

  1. 我认为您需要考虑事务问题。如果您尝试更新另一个线程中的缓存,并在repository.save之后立即返回操作结果。如果缓存操作抛出错误,则很难回滚(大多数缓存都是全局缓存,通常会面临锁定/同步问题)
  2. MVC是一种UI层模式。我通常在操作体中放入一些基于http上下文的逻辑,并将业务逻辑放入较低的层,例如,处理域模型的服务。在你的情况下,我想添加一个IPostService,并将你的保存功能和缓存功能放在其中

    [HttpPost]
    public ActionResult Save(PostViewModel viewModel)
    {
       // Map.
       var post = Mapper.Map<PostViewModel, Post>(viewModel);
       // Save.
       _postService.Save(post);
       // Redirect.
       return RedirectToAction("Index");
    }
    

    因为缓存和业务逻辑无关,所以它不需要出现在服务接口中。这是一个实现细节。

  3. 此外,我认为最好使用AOP来触发事件,并使用IoC容器来注入缓存逻辑(以及事务逻辑)。类似于:

    //codes in your PostService which implements IPostService
    [CacheEvent("POST")]
    [Transaction]
    public void Save(Post post) //care about domain model instead of view model
    {
       // Save.
       _postReposity.Save(post);
    }
    

接受了@jgaufin的答案,但由于我使用了StructureMap,我想添加一个单独的答案,它需要额外的一步

我遵循了@jgaufin指定的内容,但它并没有启动我的事件处理程序。

所以我把它添加到我的StructureMap配置中:

x.Scan(y => 
{
   y.AssembliesFromApplicationBaseDirectory();
   y.AddAllTypesOf(typeof (IAutoSubscriberOf<>));
   y.WithDefaultConventions();
});

然后它就起作用了。

我猜替代方法是手动指定每个注册的侦听器,这有点疯狂。

使用StructureMap的人能告诉我这是否是正确的方法吗?