The Tight Three: SRP, OCP, and IoC
Today I’m going to elaborate on a statement I made in my Inversion of Control (IoC) post:
Inversion of control is about responsibility placement. That is, to use IoC is to consider an object’s variant (or changeable) behaviors and then “invert” those behaviors making them controllable by the appropriate party, often the callee. In this statement we see a number of different considerations:
- Identifying variant and invariant behaviors
- Determining whether variant behaviors, which we could also call responsibilities, should be “inverted” or controllable by some callee.
- Allowing those behaviors to be controlled independent of the object
These three considerations are directly related to inversion of control, the single responsibility principle, and the open-closed principle:
I’ll now talk more about each of these considerations.
Identifying Variant and Invariant Behaviors
The single responsibility principle (SRP) says that an object should have only one reason to change. It’s actually fairly important to note that it does not say that an object only has one responsibility. Let me restate that so it can make it through my thick skull:
An object may have multiple responsibilities but must only ever have one reason to change.
To be honest, I completely missed that point until writing this post. Although I’ve learned how (which may be arguable to some) to define objects that have only a single responsibility and compose well, to structure my application as such at all times is the smell of Needless Complexity. Bob and Micah Martin put it this way in Agile Principles, Patterns, and Practices in C#:
An axis of change is an axis of change only if the changes occur. It is wise not to apply SRP — or any other principle, for that matter — if there is no symptom.
If a behavior is truly invariant and never going to change, then the best implementation is likely the quickest as it will cost the business the least. Quick, of course, is not the same as messy. Although we can do our best to make informed decisions about an axis of change, we’re likely going to be wrong much of the time.
Controllable Behavior & Behavior Placement
The Single Responsibility Principle (SRP) guides us to separate out behaviors based on axis of change. Understanding an axis of change allows us to design the software in a way that makes it easy to change in the expected manner. The Open/Closed Principle (OCP) guides us to package components in a way that makes them open to extension but closed for extension. Once again, here’s OCP defined:
Software entities should be open for extension but closed for modifications.
That means that given a class C, C must be extendable in the way intended without a change to its source code or package (e.g. dll, assembly, jar). When components aren’t packaged in a way that allows an axis of change to adhere to OCP, then the best bet may be to ignore that axis of change until the change actually happens… if it happens at all.
Let’s assume, for a moment, that we expect a change in some area. SRP says we should isolate that area of change, or area of responsibility, into a single component. One of the most fundamental ways that we tackle SRP and OCP is through the use of design patterns. The strategy, template method, and visitor patterns all aim to make at least one behavior variant while fixing others. Dependency injection is another way to allow a behavior to vary. Each of these is a form of inversion of control.
To summarize briefly, when designing software we must consider areas or axis of change. Some of those should be considered implicitly as we consider testability, ideally using some sort of test-driven development. Then, for those areas where it’s highly anticipated that changes will occur, we use inversion of control so that the appropriate party has responsibility over that area of change. But, that’s really what SRP says in the first place – an object should have only one reason to change, so move that behavior out. And lastly, OCP defines, to some extent, how different components that adhere to the SRP should be packaged. That is, they should be packaged in some way that allows the behavior to be extended (i.e. changed) without the corresponding source code or package needing to change. Thus, considering axis of change drives one to invert control, or apply SRP, in a way that can be maintained according to OCP. I’m not even sure it’s even possible to correctly use SRP without using IoC in one form or another — what do you think?