简体   繁体   中英

How to make generic repository for two domains to avoid adding dependencies?

I am making an app which has two separate domains, which contain models which represent different concepts:

架构图

Now, I want to create generic repository which would allow me to make operations on data sets, and I want it to be used by both DataAccess projects, because DRY. The problem is, I would write these repositories in a way that they would use id of entity, so I need base type or at least interface which will allow me for operating on those. I would create generic repository using BaseEntity which contains ID like that:

public class GenericRepository<T> where T: BaseEntity
{
   public T SomeActionThatRequiresId(int id)
   {
     // something that requires ID from BaseEntity
   }
}

The problem is I don't know where to put BaseEntity .

There is always the talk that domain projects should not have dependecies in them, so I don't want to create separate "foundation" project that would be referenced, to keep up with this idea.

Putting it in one of the domain projects and then referencing it in other doesn't even count if I want to keep up with above rule.

I have no idea how to properly do this.

Can anyone help?

Not to put to fine a point on it, I think you may be overestimating the importance of DRY as a principal, and trying to define common foundations too soon. A major limitation of DRY is when it causes dependency issues, which you seem to have hit at a very early design stage.

If Config & Core really are separate domains, then it would seem probable that their data access patterns will diverge over time. There is nothing wrong with repeating yourself at least once early on in development, and then waiting to see if they do diverge. You can always consolidate them into a generic repository some time later if they always need to change in sync.

Likewise your statement: "There is always the talk that domain projects should not have dependecies in them" should be treated as a guide and not an hard and fast rule.

If you want all of your domain entities to have common properties, perhaps audit fields in addition to an int id field, then a common base entity library is the obvious solution, and shouldn't be ruled out.

I'm writing this answer because comment has limited number of characters. :)

From what I understand you are not really looking to find generic way to implement interface. I think there is common misconception about DRY . If you create two repositories that are implemented in simmilar manner (having same methods like GetById ) this is not DRY . DRY is when you are copy-pasting code that is doing exactly the same thing in the same context.

You have two completly different contexts one of them is Configuration and second one is Core . In that case you would be breaking Common Closure Principle because repositories would be sharing the same interface while they probably have different axis of change.

Based on what you have described I'd like you to confirm that you are using UML correctly ( Core project is being inherited by DataAccess or you wanted to show what is direction of references? so Core is being referrenced by DataAccess ?)

I don't know if that would be best suting to you but Data Access Layer should be referencing Domain Layer . Since most of the times Data Access is only used to presist Domain objects or to retrive Domain objects and should not be referenced by any other place.

In that case that would look like this:

在此处输入图片说明

Arrows represent what is referencing what. In that case you put IConfigRepo into Config.Domain project and implementation of that Repository is done in Config.DataAccessLayer that gives you ensurence that your Domain provides contract and knows how that is allowed to fetch data but doesn't know how they are stored. So you are allowed to store them in SQL or No-SQL database.

Know your Core project is simply delegating job to correct Domain project. It can be done anyhow but you are sure that Buisness logic is stored in specific Domain project.

Regarding the idea of having abstract repository to fetch records. What would be benefit of that? Are you sure that you want to allow polymorfic execution in Core.Domain of repository that is implemented in Config.DataAccess ?

My answer is in two parts...

Part 1: Where to put BaseEntity

Put it in a separate independent place.

In this way the domains can all depend on it, without depending in each other.

Below you can see a multi-layered architecture where the usual suspects (UI, logic, data access) are appropriately separated. Concepts that they need to share are in the Common layer, which sits off to the side. It is referenced by most other projects/assemblies, but it's safe to do so because code in Common is relatively more stable.

Eg Poco's are dumb data structures and are less likely to change compared to (for example) logic code which uses them. If a Poco does need to change it's most likely you'd already be wanting to change the other layers also anyway (eg driven by a functional change).

Therefore, Common (or some similar domain independent place) where you should put your BaseEntity .

在此处输入图片说明

Part 2: Are you solving the right problem?

The sense I get (echoed by others reading this thread) is that you might be focusing on the wrong problem. If that's the case - that's ok. It's easily done and you shouldn't beat yourself up about it - or worse, get sucked into a sunk cost fallacy .

The issue is that as soon as you have one implementation that is used widely (like your generic repo), it becomes constraining. Use of base classes and inheritance (especially when contrasted with interfaces) is a great example of this general problem.

At an architectural level, reuse can be achieved in a few ways, one is through patterns . In your case all you need to do is identify a pattern (or combination of patterns) that work well for you in addressing your needs and priorities (eg code maintainability, ability to meet functional requirements, flexibility - whatever).

Reusing these patterns means you have consistency and clarity, the fact that you might have duplicate code is not a concern on the basis that the benefits of the patterns outweigh any the negatives of duplicate code.

Then there's the Single Responsibility Principle (SRP) . Having a single generic repo breaks SRP because it's trying to work across all domains - each of which will have different design and requirements forces working on them in different ways that will conflict with the generic repo idea.

The generic repo looks like it's doing just one thing (data access) - but that one thing is usually too large and complex. To say that a generic repo is 'doing SRP' is an illusion.

Having attempted similar paths myself (and observed them on projects), you'll find that there will be data-access edge cases where the generic repo idea gets in the way - such as when you have cross-domain activities, especially if runtime performance for those needs to be especially high.

Use of DRY is good, but it's one tool of many, not an end in it's own right, and it's use needs to be balanced with other considerations.

It is hard for us to find the optimal solution without knowing the details of your project.

But I will try to give you some options:

  1. Extend your core library with BaseEntity definition. Benefit: That seems to follow your basic design according to the little diagram but also means your repo module will depend on the core library.
  2. Introduce new intermediate Layer that defines BaseEntity. If for some reason it doesn't belong into the existing Core library you can create a new library that is common to both domains and derived from Core, with the same dependency problem just on the intermediate library.
  3. You don't create BaseEntity and instead the Repo Module provides the necessary interfaces in pure function form, eg IRepoObject { getId(); } and your domain objects implement that interface. This has clean separation of concerns. (And it is not a problem that the domain module requires the repo module because they provide repo functionality now)
  4. Functional Programming Approach: If the data structures that the repo module needs are not overly complex you just pass those as primitives on the function calls, eg doRepoStuff(String id, Byte[] blob, ...). So the repo module would be agnostic to any specific classes or interface implementation.

In my experience simple solutions are usually preferable over complicated object oriented designs with deep inheritance and lots of references, so I would tend to use solution 3 or 4 depending on the complexity of data exchanged.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM