Weekly Dev Tips
Applying Pain Driven Development to Patterns
Applying Pain Driven Development to Patterns
This week we talk about specific ways you can apply my strategy of Pain Driven Development to the use of design patterns. This is an excerpt from my Design Pattern Mastery presentation that goes into more detail on design patterns.
Sponsor - DevIQ
Thanks to DevIQ for sponsoring this episode! Check out their list of available courses and how-to videos.
Show Notes / Transcript
I talked about Pain Driven Development, or PDD, in episode 10 - check out that episode first if you're not familiar with the practice. I've recently been focusing a bit on some design patterns. An easy trap to fall into with design patterns is trying to apply them too frequently or too soon. PDD suggests waiting to experience pain while trying to work with the application's current design before you attempt to refactor to improve its design by applying a design pattern. In this tip, I'll walk through a few common steps where applying a specific pattern may be helpful.
To begin, let's assume we have a very simple web application. Let's say it's using MVC, and there's a controller that needs to be used to return some data fetched from a database. It could be an API endpoint or a view-based page - the UI format isn't important in this case. The absolute simplest thing you can do in this situation is hard code your data access code into your controller. So, assuming you're using ASP.NET Core and Entity Framework Core, you could instantiate an DbContext
in the controller and use that to fetch the data. This works and meets the immediate requirement, so you ship this version.
A little bit later, your application has grown more complex. You have some filters that also use data, along with other services. You start to notice occasional bugs from EF and realize that you've introduced a bug. By instantiating a new DbContext
in each controller, but occasionally passing around entities between parts of the application, EF gets in a state where entities are tracked by one instance but you're trying to operate on them with another instance of DbContext
. You need to use a single EF Core DbContext
per web request, which is to say it should have a "Scoped" lifetime. Fortunately, ASP.NET Core makes it very easy to achieve this by configuring your DbContext
inside of ConfigureServices
. In fact, if you don't read the docs, you probably don't even know what lifetime EF Core is using, because it's hidden within an extension method. In any case, once you configure DbContext
in ConfigureServices, you need a way to get it into your Controller(s). To do this requires the Strategy pattern, covered in episode 19. If you're familiar with dependency injection, you've used the Strategy pattern. Add a constructor to your Controller, pass in the DbContext
, and set a private local field with the value passed into the constructor. Do this anywhere you're otherwise newing up the DbContext
. Remind yourself 'new is glue'. You just fixed an issue with too tight of coupling to the instantiation process by using the service collection built into ASP.NET Core, an IOC container, essentially a factory on steroids. Your EF Core lifetime bug is now fixed, so you ship the code.
Some more time passes, the application has grown, and now there are a bunch of controllers and other places that all have DbContext
injected into them. You've noticed some duplication in how code works with the DbContext
. You've also found that it's tough to unit test your classes that have a real DbContext injected, except by configuring EF Core to use its In Memory data store. This works, but you'd prefer it if your unit tests truly had no dependencies so you could just test behavior, not low-level data access libraries. You decide that you can solve both of these problems by introducing the Repository pattern, which is just a fancy name for an abstraction used to encapsulate the low level details of your data access. You create a few such interfaces, implement them with DbContext
, and make sure your Controllers and other classes that were directly using DbContext
now have an interface injected instead. Along the way you fix a couple of bugs you discovered that had grown due to duplicate code that had evolved differently, but which should have remained consistent. When you're done, the only types that know about DbContext
directly are your concrete Repository implementations.
Your application is growing more popular now, and some of the pages are really hammering the database. Their data doesn't change very often, so you decide to add some caching. Initially you start putting the caching logic directly in your data access code in your repository implementations that use EF Core, but you quickly find that there is a lot of duplication and your once-simple repositories are now growing cluttered with a lot of caching logic. What's more changing the details of what is cached how is requiring you to touch and re-touch the repository types again and again. Your current approach is obviously violating both the Single Responsibility and Open-Closed principles, two of the SOLID principles. You recognize that you can apply the Decorator (or Proxy) pattern by moving the caching logic into a CachedRepository type, which you can choose when and where to use on a per-entity basis simply by adjusting the type mapping in your application's ConfigureServices
method. With this in place, you're able to quickly apply caching where appropriate, and ship a better performing version of your application.
Over time, as you built out your repositories, you kept basic methods for creating, reading, updating, and deleting entities in one place. Maybe you implemented a generic repository, or used a base class. You were careful not to expose IQueryable
interfaces from your Repositories, so their query details didn't leak throughout your application. However, to support many different kinds of queries, with different filters and including different amounts of data from related types, you found that you needed to add many additional methods and overloads. In addition to a simple List method on your Order repository, you needed ListByCustomer, ListByProduct, ListByCompany, not to mention ListWithOrderDetails and other variations. Some of your repositories were growing quite large, and included quite a bit of complex query logic, which wasn't always easy to reuse even between methods in the same repository. To address this pain, you applied the Specification pattern, which treats each unique query as its own type. Using this approach, you were able to create specifications like OrdersByCustomer, OrdersByProduct, and OrdersByCompany which included the appropriate OrderDetails if desired, or included an option to specify whether to include it. Your Repository implementations dropped down to just simple CRUD methods, with the List method now taking in a Specification as a parameter.
Hopefully this helps you see how you can recognize a certain kind of pain, and respond to that pain by refactoring to use a specific design pattern. If you keep your code clean and simple, it's fairly easy to do this kind of refactoring as you need it, so there's no need to try and use every pattern you know speculatively as you begin a project.