34

I am learning the well-known Onion Architecture from Jeffrey Palermo. Not specific to this pattern, but I cannot see clearly the separation between repositories and domain services. I (mis)understand that repository concerns data access and service are more about business layer (reference one or more repositories).

In many examples, a repository seems to have some kind of business logic behind like GetAllProductsByCategoryId or GetAllXXXBySomeCriteriaYYY.

For lists, it seems that service is just a wrapper on repository without any logic. For hierarchies (parent/children/children), it is almost the same problem : is it the role of repository to load the complete hierarchy ?

4

7 回答 7

37

The repository is not a gateway to access Database. It is an abstraction that allow you to store and load domain objects from some form of persistence store. (Database, Cache or even plain Collection). It take or return the domain objects instead of its internal field, hence it is an object oriented interface.

It is not recommended to add some methods like GetAllProductsByCategoryId or GetProductByName to the repository, because you will add more and more methods the repository as your use case/ object field count increase. Instead it is better to have a query method on the repository which takes a Specification. You can pass different implementations of the Specification to retrieve the products.

Overall, the goal of repository pattern is to create a storage abstraction that does not require changes when the use cases changes. This article talks about the Repository pattern in domain modelling in great detail. You may be interested.

For the second question: If I see a ProductRepository in the code, I'd expect that it returns me a list of Product. I also expect that each of the Product instance is complete. For example, if Product has a reference to ProductDetail object, I'd expect that Product.getDetail() returns me a ProductDetail instance rather than null. Maybe the implementation of the repository load ProductDetail together with Product, maybe the getDetail() method invoke ProductDetailRepository on the fly. I don't really care as a user of the repository. It is also possible that the Product only returns a ProductDetail id when I call getDetail(). It is perfect fine from the repository's contract point of view. However it complicates my client code and forces me to call ProductDetailRepository myself.

By the way, I've seen many service classes that solely wrap the repository classes in my past. I think it is an anti-pattern. It is better to have the callers of the services to use the repositories directly.

于 2012-09-07T23:33:03.923 回答
16

Repository pattern mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.

So, repositories is to provide interface for CRUD operation on domain entities. Remember that Repositories deals with whole Aggregate.

Aggregates are groups of things that belong together. An Aggregate Root is the thing that holds them all together.

Example Order and OrderLines:

OrderLines have no reason to exist without their parent Order, nor can they belong to any other Order. In this case, Order and OrderLines would probably be an Aggregate, and the Order would be the Aggregate Root

Business logic should be in Domain Entities, not in Repository layer , application logic should be in service layer like your mention, services in here play a role as coordinator between repositoies.

于 2012-09-07T12:59:56.063 回答
7

While I'm still struggling with this, I want to post as an answer but also I accept (and want) feedback about this.

In the example GetProductsByCategory(int id)

First, let's think from the initial need. We hit a controller, probably the CategoryController so you have something like:

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

so far, so good. We need to declare 'something' that creates the view model. Let's simplify and say:

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

ok, what are dependencies ? We maybe need the category tree, the products, the page, how many total products, etc.

so if we implemented this in a repository way, this could look like more or less like this :

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

instead, back to services :

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

much better, but what is exactly inside 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;
    }

}

Let's try another approach, the unit of work, I will use Entity framework as the UoW and Repositories, so no need to create those.

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

So here we are using the 'query' syntax instead of the method syntax, but instead of implementing our own complex, we can use our ORM. Also, we have access to ALL repositories, so we can still do our Unit of work inside our service.

Now we need to select which data we want, I probably don't want all the fields of my entities.

The best place I can see this is happening is actually on the ViewModel, each ViewModel may need to map it's own data, so let's change the implementation of the service again.

public CategoryService(DbContext db) {
    // same dependency injection here

    public Category GetCategory(int id) {
        var category = db.Category.Find(id);
        return category;
    }
}

so where are all the products and inner categories?

let's take a look at the ViewModel, remember this will ONLY map data to values, if you are doing something else here, you are probably giving too much responsibility to your 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.
}

you can imagine the CategoryProductViewModel by yourself right now.

BUT (why is there always a but??)

We are doing 3 db hits, and we are fetching all the category fields because of the Find. Also Lazy Loading must be enable. Not a real solution isn't it ?

