The repository pattern is another abstraction, like most things in Computer Science. It is a pattern that is applicable in many different languages. In fact a lot of developers use the repository pattern and don’t even realize it.
In this post I am going to transform a piece of code. We start with a piece of code that is loading a single record from a database. Once the record is fetched it is returned to the caller. Let’s take a look at some code.
Needs Improvement
The record we are loading out of our database is PersonModel
.
public class PersonModel
{
public string Name { get; set; }
public int Age { get; set; }
}
The service that is loading a person out of the database is ICompanyLogic
. It consists of the following method
definition.
public interface ICompanyLogic
{
PersonModel GetPersonByName(string name);
}
The implementation of the ICompanyLogic
is handled by CompanyLogic
.
public class CompanyLogic: ICompanyLogic
{
private IPersonDataContext _personDataContext;
public PersonService(IPersonDataContext personDataContext)
{
_personDataContext= personDataContext;
}
public PersonModel GetPersonByName(string name)
{
using(var ctx = _personDataContext.NewContext())
{
var person = ctx.People.First(p => p.Name.Equals(name));
return person;
}
}
}
So far, this isn’t so bad. We have a business service CompanyLogic
that can retrieve a single person from the
database.
But then we have a new requirement that says we also need a way to load a company from another database. So we need to
add a new method and extend CompanyLogic
.
CompanyModel
represents the model stored in the company database.
public class CompanyModel
{
public string Name { get; set; }
public int Size { get; set; }
public bool Public { get; set; }
}
We extend CompanyLogic
to have a method that returns a company by name.
public class CompanyLogic: ICompanyLogic
{
private IPersonDataContext _personDataContext;
private ICompanyDataContext _companyDataContext;
public PersonService(IPersonDataContext personDataContext,
ICompanyDataContext companyDataContext)
{
_personDataContext= personDataContext;
_companyDataContext = companyDataContext;
}
public PersonModel GetPersonByName(string name)
{
using(var ctx = _personDataContext.NewContext())
{
var person = ctx.People.First(p => p.Name.Equals(name));
return person;
}
}
public CompanyModel GetCompanyByName(string companyName)
{
using(var ctx = _companyDataContext.NewContext())
{
var person = ctx.Company.First(c => c.Name.Equals(companyName));
return person;
}
}
}
Now we are starting to see the problems with this initial solution. Here is a short list of things that are not ideal.
CompanyLogic
, knows how to access two different databases.- We have duplicated code with our
using
statements. - Our logic knows how people and companies are stored.
GetPersonByName
andGetCompanyByName
cannot be reused without bringing in all ofCompanyLogic
.
In addition to all of these things, how do we test CompanyLogic
in its current state? We have to mock the data context
for people and companies to have literal database records. This is possible to do. But our hard work should be going
into testing our logic, not mocking database objects.
Implementing Repository Pattern
The repository pattern adds an abstraction layer over the top of data access. A little bit of abstraction goes a long
way. With the repository pattern we can add a thin layer of abstraction for accessing the people and company databases.
Then CompanyLogic
or any other logic can leverage those abstractions.
Let’s begin by creating our IPersonRepository
interface and its accompanying implementation.
public interface IPersonRepository
{
PersonModel GetPersonByName(string name);
}
public class PersonRepository: IPersonRepository
{
private IPersonDataContext _personDataContext;
public PersonRepository(IPersonDataContext personDataContext)
{
_personDataContext= personDataContext;
}
public PersonModel GetPersonByName(string name)
{
using(var ctx = _personDataContext.NewContext())
{
return ctx.People.First(p => p.Name.Equals(name));
}
}
}
Then we can do something very similar for companies. We can create the ICompanyRepository
interface and its
implementation.
public interface ICompanyRepository
{
PersonModel GetCompanyByName(string name);
}
public class CompanyRepository: ICompanyRepository
{
private ICompanyDataContext _companyDataContext;
public CompanyRepository(ICompanyDataContextcompanyDataContext)
{
_companyDataContext= personDataContext;
}
public CompanyModel GetCompanyByName(string name)
{
using(var ctx = _companyDataContext.NewContext())
{
return ctx.Company.First(p => p.Name.Equals(name));
}
}
}
We now have two separate repositories. PersonRepository
knows how to load a given person by name from the person
database. CompanyRepository
can load companies by name from the company database. Now let’s refactor CompanyLogic
to
leverage these repositories instead of the data contexts.
public class CompanyLogic: ICompanyLogic
{
private IPersonRepository _personRepo;
private ICompanyRepository _companyRepo;
public PersonService(IPersonRepository personRepo,
ICompanyRepository companyRepo)
{
_personRepo= personRepo;
_companyRepo= companyRepo;
}
public PersonModel GetPersonByName(string name)
{
return _personRepo.GetPersonByName(name);
}
public CompanyModel GetCompanyByName(string companyName)
{
return _companyRepo.GetCompanyByName(companyName);
}
}
Look at that, our logic layer no longer knows anything about databases. We have abstracted away how a person and a company are loaded. So what benefits have we gained?
- The repository interfaces are reusable. They could be used in other logic layers without changing a thing.
- Testing is a lot simpler. We mock the interface response so we can focus on testing our logic.
- Database access code for people and companies is centrally managed in one place.
- Optimizations can be made at a repository level. The interface is defined and agreed upon. The developer working on the repository can then store data how she sees fit.
Repository pattern provides us with a nice abstraction for our data. This is applicable to a variety of languages. The moral of the story is that data access should be a single responsibility interface. This interface can then be injected into business layers to add any additional logic.