← All Posts

Getting Familiar With The Awesome Repository Pattern

Written by
Kyle Galbraith
Published on
6 March 2018
Share
The repository pattern is another abstraction, like most things in Computer Science. It is a pattern that is applicable in many different languages.
Build Docker images faster using build cache banner

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 and GetCompanyByName cannot be reused without bringing in all of CompanyLogic.

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.

© 2024 Kyle Galbraith