可以使用设计模式重构此MVC代码吗?

时间:2012-03-07 07:40:56

标签: c# asp.net-mvc asp.net-mvc-3 events design-patterns

我的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。控制器做得/知道太多。
  4. 我解决了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空间中实现的内容。

    首先想到的是要创建某种“事件管理器”。但后来我想,这是怎么回事?在域名?那么它如何处理与缓存的交互,这是一个基础设施问题。然后是线程,这也是一个基础设施问题。如果我想要做的是同步,而不是异步?是什么决定的?

    我不想只是把所有这些逻辑都堆积在其他地方。理想情况下,它应该重新考虑到可管理和有意义的组件,而不是转移责任,如果这是有道理的。

    有什么建议吗?

7 个答案:

答案 0 :(得分:2)

  

首先想到的是要创建某种“事件管理器”。但后来我想,这是怎么回事?在域名?

这是我解决问题的方式。我将事件管理器视为基础架构。但实际事件属于域名。

  

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

异步很好,但是使事务处理变得复杂。如果您使用IoC容器,则您已经具有明确定义的范围和可在事件传播期间使用的事务。

如果用户知道它的事件处理需要时间,那么由用户来安排/解决它的任务。

建议的解决方案:

使用您的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. 按照here所述使用。

答案 1 :(得分:2)

也许post对象应该更新自己,并传递一个IRepository(它本身被传递给控制器​​。)(这是基本依赖注入/ IOC,并保持控制器更加苗条)

//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
}

答案 2 :(得分:2)

您可以对此特定问题使用aspect oriented programming。 .NET世界中常用的产品是PostSharp

我们的想法是在方法上方添加一个属性。该属性将告诉应该执行哪些特定操作(在您的情况下缓存刷新,指向增加等)以及何时应该发生(例如在退出方法时)。

您还可以将这些属性与不同的属性分开,这样您就可以进行不同类型的组合。

答案 3 :(得分:1)

作为PortableArea功能的一部分,MvcContrib中有一个消息总线实现。它的主要目的是允许独立实现的功能触发和监听事件,这听起来非常像你想要的。

我不确定它是否是最好的选择,因为MvcContrib的状态有点无证且粗略。有些部分是积极维护的,而有些则是过时的。

另一个需要考虑的选择是ZeroMQ,但对您的需求可能有点过分。

答案 4 :(得分:1)

如果您正在处理缓存清除或审计(可能更好地异步服务),您可能需要考虑实施服务总线系统,例如NServiceBus

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

答案 5 :(得分:0)

  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);
    }
    

答案 6 :(得分:0)

接受@ jgauffin的回答,但我想我会添加一个单独的答案,因为我使用的是StructureMap,它需要一个额外的步骤

我遵循@jgauffin指定的内容,但它没有触发我的事件处理程序。

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

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

然后它有效。

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

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

相关问题