To improve this, we can change find with where... but this will delegate the Single or Find to the ViewModel, also it will return an IQueryable<Category>, where we know it should be exactly one.

Remember I said "I'm still struggling?" this is mostly why. To fix this, we should return the exact needed data from the service (also know as the ..... you know it .... yes! the ViewModel).

so let's back to our controller :

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

inside the GetProductCategoryViewModel method, we can call private methods that return the different pieces and assemble them as the ViewModel.

this is bad, now my services know about viewmodels... let's fix that.

We create an interface, this interface is the actual contract of what this method will return.

ICategoryWithProductsAndChildsIds // quite verbose, i know.

nice, now we only need to declare our ViewModel as

public class CategoryViewModel : ICategoryWithProductsAndChildsIds 

and implement it the way we want.

The interface looks like it has too many things, of course it can be splitted with ICategoryBasic, IProducts, IChilds, or whatever you may want to name those.

So when we implement another viewModel, we can choose to do only IProducts. We can have our services having methods (private or not) to retrieve those contracts, and glue the pieces in the service layer. (Easy to say than done)

When I get into a fully working code, I might create a blog post or a github repo, but for now, I don't have it yet, so this is all for now.

于 2015-04-04T21:11:29.993 回答
6

I believe the Repository should be only for CRUD operations.

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

So IRepository would have: Add, Remove, Update, Get, GetAll and possibly a version of each of those that takes a list, i.e, AddMany, RemoveMany, etc.

For performing search retrieval operations you should have a second interface such as an IFinder. You can either go with a specification, so IFinder could have a Find(criteria) method that takes criterias. Or you can go with things like IPersonFinder which would define custom functions such as: a FindPersonByName, FindPersonByAge etc.

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

The alternative would be:

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

This second approach is more complex. You need to define a strategy for the criterias. Are you going to use a query language of some sort, or a more simple key-value association, etc. The full power of the interface is also harder to understand from simply looking at it. It's also easier to leak implementations with this method, because the criterias could be based around a particular type of persistence system, like if you take a SQL query as criteria for example. On the other hand, it might prevent you from having to continuously come back to the IFinder because you've hit a special use case that requires a more specific query. I say it might, because your criteria strategy will not necessarily cover 100% of the querying use cases you might need.

You could also decide to mix both together, and have an IFinder defining a Find method, and IMyObjectFinders that implement IFinder, but also add custom methods such as FindByName.

The service acts as a supervisor. Say you need to retrieve an item but must also process the item before it is returned to the client, and that processing might require information found in other items. So the service would retrieve all appropriate items using the Repositories and the Finders, it would then send the item to be processed to objects that encapsulates the necessary processing logic, and finally it would return the item requested by the client. Sometime, no processing and no extra retrievals will be required, in such cases, you don't need to have a service. You can have clients directly call into the Repositories and the Finders. This is one difference with the Onion and a Layered architecture, in the Onion, everything that is more outside can access everything more inside, not only the layer before it.

It would be the role of the repository to load the full hierarchy of what is needed to properly construct the item that it returns. So if your repository returns an item that has a List of another type of item, it should already resolve that. Personally though, I like to design my objects so that they don't contain references to other items, because it makes the repository more complex. I prefer to have my objects keep the Id of other items, so that if the client really needs that other item, he can query it again with the proper Repository given the Id. This flattens out all items returned by the Repositories, yet still let's you create hierarchies if you need to.

You could, if you really felt the need to, add a restraining mechanism to your Repository, so that you can specify exactly which field of the item you need. Say you have a Person, and only care for his name, you could do Get(id, name) and the Repository would not bother with getting every field of the Person, only it's name field. Doing this though, adds considerable complexity to the repository. And doing this with hierarchical objects is even more complex, especially if you want to restrict fields inside fields of fields. So I don't really recommend it. The only good reason for this, to me, would be cases where performance is critical, and nothing else can be done to improve the performance.

于 2014-02-17T20:15:20.050 回答
4

In Domain Driven Design the repository is responsible for retrieving the whole Aggregate.

于 2012-09-07T11:28:40.767 回答
1

See the relation between services and repositories on Services in Domain-Driven Design (DDD) by Lev Gorodinski with updates from Vaughn Vernon emphasizing the Hexagonal Architecture style:

