【问题标题】:Should controllers in an ASP.NET MVC web app call repositories, services, or both?ASP.NET MVC Web 应用程序中的控制器应该调用存储库、服务还是两者都调用?
【发布时间】:2010-09-25 05:26:09
【问题描述】:

我的 ASP.NET MVC Web 应用程序中的控制器开始因业务逻辑而变得有些臃肿。 Web 上的示例都显示了简单的控制器操作,这些操作只是将数据从存储库中拉出并将其传递给视图。但是,如果您还需要在此之上支持业务逻辑怎么办?

例如,完成订单的操作也需要发送电子邮件。我是否将其粘贴在控制器中并将此逻辑复制/粘贴到也履行订单的任何其他操作中?我的第一个直觉是创建一个像 OrderFulfillerService 这样的服务来处理所有这些逻辑并让控制器操作调用它。但是,对于从数据库中检索用户或订单列表等简单操作,我希望直接与存储库交互,而不是让该调用被服务包装。

这是一种可接受的设计模式吗?控制器操作在需要业务逻辑时调用服务,而在它们只需要数据访问时调用存储库?

【问题讨论】:

    标签: .net asp.net-mvc


    【解决方案1】:

    您的控制器(在 MVC 项目中)应该在 Service 项目中调用您的对象。服务项目是处理所有业务逻辑的地方。

    一个很好的例子是:

    public ActionResult Index()
    {
        ProductServices productServices = new ProductServices();
    
        // top 10 products, for example.
        IList<Product> productList = productServices.GetProducts(10); 
    
        // Set this data into the custom viewdata.
        ViewData.Model = new ProductViewData
                             {
                                 ProductList = productList;
                             };
    
        return View();
    }  
    

    或使用依赖注入(我的最爱)

    // Field with the reference to all product services (aka. business logic)
    private readonly ProductServices _productServices;
    
    // 'Greedy' constructor, which Dependency Injection auto finds and therefore
    // will use.
    public ProductController(ProductServices productServices)
    {
        _productServices = productServices;
    }
    
    public ActionResult Index()
    {
        // top 10 products, for example.
        // NOTE: The services instance was automagically created by the DI
        //       so i din't have to worry about it NOT being instansiated.
        IList<Product> productList = _productServices.GetProducts(10); 
    
        // Set this data into the custom viewdata.
        ViewData.Model = new ProductViewData
                             {
                                 ProductList = productList;
                             };
    
        return View();
    }
    

    现在 .. 什么是服务项目(或什么是 ProductServices)?那是一个带有您的业务逻辑的类库。例如。

    public class ProductServices : IProductServices
    {
        private readonly ProductRepository _productRepository;
        public ProductServices(ProductRepository productRepository)
        {
            _productRepository = productRepository;
        }
    
        public IList<Product> GetProducts(int numberOfProducts)
        {
            // GetProducts() and OrderByMostRecent() are custom linq helpers...
            return _productRepository.GetProducts()
                .OrderByMostRecent()
                .Take(numberOfProducts)
                .ToList();
        }
    }
    

    但这可能是非常核心和令人困惑的......所以 ServiceProduct 类的简单版本可能是(但我不会真的推荐)......

    public class ProductServices
    {
        public IList<Product> GetProducts(int numberOfProducts)
        {
            using (DB db = new Linq2SqlDb() )
            {
                return (from p in db.Products
                        orderby p.DateCreated ascending
                        select p).Take(10).ToList();
            }
        }
    }
    

    所以你去。您可以看到所有逻辑都在 Service 项目中,这意味着您可以在其他地方重用该代码。

    我从哪里学来的?

    来自Rob ConeryMVC StoreFront 媒体和tutorials。自从切片面包以来最好的事情。 他的教程通过完整的解决方案代码示例详细解释了(我做了什么)。他使用的是 SOO kewl 的依赖注入,现在我已经看到他在 MVC 中是如何使用它的。

    HTH。

    【讨论】:

    • 干杯伙伴 :) 感谢 Rob Conery/Phil Haack/Scott Hanselman 等人的教导。
    • 这种将实体拆分为 2 个对象(实体和服务/控制器)的模式从何而来?我在几个(主要是基于微软的)地方见过它,我真的不明白。事实上,我已经看到它实际上会在 ASP.NET Web 开发项目中引起问题。
    • @Harper - 我认为这是从 3 层概念发展而来的。上面,服务项目是业务逻辑所在(例如第二层)。数据访问是第三层,控制+视图是第一层。在我的项目中,我有一个 SERVICES 和一个 PIPELINES 项目,它们都是业务逻辑。
    • @Pure.Krome - 我认为这让我感到困惑,因为我在 OOP 中“长大”了一个基本解释,即对象是数据对它们进行操作的方法。将项目拆分为“纯数据”实体和服务似乎违反了该方法。
    • NTier 不会破坏 OOP 原则。对象仍然可以具有状态和行为。使用存储库/服务模式强制执行 SRP。 (对象不负责如何检索/保留它。)
    【解决方案2】:

    我不确定是否要为此使用服务。

    据我了解,DDD 的原则之一(我目前正在阅读)是域对象被组织成聚合,并且当您创建聚合根的实例时,它只能直接处理 Aggregate 中的对象(以帮助保持清晰的责任感)。

    创建聚合应强制执行任何不变量等。

    以客户类为例,客户可能是聚合根,聚合中的另一个类可能是地址。

    现在,如果您想创建一个新客户,您应该能够使用客户构造函数或工厂来执行此操作。这样做应该返回一个在聚合边界内功能齐全的对象(因此它不能处理产品,因为它们不是聚合的一部分,但它可以处理地址)。

    数据库是次要关注点,仅在将聚合持久化到数据库或从数据库中检索它时发挥作用。

    为避免直接与数据库交互,您可以创建一个 Repository 接口(如所讨论的),该接口给定 Customer 实例(包括对 Address 的引用)应该能够将 Aggregate 持久化到数据库中。

    关键是存储库接口是您的域模型/层的一部分(存储库的实现不是)。另一个因素是存储库可能最终应该调用相同的“创建”方法,就像您正在创建一个新对象一样(以维护不变量等)。如果您使用的是构造函数,这很简单,因为无论如何,当存储库从数据“创建”对象时,您最终都会调用构造函数。

    应用层可以直接与域通信(包括repository接口)。

    所以如果你想创建一个对象的新实例,你可以例如

    Customer customer = new Customer();
    

    如果应用需要从存储库中检索客户的实例,我想不出它不调用的特殊原因...

    Customer customer = _custRepository.GetById(1)
    

    或者...

    Customer customer = _custRepository.GetByKey("AlanSmith1")
    

    最终它将得到一个 Customer 对象的实例,该实例在它自己的限制和规则内运行,就像它直接创建新的 Customer 对象一样。

    我认为服务应该保留在您尝试使用的“事物”不是对象时。大多数规则(约束等)都可以作为域对象本身的一部分编写。

    我正在阅读的 DDD Quickly pdf 就是一个很好的例子。在那里,它们对 Bookshelf 对象有一个约束,因此您只能添加书架可以包含的书籍。

    对 BookShelf 对象调用 AddBook 方法会在将书添加到 BookShelf 的 Book 对象集合之前检查是否有可用空间。一个简单的示例,但业务规则由域对象本身强制执行。

    顺便说一句,我并不是说以上任何一个都是正确的!目前我正在努力解决所有这些问题!

    【讨论】:

    • 是的,应用程序层可以直接与存储库接口通信,但是,如果您在其中包含逻辑(不仅仅是对存储库的简单调用),那么您将其重构为服务
    【解决方案3】:

    如果你打算有一个业务层,那么我认为最好业务层与数据层对话。我可以理解为什么在一个简单的用例中,你会让表示层(控制器)直接与数据层对话,但是一旦你确定需要一个独立的业务层,那么在更高级别混合使用两者就可以了是危险的。

    例如,如果控制器 A 调用业务层中的一个方法来获取对象 A 的列表(并且此方法应用业务规则 - 可能是一些过滤或排序),但随后控制器 B 出现,需要相同的一条数据,却忘了业务层,直接调用数据层?

    【讨论】:

      【解决方案4】:

      在商业服务中看到很多这样的东西可能看起来很烦人:

      public Customer GetCustomer(int id)
      {
           return customerRepository.Get(id);
      }
      

      而且很自然地会有绕过服务的强烈冲动。但从长远来看,允许您的业务服务在控制器和存储库之间进行中介会更好。

      现在,对于一个非常简单的 CRUD 类型的应用程序,您可以让您的控制器直接使用存储库,而不是通过业务服务。您仍然可以拥有像 EmailerService 之类的东西,但 IMO 在获取和处理实体时,最好不要在控制器中混合和匹配业务服务和存储库调用。

      至于让实体(业务对象)调用服务或任何基础设施组件,我不会那样做。我更喜欢保持实体 POCO 并且没有依赖关系。

      【讨论】:

        【解决方案5】:

        嗯,这完全取决于你,我喜欢让控制器尽可能简单,为了归档它,我需要将业务逻辑封装在一个单独的层中,这是最重要的,主要是你有 2 个选项,假设您使用的是 Linq2SQL 或实体框架:

        • 您可以使用扩展器方法和 部分类来验证您的模型 就在保存更改之前(挂钩 方法,你可以看到一个例子 这个在 Scott 的 Nerdinner 样本中 顾)。

        • 另一种方式(也是我最喜欢的, 因为我感觉更有控制力 在应用程序流程上),是使用 完全独立的业务层 服务层之类的逻辑(您可以 在系列中看到这个方法 Stephen Walther 的教程 asp.net/mvc 区域)。

        通过这两种方法,您可以 DRY 并清理您原本凌乱的控制器。

        【讨论】:

          【解决方案6】:

          您的业务逻辑应该封装在业务对象中 - 如果您有一个 Order 对象(并且您有,不是吗?),并且业务规则规定应在订单完成时发送一封电子邮件,那么您的Fulfill 方法(或者,如果更合适,IsFulfilled 的设置器)应该触发该操作。我可能会有配置信息将业务对象指向应用程序的适当电子邮件服务,或者更一般地指向“通知程序”服务,以便可以在必要时/如果需要添加其他通知类型。

          【讨论】:

            【解决方案7】:

            如果我们可以不再一遍又一遍地看到这个例子会有所帮助......

            public ActionResult Index()
            {
              var widgetContext = new WidgetDataContext();
              var widgets = from w in widgetContext.Widget
                            select w;
              return View(widgets);
            }
            

            我确实意识到这对您的问题没有帮助,但它似乎是我认为可能具有误导性的许多演示软件的一部分。

            【讨论】:

            • 同意。我想这是“可以”的例子,但如果他们提到从架构的角度来看这不是最佳实践,那就太好了。
            猜你喜欢
            • 1970-01-01
            • 2015-10-31
            • 2010-11-24
            • 2013-09-03
            • 2012-08-28
            • 1970-01-01
            • 2011-09-29
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多