Notes from my Brown Bag Learning Forum Presentation. Download the source code, or just sit back and relax.
- Chain of Responsibility
- Single Responsibility Principle
- Open-Closed Principle
- Inversion of Control
- Aspect Oriented Programming
Embracing the Single Responsibility Principle, Open-Closed Principle and Inversion of Control results in trading fragile application logic for fragile configuration logic. That’s a pretty good trade.
Fragile application logic is costly and it will come back to hurt you repeatedly. It goes without saying that fragile application logic is not testable, otherwise it wouldn’t be fragile. No tests mean changes are scary, so you have to compensate by regaining an intimate understanding of all the twists and turns in order to have enough confidence to make the change. The time and mental energy it takes to work through delicate and subtle conditional logic is enormous. My mediocre brain can only manage a short call stack and juggle a handful of variables at once.
Let’s say you’re sold on the promise of pretentious acronyms like SRP, OCP, IoC and the like. So now you end up with a billion small classes and gobs of code dedicated to wiring-up your favorite inversion of control container (mine is Unity). Are we better for it? Let’s examine this trade-off by implementing the same functionality conventionally and using fancy patterns and principles.
The Scenario: Implement captcha as the first step in some business process, like a registration form.
That’s pretty easy, I don’t need anything fancy to do that.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
Fast forward 3 months and amazingly new requirements have crept into the simple captcha controller. Consider these contrived yet poignant scenarios:
- After the project goes to QA, you realize there needs to be a way to bypass the captcha check so automated Selenium tests can get to the rest of the application.
- After the project goes live, the business decides it wants to know how many users are hitting the form. So an audit log is added.
- After reviewing the audit log, it is discovered that some IPs are attacking the form, so we decide to implement a black list.
- After the black list is live, the servers begin to perform slowly, so detailed logging is added.
- The logs show the black list lookup is slow during attacks, so it is determined that caching should be implemented.
- The business is doing a television promotion and expects traffic spikes. IT wants real-time visibility to monitor the application during heavy load.
SimpleController has morphed into a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
How does this smell? Let me count the ways:
What if we had followed Uncle Bob’s SOLID principles? Our controller might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
And our original simple captcha provider could look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
If you’re wondering why we don’t have to store the captcha text in the session, it’s because we’re putting the onus on the container to give us the same instance of
SimpleCaptchaProvider each time it’s requested in the same session.
Let’s revisit the list of features that made our controller smelly and see how we could have done it open-closed style (by writing new code instead modifying old code). The go-to technique for this is the decorator pattern. So let’s make a decorator to look for a secret captcha password that our selenium test knows and let it through.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Next is the audit log, then the black list. These could be implemented as two more decorators, however we’re outgrowing this solution which means it’s time to refactor. Let’s switch hats for a few minutes and promote our decorator chain into an explicit chain of responsibility. This is like adding another lane to the freeway, it really opens things up. We’re modifying existing code so maybe you’re wondering what happened to our open-closed principle? It’s still there, I promise. The first point I’ll make is that refactoring is a special activity. It does not change the observable behavior of the application. When working with single responsibility classes, all we end up doing is adapting the logic to a different interface. In our case, we’re moving logic from
BlackListVerifyFilter. The logic stays intact and the unit tests are minimally impacted.
The end result of this refactor might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
Was it worth it? Well, we’re left with all these little classes each with their single responsibility, however we still have to wire it up. I’m not going to lie, it’s ugly and it’s fragile. So why is this design any better? It’s better because troubleshooting bad configuration is better than troubleshooting bad application logic. Bad application logic can do really bad things. In a 24/7 business-critical application, this usually happens around 3 AM and involves you waking up and trying adjust your eyes to a harsh laptop screen. With bad configuration on the other hand, whole chunks of functionality are just missing. Chances are your app won’t even start, or maybe it starts but the black list or the audit logging isn’t wired in. These issues are easy to test and when you fix them, you have enormous confidence that the functionality you just wired-in will work and continue to work in the wee hours of the morning.
The second point I’ll make is the code more closely follows the way we think about our application. This makes it easier to respond to change because new requirements are extensions of the way we already think about our application. Consider the scenario that our selenium secret passphrase is not secure enough for production and we want to add an IP restriction or signature to make sure it’s really our selenium test that is getting through. In our smelly controller, a selenium test bypass is not an explicit concept, it’s just an or-clause tacked on to the end of an already abused if statement. We’ll have to go into our smelly controller and do some thrashing around in the most important and delicate block of code. In our solid controller however, we have a nicely abstracted testable single responsibility class we can isolate our change to.
As another example, consider the scenario that our black list caching is consuming too much memory on the web server. With our SOLID design we can surgically replace our
ICacheProvider with an implementation backed by Memcached. Bits of functionality are free to evolve at their own pace. Some areas of your application will need a beefy solution and some will be just fine with a simple one. The important thing is that concerns are isolated from each other and allowed to fulfill their own destiny.
I mentioned aspect oriented programming at the beginning of the article in a shallow attempt to pique your interest. So before I wrap things up I’ll show how it fits in. Since we’re already using an IoC container and faithfully employing our SOLID design principles, we pretty much get AOP for free. This is a big deal. Software running under service level agreements and government regulations demands visibility and having aspects in your toolbox is a must. Because aspects are reusable, they are typically higher quality and more mature than something hand-rolled for a one-off scenario. And because they are bolt-on, our core business logic stays focused on our business domain.
Consider the cliché logging example. It’s overused, but works well not unlike the calculator example for unit testing or the singleton job interview question. The idea is that we tell our IoC container to apply a logging aspect to all objects it has registered. Here’s what my logging aspect produces:
DEBUG VerifyChainCaptchaProvider.Render() stream = MemoryStream, format = Jpeg DEBUG SimpleCaptchaProvider.Render() stream = MemoryStream, format = Jpeg DEBUG SimpleCaptchaProvider.Render() [72 ms] DEBUG VerifyChainCaptchaProvider.Render() [74 ms] DEBUG VerifyChainCaptchaProvider.Verify() text = afds DEBUG AuditLoggingFilter.Process() input = afds DEBUG BlackListingFilter.Process() input = afds DEBUG CachingBlackListService.IsBlocked() ip = 127.0.0.1 DEBUG CachingBlackListService.IsBlocked() > False [0 ms] DEBUG SeleniumBypassFilter.Process() input = afds DEBUG SimpleCaptchaProvider.Verify() text = afds DEBUG SimpleCaptchaProvider.Verify() > False [0 ms] DEBUG SeleniumBypassFilter.Process() > False [1 ms] DEBUG BlackListingFilter.Process() > False [4 ms] DEBUG AuditLoggingFilter.Process() > False [8 ms] DEBUG VerifyChainCaptchaProvider.Verify() > False [14 ms]
Again, this is powerful because we didn’t have to pollute our code with logging statements, yet we see quality log entries with input, output and execution time. In addition to logging, we can attach performance counters to meaningful events, add exception policies to notify us when things go wrong, selectively add caching at run-time and lots more. To see it all in action, you can download the source code for the examples used in this article.