代码之家  ›  专栏  ›  技术社区  ›  Cybermaxs

洋葱架构-存储库与服务?

  •  37
  • Cybermaxs  · 技术社区  · 12 年前

    我正在向Jeffrey Palermo学习著名的洋葱建筑。 不是特定于这种模式,但我无法清楚地看到存储库和域服务之间的分离。 我(错误地)理解存储库与数据访问有关,而服务更多地与业务层有关(引用一个或多个存储库)。

    在许多示例中,存储库背后似乎有某种业务逻辑,比如 GetAllProductsByCategoryId GetAllXXXBySomeCriteriaYYY

    对于列表,服务似乎只是存储库上的包装器,没有任何逻辑。 对于层次结构(父/子/子),几乎是同样的问题:加载完整的层次结构是存储库的职责吗?

    7 回复  |  直到 12 年前
        1
  •  40
  •   Sergey Vyacheslavovich Brunov prodigitalson    8 年前

    存储库 不是 访问数据库的网关。它是一种抽象,允许您从某种形式的持久性存储中存储和加载域对象。(数据库、缓存甚至普通集合)。它获取或返回域对象,而不是其内部字段,因此它是一个面向对象的接口。

    不建议添加一些方法,如 GetAllProductsByCategoryId GetProductByName 到存储库,因为随着用例/对象字段数的增加,您将在存储库中添加越来越多的方法。相反,最好在存储库上有一个采用规范的查询方法。您可以传递规范的不同实现来检索产品。

    总的来说,存储库模式的目标是创建一个存储抽象,在用例发生变化时不需要进行更改。 This article 详细讨论了领域建模中的Repository模式。你可能感兴趣。

    对于第二个问题:如果我看到 ProductRepository 在代码中,我希望它向我返回一个Product列表。我还希望每个Product实例都是完整的。例如,如果Product引用了 ProductDetail 对象,我希望 Product.getDetail() 还给我一个 产品详细信息 实例而不是null。也许是实现了存储库加载 产品详细信息 与产品一起 getDetail() 方法调用 ProductDetailRepository 在飞行中。作为存储库的用户,我其实并不在乎。也有可能产品只返回 产品详细信息 我打电话时的身份证 获取详细信息() 。从存储库的合同角度来看,这是非常好的。然而,它使我的客户端代码复杂化,并迫使我调用 产品详细信息存储库 我自己

    顺便说一句,我在过去看到过许多只包装存储库类的服务类。我认为这是一种反模式。最好让服务的调用方直接使用存储库。

        2
  •  17
  •   cuongle    12 年前

    存储库模式使用类似集合的接口在域和数据映射层之间进行中介,用于访问域对象。

    所以,存储库就是为域实体上的CRUD操作提供接口。请记住,存储库处理整个聚合。

    聚合是一组属于一起的事物。聚合根是将它们连接在一起的东西。

    实例 Order OrderLines 以下为:

    没有上级订单,订单行没有理由存在,也不能属于任何其他订单。在这种情况下,Order和OrderLines可能是一个聚合,而Order则是聚合根

    业务逻辑应该在域实体中,而不是在存储库层中,应用程序逻辑应该在服务层中,就像您提到的那样,这里的服务充当着存储库之间的协调器。

        3
  •  7
  •   Bart Calixto    9 年前

    虽然我仍在努力解决这个问题,但我想发布一个答案,但我也接受(并希望)对此的反馈。

    在示例中 GetProductsByCategory(int id)

    首先,让我们从最初的需求开始思考。我们碰到了一个控制器,可能是类别控制器,所以你有这样的东西:

    public CategoryController(ICategoryService service) {
        // here we inject our service and keep a private variable.
    }
    
    public IHttpActionResult Category(int id) {
        CategoryViewModel model = something.GetCategoryViewModel(id); 
        return View()
    } 
    

    到目前为止,一切都很好。我们需要声明创建视图模型的“something”。 让我们简单地说:

    public IHttpActionResult Category(int id) {
        var dependencies = service.GetDependenciesForCategory(id);
        CategoryViewModel model = new CategoryViewModel(dependencies); 
        return View()
    } 
    

    好的,什么是依赖关系?我们 大概 需要类别树、产品、页面、总产品数量等。

    因此,如果我们以存储库的方式实现这一点,它看起来或多或少是这样的:

    public IHttpActionResult Category(int id) {
        var products = repository.GetCategoryProducts(id);
        var category = repository.GetCategory(id); // full details of the category
        var childs = repository.GetCategoriesSummary(category.childs);
        CategoryViewModel model = new CategoryViewModel(products, category, childs); // awouch! 
        return View()
    } 
    

    相反,返回服务:

    public IHttpActionResult Category(int id) {
        var category = service.GetCategory(id);
        if (category == null) return NotFound(); //
        var model = new CategoryViewModel(category);
        return View(model);
    }
    

    好多了,但里面到底是什么 service.GetCategory(id) ?

    public CategoryService(ICategoryRespository categoryRepository, IProductRepository productRepository) {
        // same dependency injection here
    
        public Category GetCategory(int id) {
            var category = categoryRepository.Get(id);
            var childs = categoryRepository.Get(category.childs) // int[] of ids
            var products = productRepository.GetByCategory(id) // this doesn't look that good...
            return category;
        }
    
    }
    

    让我们尝试另一种方法,即工作单元,我将使用实体框架作为UoW和存储库,因此无需创建它们。

    public CategoryService(DbContext db) {
        // same dependency injection here
    
        public Category GetCategory(int id) {
            var category = db.Category.Include(c=> c.Childs).Include(c=> c.Products).Find(id);
            return category;
        }
    }
    

    因此,我们在这里使用“query”语法而不是方法语法,但我们可以使用ORM,而不是实现我们自己的复合体。此外,我们可以访问所有存储库,因此我们仍然可以在我们的服务中完成我们的工作单元。

    现在我们需要选择我们想要的数据,我可能不想要实体的所有字段。

    我能看到的最好的地方实际上是在ViewModel上,每个ViewModel可能需要映射它自己的数据,所以让我们再次更改服务的实现。

    public CategoryService(DbContext db) {
        // same dependency injection here
    
        public Category GetCategory(int id) {
            var category = db.Category.Find(id);
            return category;
        }
    }
    

    那么,所有的产品和内部类别都在哪里呢?

    让我们来看看ViewModel,记住这只会将数据映射到值,如果你在这里做其他事情,你可能会给ViewModel太多的责任。

    public CategoryViewModel(Category category) {
        Name = category.Name;
        Id = category.Id;
        Products = category.Products.Select(p=> new CategoryProductViewModel(p));
        Childs = category.Childs.Select(c => c.Name); // only childs names.
    }
    

    你可以想象 CategoryProductViewModel 现在就自己一个人。

    但是(为什么总是有一个但是??)

    我们正在进行3次数据库命中,并且由于Find,我们正在获取所有类别字段。同样是懒惰加载 必须 启用。不是一个真正的解决方案,不是吗?

    为了改善这一点,我们可以改变查找。。。但这将委托 Single Find 对于ViewModel,它还将返回 IQueryable<Category> ,我们知道它应该是一个。

    还记得我说过“我还在挣扎吗?”这就是为什么。要解决此问题,我们应该从服务返回所需的确切数据(也称为……你知道的……是的!ViewModel)。

    那么让我们回到我们的控制器:

    public IHttpActionResult Category(int id) {
        var model = service.GetProductCategoryViewModel(id);
        if (category == null) return NotFound(); //
        return View(model);
    }
    

    内部 GetProductCategoryViewModel 方法,我们可以调用返回不同部分的私有方法,并将它们组装为ViewModel。

    这很糟糕,现在我的服务知道了视图模型。。。让我们来解决这个问题。

    我们创建了一个接口,这个接口是这个方法将返回的实际合约。

    ICategoryWithProductsAndChildsIds // quite verbose, i know.
    

    很好,现在我们只需要将ViewModel声明为

    public class CategoryViewModel : ICategoryWithProductsAndChildsIds 
    

    并按照我们想要的方式实施。

    这个接口看起来有太多的东西,当然可以用 ICategoryBasic , IProducts , IChilds ,或者你想给它们起什么名字。

    因此,当我们实现另一个viewModel时,我们可以选择只执行 I产品 。 我们可以让我们的服务具有方法(私有或非私有)来检索这些契约,并将这些部分粘在服务层中。(说起来容易做起来难)

    当我进入一个完全可以工作的代码时,我可能会创建一篇博客文章或github repo,但目前我还没有,所以这就是目前的全部内容。

        4
  •  6
  •   Didier A.    11 年前

    我认为存储库应该只用于CRUD操作。

    public interface IRepository<T>
    {
        Add(T)
        Remove(T)
        Get(id)
        ...
    }
    

    因此,IRepository将有:Add、Remove、Update、Get、GetAll,以及可能采用列表的每个版本,即AddMany、RemoveMany等。

    为了执行搜索检索操作,您应该有第二个接口,例如IFinder。你可以使用一个规范,所以IFinder可以有一个接受标准的Find(criteria)方法。或者你可以使用IPersonFinder之类的东西,它可以定义自定义函数,例如:FindPersonByName、FindPersonByAge等。

    public interface IMyObjectFinder
    {
        FindByName(name)
        FindByEmail(email)
        FindAllSmallerThen(amount)
        FindAllThatArePartOf(group)
        ...
    }
    

    替代方案是:

    public interface IFinder<T>
    {
        Find(criterias)
    }
    

    第二种方法更为复杂。你需要为标准制定一个策略。你是要使用某种查询语言,还是更简单的键值关联等。简单地看界面也很难理解它的全部功能。使用这种方法也更容易泄露实现,因为标准可能基于特定类型的持久性系统,比如以SQL查询为标准。另一方面,它可能会阻止您继续返回IFinder,因为您遇到了一个需要更具体查询的特殊用例。我说可能,因为您的标准策略不一定涵盖您可能需要的100%查询用例。

    您还可以决定将两者混合在一起,让一个定义Find方法的IFinder和实现IFinder的IMyObjectFinders,但也可以添加自定义方法(如FindByName)。

    该服务充当主管。假设您需要检索一个项,但在将该项返回到客户端之前还必须对其进行处理,并且处理可能需要在其他项中找到信息。因此,服务将使用存储库和查找器检索所有适当的项,然后将要处理的项发送到封装必要处理逻辑的对象,最后返回客户端请求的项。有时,不需要处理,也不需要额外的检索,在这种情况下,您不需要服务。您可以让客户端直接调用存储库和查找程序。这是洋葱和分层架构的一个区别,在洋葱中,外部的一切都可以访问内部的一切,而不仅仅是之前的层。

    存储库的作用是加载正确构造返回的项所需的全部层次结构。因此,如果您的存储库返回的项目具有另一种类型的项目的列表,那么它应该已经解决了这个问题。不过就我个人而言,我喜欢设计我的对象,这样它们就不会包含对其他项目的引用,因为这会使存储库更加复杂。我更喜欢让我的对象保留其他项目的Id,这样,如果客户真的需要其他项目,他可以在给定Id的适当存储库中再次查询它。这会使存储库返回的所有项目变平,但如果需要,仍然可以创建层次结构。

    如果您真的觉得有必要,您可以在您的存储库中添加一个约束机制,这样您就可以准确地指定您需要的项目的哪个字段。假设你有一个Person,并且只关心他的名字,你可以做Get(id,name),Repository不会麻烦获取Person的每个字段,只需要它的name字段。但是,这样做会给存储库增加相当大的复杂性。对层次对象执行此操作甚至更为复杂,尤其是如果您希望将字段限制在字段的字段中。所以我并不真的推荐它。对我来说,唯一好的理由是性能至关重要,而无法采取其他措施来提高性能。

        5
  •  4
  •   Dennis Traub    12 年前

    在域驱动设计中,存储库负责检索整个聚合。

        6
  •  0
  •   Amokrane Belloui    4 年前

    洋葱和六边形架构的目的是颠倒对域的依赖性->数据访问。
    而不是具有UI->api->域->数据访问,
    您将拥有类似UI->api->域**<-**数据存取
    为了使您最重要的资产,即域逻辑处于中心位置,并且没有外部依赖关系。 通常通过将存储库拆分为接口/实现,并将接口与业务逻辑放在一起。

    现在,对于服务,有不止一种类型的服务:

    • 应用程序服务:您的控制器和视图模型,它们是UI和显示的外部问题,不属于域的一部分
    • 域服务:提供域逻辑。在您的情况下,如果您在应用程序服务中拥有的逻辑开始做更多的事情,那么它就是表示职责。您应该考虑提取到域服务
    • 基础设施服务:与存储库一样,它在域内有一个接口,在外层有一个实现

    @Bart Calixto,你可以看看CQRS,当你试图使用为域逻辑设计的存储库时,构建你的视图模型太复杂了。 您可以为ViewModel重写另一个repo,例如使用SQL联接,它不必在域中

        7
  •  0
  •   Adrian    2 年前

    存储库的作用是加载完整的层次结构吗?

    简短回答:是的,如果存储库的结果是一个层次结构

    存储库的作用是在 任何形状 你需要,从 数据源 (例如, Lucene 索引等)。

    让我们假设一个存储库(接口)具有 GetSomeHierarchyOrListBySomeCriteria 操作-操作、参数及其结果是 应用程序核心 !

    让我们关注结果:无论是形状(列表、层次结构等)还是存储库 实施 应该做的 每件事 必须归还。

    如果使用的是NoSql数据库,则 按某些条件获取某些层次结构或列表 实施 可能只需要一个NoSql数据库查询,而不需要其他转换或转换即可获得所需结果(例如层次结构)。另一方面,对于SQL数据库,相同的结果可能意味着多个查询和复杂的转换或转换,但这是一个 实施 详细信息,存储库 界面 是一样的。

    存储库与域服务

    根据 The Onion Architecture : part 1 ,我指的是官方页面,而不是其他人的解释:

    领域模型周围的第一层通常是我们 找到提供对象保存和检索行为的接口, 称为存储库接口。[…]只有接口在应用程序核心中。

    请注意 域服务 上层 领域模型

    从第二官方页面开始, The Onion Architecture : part 2 ,作者忘记了 域服务 层,并正在描述IConference 存储库 作为 对象服务 正上方的层 领域模型 ,更换 域服务 层这个 对象服务 层在中继续 The Onion Architecture : part 3 ,所以我问:什么 域服务 ?:))

    在我看来,作者的意图 对象服务 域服务 只由存储库组成,否则他不会为其他事情留下任何线索。