SOLID is Append-only by Mark Seemann
SOLID is a set of principles that, if applied consistently, has some surprising effect on code. In a previous post I provided a sketch of what it means to meticulously apply the Single Responsibility Principle. In this article I will describe what happens when you follow the Open/Closed Principle (OCP) to its logical conclusion.
In case a refresher is required, the OCP states that a class should be open for extension, but closed for modification. It seems to me that people often forget the second part. What does it mean?
It means that once implemented, you shouldn't touch that piece of code ever again (unless you need to correct a bug).
Then how can new functionality be added to a code base? This is still possible through either inheritance or polymorphic recomposition. Since the L in SOLID signifies the Liskov Substitution Principle, SOLID code tends to be based on loosely coupled code composed into an application through copious use of interfaces - basically, Strategies injected into other Strategies and so on (also due to Dependency Inversion Principle). In order to add functionality, you can create new implementations of these interfaces and redefine the application's Composition Root. Perhaps you'd be wrapping existing functionality in a Decorator or adding it to a Composite.
Once in a while, you'll stop using an old implementation of an interface. Should you then delete this implementation? What would be the point? At a certain point in time, this implementation was valuable. Maybe it will become valuable again. Leaving it as an potential building block seems a better choice.
Thus, if we think about working with code as a CRUD endeavor, SOLID code can be Created and Read, but never Updated or Deleted. In other words, true SOLID code is append-only code.
Example: Changing AutoFixture's Number Generation Algorithm #
In early 2011 an issue was reported for AutoFixture: Anonymous numbers were created in monotonically increasing sequences, but with separate sequences for each number type:
integers: 1, 2, 3, 4, 5, …
decimals: 1.0, 2.0, 3.0, 4.0, 5.0, …
and so on. However, the person reporting the issue thought it made more sense if all numbers shared a single sequence. After thinking about it a little while, I agreed.
Because the AutoFixture code base is fairly SOLID we decided to leave the old implementations in place and implement the new behavior in new classes.
The old behavior was composed from a set of ISpecimenBuilders. As an example, integers were generated by this class:
public class Int32SequenceGenerator : ISpecimenBuilder { private int i; public int CreateAnonymous() { return Interlocked.Increment(ref this.i); } public object Create(object request, ISpecimenContext context) { if (request != typeof(int)) { return new NoSpecimen(request); } return this.CreateAnonymous(); } }
Similar implementations generated decimals, floats, doubles, etc. Instead of modifying any of these classes, we left them in the code base and created a new ISpecimenBuilder that generates all numbers from a single sequence:
public class NumericSequenceGenerator : ISpecimenBuilder { private int value; public object Create(object request, ISpecimenContext context) { var type = request as Type; if (type == null) return new NoSpecimen(request); return this.CreateNumericSpecimen(type); } private object CreateNumericSpecimen(Type request) { var typeCode = Type.GetTypeCode(request); switch (typeCode) { case TypeCode.Byte: return (byte)this.GetNextNumber(); case TypeCode.Decimal: return (decimal)this.GetNextNumber(); case TypeCode.Double: return (double)this.GetNextNumber(); case TypeCode.Int16: return (short)this.GetNextNumber(); case TypeCode.Int32: return this.GetNextNumber(); case TypeCode.Int64: return (long)this.GetNextNumber(); case TypeCode.SByte: return (sbyte)this.GetNextNumber(); case TypeCode.Single: return (float)this.GetNextNumber(); case TypeCode.UInt16: return (ushort)this.GetNextNumber(); case TypeCode.UInt32: return (uint)this.GetNextNumber(); case TypeCode.UInt64: return (ulong)this.GetNextNumber(); default: return new NoSpecimen(request); } } private int GetNextNumber() { return Interlocked.Increment(ref this.value); } }
Adding a new class in itself has no effect, so in order to recompose the default behavior of AutoFixture, we changed a class called DefaultPrimitiveBuilders by removing the old ISpecimenBuilders like Int32SequenceGenerator and instead adding NumericSequenceGenerator:
yield return new StringGenerator(() => Guid.NewGuid()); yield return new ConstrainedStringGenerator(); yield return new StringSeedRelay(); yield return new NumericSequenceGenerator(); yield return new CharSequenceGenerator(); yield return new RangedNumberGenerator(); // even more builders...
NumericSequenceGenerator is the fourth class being yielded here. Before we added NumericSequenceGenerator, this class instead yielded Int32SequenceGenerator and similar classes. These were removed.
The DefaultPrimitiveBuilders class is part of AutoFixture's default Facade and is the closest we get to a Composition Root for the library. Recomposing this Facade enabled us to change the behavior of AutoFixture without modifying (other) existing classes.
As Enrico (who implemented this change) points out, the beauty is that the previous behavior is still in the box, and all it takes is a single method call to bring it back:
var fixture = new Fixture().Customize( new NumericSequencePerTypeCustomization());
The only class we had to modify was the DefaultPrimitiveBuilders, which is where the object graph is composed. In applications this corresponds to the Composition Root, so even in the face of SOLID code, you still need to modify the Composition Root in order to recompose the application. However, use of a good DI Container and a strong set of conventions can do much to minimize the required editing of such a class.
SOLID versus Refactoring #
SOLID is a goal I strive towards in the way I write code and design APIs, but I don't think I've ever written a significant code base which is perfectly SOLID. While I consider AutoFixture a ‘fairly' SOLID code base, it's not perfect, and I'm currently performing some design work in order to change some abstractions for version 3.0. This will require changing some of the existing types and thereby violating the OCP.
It's worth noting that as long as you can stick with the OCP you can avoid introducing breaking changes. A breaking change is also an OCP violation, so adhering to the OCP is more than just an academic exercise - particularly if you write reusable libraries.
Still, while none of my code is perfect and I occasionally have to refactor, I don't refactor much. By definition, refactoring means violating the OCP, and while I have nothing against refactoring code when it's required, I much prefer putting myself in a situation where it's rarely necessary in the first place.
I've often been derided for my lack of use of Resharper. When replying that I have little use for Resharper because I write SOLID code and thus don't do much refactoring, I've been ridiculed for being totally clueless. People don't realize the intimate relationship between SOLID and refactoring. I hope this post has helped highlight that connection.
Comments
However, adhering to OCP doesn't indicate that you have to do BDUF. What it means is that once you get a 'rush of insight' (as Domain-Driven Design puts it) you don't modify existing classes. Instead, you introduce new classes to follow the new model.
This may seem wasteful, but due to the very fine-grained nature of SOLID code, it means that those classes that follow the old model (that you've just realized can be improved) are basically 'wrong' because they model the domain in the 'wrong' way. Re-implementing that part of the application's behavior while leaving the old code in place is typically more efficient because it's only going to be a very small part of the code base (again due to the granularity) and because you can do it in micro-iterations since you're not changing anything. Thus, dangerous 'big-bang' refactorings are avoided.
In any case, I never said that SOLID is easy. What I'm saying is that SOLID code has certain characteristics, and if a code base doesn't exhibit those characteristics, it's not (perfectly) SOLID. In reality, I expect that there are very few code bases that can live up to what is essentially an ideal more than a practically attainable goal.
Irrelevant of my association with ReSharper, I think that refactoring (be it with or without tools) and SOLID design are not mutually exclusive. You are basing your argument mostly on the premise that we get things right the first and that is not always the case. Test Driven Development in itself for instance is about evolving design.
As for ReSharper not being needed (or replace ReSharper with any other enhancing tool), I find it kind of amusing because it seems that there is some imaginary line that developers draw whereby what's included in Visual Studio is sufficient when it comes to refactoring. Everything else is superflous. That is of course until the next version of Visual Studio includes it. And that's if we think about these types of tools as refactoring only, which is so far from the truth.
Btw, switch statement violates OCP and yes it doesn't change until it does change. I'd add that normally when I violate OCP I try and make sure the tests are in pace to let me know if something breaks.
When it comes to fixing bugs, the OCP specifically states that it's OK to modify existing code, so you shouldn't be uneasy about that.
Still, instead of refactoring, sometimes it makes more sense to leave the old stuff to atrophy and build the new, better API up around it. That's basically the Strangler pattern applied at the code level.
That said, there are some of the refactorings that ReSharper has that I'd love to have in my IDE. It's just that I think that already VS is too slow and heavy on my machine - even without ReSharper...
I tried an extremely SOLID design for a prototype recently, with very few stable dependencies and leaning on the container for almost everything. I didn't quite adhere to OCP as you've described it here, but in retrospect it would have almost eliminated regressions. There was a lot of pushback to the design as soon as we got another developer on (just before we threw away the prototype), though.
The usual complaints about being unable to see the big picture (due to container registrations being made mostly linearly) came through, and my choice to compose functions rather than objects certainly didn't help as it resulted in some quite foreign-looking code. I think tooling could have helped, but we've decided to stick to KISS and stabilise more dependencies for the upcoming releases.
On the subject of tooling, I think something that's missing from DI tooling is a graphical designer containing a tree view of what your container will resolve at runtime, with markers for missing dependencies and such. A "DIML" file, perhaps, that generates a .diml.cs or .diml.vb when saved. Then you could have a find-and-replace-style feature for replacing dependencies, respecting OCP.
Event though I´m using it, I´m not using it much for refactoring. Out of all the refactorings I use maybe just 2-3 (rename, extract method, move class to separate file).
My main use for ReSharper is as a test runner.
So I agree: ReSharper is a tool for developers wrestling with tons of legacy code they need to refactor. But if your code base is clean... then the need for larger rearrangements is rare. "Refactoring to deeper insight" sometimes requires such rearrangements. But this too need not be that hard, if the functional units are fine grained.
Andreas, the 'old' definition of OCP (Meyer's) is to use inheritance to extend an existing class. However, when we favor composition over inheritance, the default way to extend a class is to apply a Decorator to it.
Ralf, I think you've nailed it. The reason why I've never felt much need for ReSharper is because I avoid legacy code like the plague. In fact, I've turned down job offers (that payed better than anything I've ever received) because it involved dealing with too much legacy code. If I were ever to deal substantially with legacy code, I might very well install ReSharper.
I guess the only relation that I was riffing off is that a great tool for writing and composing SOLID code would help with both.
Refactoring aside, it seems to me that TDD practice makes you violate OCP since you start with the simplest implementation and keep improving it (hence changing existing code) to make new tests pass.
What I meant (but perhaps did not explicitly state) was that once a piece of code is released to production, it changes status. That kind of code I don't often refactor.