Tuesday, March 20, 2012

I May Be Done With Generic Repositories

I love repositories. I really do. It's one of the first patterns I learned anything about, it's my most used pattern, etc. And the first thing I've always done is to define the interface for the repository. And, invariably, I always come to some new realization about the design of the repository each time I do it.

For example, I once used an interface very similar to this:

public interface IRepository<T>
{
  IEnumerable<T> GetAll();
  T GetByID(int id);
  void Save(T model);
  void Delete(T model);
  void DeleteByID(int id);
}

That's all well and good, and it works. But after a while it didn't sit right with me. I didn't like the GetAll() and GetByID(). So the next time around I tried this:

public interface IRepository<T>
{
  IEnumerable<T> Get();
  void Save(T model);
  void Delete(T model);
  void DeleteByID(int id);
}

At this point I rely on chaining queries onto Get() to actually get stuff. So if I want to get by the ID, I'd do something like this:

SomeRepositoryInstance.Get().Single(foo => foo.ID == id);

Again, it works. This freed me from having to generically define my gets, since different models have different keys on which to commonly get.

Then I started to dislike the Delete() and its ilk. So I'd add some kind of "mark to delete" mechanism on the models themselves and just include it in the Save():

public interface IRepository<T>
{
  IEnumerable<T> Get();
  void Save(T model);
}

It's starting to get a little light. And now, in more recent iterations with more complex domains, I may be done with this generic interface entirely.

A few realities go against this interface and others along the same pattern:
  • Not every model has a repository. So I'd have to jump through hoops to try to limit what can have repositories. This can result in bare repository implementations with NotImplementedExceptions being thrown, which is ugly. Or in complex IoC bootstrapping. Or all kinds of little workarounds.
  • Not every model can be inserted, or updated, or deleted. Some just aren't supposed to be as a result of business logic. And that's a domain logic concern, not a data access layer concern. So how would one reconcile that logic in the implementations of the generic repository?
  • Some repositories need extra stuff. So I have an IRepository for all models, and some of them also have an IModelRepository? That made for unintuitive usage by other developers. (Recognizing the fact that myself a few months later counts as an "other developer.")

These realities got me thinking about the entire concept of the generic repository. It's great for things like straight Linq-to-SQL where every database entity is also a model (don't get me started on that). But for more complex domains where either you're defining the models first before the persistence (something most companies still don't like to do for some reason) or where the two are growing/evolving a lot over time, it doesn't seem to work very well.

So I'm at the point now where I'm basically just dropping the IRepository and sticking with custom repositories. IFooRepository, IBazRepository, etc. A repository shouldn't be some second-class citizen that gets handed over to some code generation tool and generically applied across the board. There's business logic to be applied here. Like I said, what if some of the models are un-deleteable. Or, worse, only deleteable in certain context. That business logic belongs in the domain where those repository interfaces are defined.

Interestingly enough, this same leap has led me to another design consideration. The repository itself isn't anything special, structurally. It represents a very simple concept... a list of models from a data store. So, from the perspective of the domain, the repository implementation is simply a collection of models.

An IList, if you will.

This presents an opportunity for object oriented thinking. What logic should be encapsulated within the model, and what logic should be encapsulated within the collection of the model? A colleague and I just discussed this today and it had never really occurred to me, but he's right. Some logic belongs on the collection, and not all collections are as generic as I used to want my repositories to be.

For example, what if for a particular model there's a business rule that says there can never be more than five of them together? That shouldn't be a concern of the model itself, but it is a concern of a collection of the model. So if my "repositories" implement IEnumerable and maintain their own iterators, then they can internally contain the logic of collections of the models.

I'll have to try this out and see how it works, but I'm intrigued by the idea.

No comments:

Post a Comment