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;
var type = request as Type;
if (type == null)
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:
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.
Remember Me
a@href@title, b, em, i, strike, strong
Page rendered at Thursday, February 23, 2012 4:59:52 AM (Romance Standard Time, UTC+01:00)
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.