ASP.NET MVC Web应用程序中的控制器是应该调用存储库,服务还是两者?

时间:2008-12-03 21:39:52

标签: .net asp.net-mvc

我的ASP.NET MVC Web应用程序中的控制器开始变得有点臃肿与业务逻辑。 Web上的示例都显示了简单的控制器操作,这些操作只是将数据从存储库中提取出来并将其传递给视图。但是如果你还需要支持业务逻辑呢?

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

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

7 个答案:

答案 0 :(得分:26)

您的控制器(在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。

答案 1 :(得分:5)

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

据我了解,DDD的(这我目前读了上)的原则之一是,当你创建聚合根的一个实例域对象被组织成骨料和,它只能直接处理Aggregate中的对象(以帮助保持清晰的责任感)。

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

以Customer类为例,Customer可能是Aggregate root,而Aggregate中的另一个类可能是Address。

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

数据库是次要问题,只有将Aggregate持久保存到数据库或从数据库中检索它才能发挥作用。

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

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

应用程序层可以直接与域通信(包括存储库接口)。

因此,如果你想创建一个对象的新实例,你可以这样做。

Customer customer = new Customer();

如果应用程序需要从存储库中检索客户的实例,那么我没有特别的理由认为它不能调用...

Customer customer = _custRepository.GetById(1)

...或

Customer customer = _custRepository.GetByKey("AlanSmith1")

最终,它将以Customer对象的实例结束,该对象在其自身的限制和规则中运行,就像它直接创建新的Customer对象一样。

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

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

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

顺便说一句,我不是说上面的任何一个都是正确的!我现在正试图解决所有这些问题!

答案 2 :(得分:1)

如果您要拥有业务层,那么我认为最好只让 业务层与数据层进行通信。我可以理解为什么在一个简单的用例中,你会让表示层(控制器)直接与数据层对话,但是一旦你确定需要一个孤立的业务层,那么将两者的使用混合在更高的层次中危险

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

答案 3 :(得分:1)

在商业服务中看到很多这样的事情似乎很烦人:

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

强烈要求绕过这项服务是很自然的。但是从长远来看,您最好还是允许您的业务服务介于控制器和存储库之间。

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

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

答案 4 :(得分:1)

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

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

  • 另一种方式(和我最喜欢的, 因为我觉得更有控制力 在app流程上),就是用了 完全独立的商务层 服务层之类的逻辑(你可以 看到这一系列中的这个问题 Stephen Walther的教程 asp.net/mvc zone。。

通过这两种方法,你可以干掉并清理乱糟糟的控制器。

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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

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

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