【问题标题】:Can this MVC code be refactored using a design pattern?可以使用设计模式重构此 MVC 代码吗?
【发布时间】:2012-03-07 07:40:55
【问题描述】:

我的 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 数据缓存),其中大部分都有“永不过期”缓存策略,因此我在需要时手动将其逐出(如上)。

而用户部分基本上是给用户代表做某事(如堆栈)。

让我们回顾一下:我们将缓存、用户信誉处理、审计一应俱全。确实不属于一个地方。因此存在当前代码的问题,以及试图弄清楚如何将其移走的问题。

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

  1. 难以进行单元测试。多线程和单元测试并不能很好地发挥作用。
  2. 可读性。很难阅读。凌乱。
  3. 建议零售价。控制器做得/知道的太多。

我解决了 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 空间中实现的。

最初的想法会告诉我创建某种“事件管理器”。但后来我想,这去哪里了?在域中?那么它如何处理与缓存的交互,这是一个基础设施问题。然后是线程,这也是一个基础架构问题。如果我想做的是同步而不是异步怎么办?是什么做出了这个决定?

我不想把所有这些逻辑都堆放在其他地方。理想情况下,应该将其重新分解为可管理且有意义的组件,而不是转移责任,如果这有意义的话。

有什么建议吗?

【问题讨论】:

  • +1 这是一个针对常见问题的好问题。我希望看到提供 n+1 个解决方案。之后去哪里......嗯:)

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


【解决方案1】:

最初的想法会告诉我创建某种“事件管理器”。但后来我想,这去哪里了?在域中?

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

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

异步很好,但使事务处理变得复杂。如果您使用 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 的说明使用。

【讨论】:

  • 有趣。但是线程是如何进入混合体的呢?缓存是否需要触发线程?
  • 那种。当我开始工作时,我只需要玩一玩,并弄清楚如何使用我的 DI 容器(StructureMap)来做到这一点。敬请期待……
  • 查看您的 github 代码,对我来说似乎有很多代码可以尝试移植到 StructureMap。您知道在结构图中执行此操作的任何现有 nuget/资源吗?我不想花很多时间滚动我自己的 StructureMap 事件管理器。
  • 唯一需要移植的代码就是这个类:github.com/sogeti-se/Sogeti.Pattern/blob/master/source/… 其余的可以使用Sogeti.pattern.code nuget 包。
  • 您是否在任何地方记录了该库在 MVC 应用程序中的典型用法? nuget 网站上似乎没有任何信息/文档。我已经安装了这个包,但是我该如何使用它呢?显然我需要进行一些 DI 更改,等等...但是有没有一个示例 MVC 站点你已经启动并使用 Sogeti 运行?
【解决方案2】:

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

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

您也可以将它们分成不同的属性,这样您就可以进行不同类型的组合。

【讨论】:

    【解决方案3】:

    也许 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
    }
    

    【讨论】:

    • a) 然后 Post(域)依赖于存储库,尽管可以说存储库是域的一部分,但我喜欢让我的 POCO 保持愚蠢。 b) 并没有真正解决缓存问题。
    • @RPM1984,领域逻辑在模型内部的 DDD。
    • @DarinDimitrov - 好吧,那我不做 DDD。 :) 但我希望我的 POCO 只有属性和业务逻辑(内部),但不需要外部依赖项。
    【解决方案4】:

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

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

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

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:
        1. 我认为您需要考虑交易问题。如果您尝试在另一个线程中更新缓存并在 repository.save 之后立即返回操作结果。如果缓存操作抛出错误很难回滚(大多数缓存是全局的,通常面临锁定/同步问题)。
        2. MVC 是一种 UI 层模式。我通常将一些基于 http 上下文的逻辑放在 action body 中,并将业务逻辑放在较低层,例如处理域模型的服务。在您的情况下,我想添加一个 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);
          }
          

        【讨论】:

        • 我曾经有服务,但我把它们撕掉了,因为我意识到它们只是在包装我的存储库并且没有增加任何价值。我不认为将缓存移到那里的事实值得将其带回。不过感谢您的提示。所有有用的建议。 :)
        【解决方案7】:

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

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

        所以我将此添加到我的 StructureMap 配置中:

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

        然后就可以了。

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

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

        【讨论】:

          猜你喜欢
          • 2011-03-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-07-13
          相关资源
          最近更新 更多