An application service has an important and distinguishing role - it provides a hosting environment for the execution of domain logic. As such, it is a convenient point to inject various gateways such as a repository or wrappers for external services.

A service is part of the Application Core as one can see graphically depicted by Onion Architecture or Microservices Patterns by Chris Richardson page 148 in the context of the Hexagonal Architecture.

A repository implementation on the other hand is just an adapter, something that is able to translate the application's messages to a storage system (which could be anything: database, files, etc). See also Interface Adapters section from The Clean Architecture (talking about Interface Adapters layer):

If the database is a SQL database, then all the SQL should be restricted to this layer ...

Search for "repository adapter" at Hexagonal Architecture section Stage 3: (FIT or UI) App mock database talking about the repository implementation viewed as an adapter.

A repository interface on the other hand is part of the Application Core:

The first layer around the Domain Model is typically where we would find interfaces that provide object saving and retrieving behavior, called repository interfaces.

  • see also page 159 from Microservices Patterns by Chris Richardson in the context of the Hexagonal Architecture

According to Eric Evans Domain-Driven Design:

When a significant process or transformation in the domain is not a natural responsibility of an ENTITY or VALUE OBJECT, add an operation to the model as standalone interface declared as a SERVICE. Define the interface in terms of the language of the model and make sure the operation name is part of the UBIQUITOUS LANGUAGE. Make the SERVICE stateless.

So, a service is just something that contains business logic.

My conclusion: a service is a business logic building block; a repository is just an adapter providing access for the business logic to a storage system. One could mock the adapter to test the business logic in isolation or one could create additional repository adapters to provide more storage options for the application (e.g. file adapter, no-sql database adapter, etc).

About GetAllProductsByCategoryId or GetAllXXXBySomeCriteriaYYY

The repository methods should denote their intention so their naming could be very expressive; while those methods contain only the logic to communicate with the storage system they are fine. Based on the fact that the method naming might be very meaningful some frameworks (e.g. Spring Data) are able to create/generate the implementation for those methods and there's nothing wrong/odd there. On the other hand one might want to optimize/reduce the number of repository methods in various ways like having them accept a complex criteria object or a criteria list instead of having a method for each combination of those criteria, but this doesn't change the role of the repository (implementation) which is still an adapter.

About "is it the role of repository to load the complete hierarchy?"

By now the role of the repository (implementation) should be clear: is to convey the application's messages/requests intended for the storage system. The message/request could be e.g. "give me the Person identified by 123" or "give me a list of Person conform to some criteria" or "give me a complete hierarchy conform to some criteria" as long as the message/request can be translated to the storage system as such. Some storage systems are not able, for example, to give/return a hierarchy object so one should create a service to construct the hierarchy object; other storage systems are able to give/return a hierarchy object but of a type specific to their API/driver so the repository (implementation) should convert it to a DTO which might be further processed by a service in order to construct the business hierarchy object. Remember: a repository (implementation) is just an adapter which should be possible to mock (for testing purposes).

SIDE NOTE

Note also that there could be more types of services: application services and domain services - see them depicted at Onion Architecture but better explained at Services in Domain-Driven Design (DDD) by Lev Gorodinski where one should check for:

The differences between a domain service and an application services are subtle but critical

于 2020-07-05T07:13:51.653 回答
0

Onion and Hexagonal Architectures purpose is to invert the dependency from domain->data access.
Rather than having a UI->api->domain->data-access,
you'll have something like UI->api->domain**<-**data-access
To make your most important asset, the domain logic, is in the center and free of external dependencies. Generally by splitting the Repository into Interface/Implementation and putting the interface along with the business logic.

Now to services, there's more that one type of services:

  • Application Services: your controller and view model, which are external concerns for UI and display and are not part of the domain
  • Domain Services: which provide domain logic. In you're case if the logic you're having in application services starts to do more that it's presentation duties. you should look at extracting to a domain service
  • Infrastructure Services: which would, as with repositories, have an interface within the domain, and an implementation in the outer layers

@Bart Calixto, you may have a look at CQRS, building your view model is too complex when you're trying to use Repositories which you design for domain logic. you could just rewrite another repo for the ViewModel, using SQL joins for example, and it doesn't have to be in the domain

于 2021-02-07T13:40:52.977 回答