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

Jon Wingfield
I've found that it's very difficult to accomplish OCP in practice, mostly because of evolutionary design. An example is having the foresight to know when the cohesion/coupling barrier has been crossed. Also, when one gains greater insight into a domain, refactoring is necessary (even crucial). but this violates OCP as well. I find that my designs are pretty crappy up front but evolve as patterns emerge. I prefer this to up-front engineering (except in some cases), because it yields a code base that is cohesive when appropriate, but also decoupled when appropriate.
2012-01-03 16:09 UTC
Agreed, OCP is hard.

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.
2012-01-03 16:42 UTC
Jon Wingfield
Not trying to spam your blog, but maybe OCP is better viewed as a means for enhancement, whereas refactoring is intended to improve the existing design. Sometimes when adding functionality, we realize that the existing design doesn't accomodate change very well, and thus we refactor (hopefully separately from implementing new functionality). I'm still a little uneasy about applying OCP when fixing bugs and especially design issues (which you already alleviated to).
2012-01-03 17:33 UTC
Mark,

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.
2012-01-03 17:55 UTC
Jon, that's well put.

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.
2012-01-03 19:32 UTC
Hadi, I'd never claim that it's possible to get things right on the first attempt. Looking at AutoFixture (again), I had to do a lot of refactoring and redesign to arrive at version 2.0, which is 'fairly' SOLID. Still, I have more changes in store for version 3.0, which indicates to me that it's still not SOLID - although it's vastly better than before.

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...
2012-01-03 19:42 UTC
You could build a whole set of tools around append-only programming; version control in particular would be a piece of cake. An editor that let you use a class or function as a starting point and then save an edited version as a new class or function (without affecting the existing version) would be quite helpful.

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.
2012-01-03 21:53 UTC
Andreas Triesch
This might seem a bit like a newbie question (well, with regards to SOLID I am one) - does that mean one should mostly use 'protected virtual' methods as opposed to 'private', in case I need something just a little different? Or does the notion I might need to do so in a particular case rather imply my class might be too large (violating SRP) and I should try to achieve flexibility by breaking it up and composing my logic via DI instead of using inheritance?
2012-01-03 22:11 UTC
Let me support you, Mark, in not having much use for ReSharper.

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.
2012-01-04 10:15 UTC
Jeff, I think that the use of a DI Container and application of the OCP are perpendicular concerns.

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.
2012-01-04 18:30 UTC
Mark, you're right. I went off on a bit of a tangent!

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.
2012-01-04 19:32 UTC
You said that you don't refactor often. Does that mean that you don't practice TDD? As I understand it, refactoring is an essential step in each TDD cycle.
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.
2012-03-17 08:51 UTC
Payman, I do practice TDD, and I do refactor regularly as part of the Red/Green/Refactor cycle.

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.
2012-03-18 14:02 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or Google Plus, or somewhere else with a permalink. Ping me with the link, and I may add it as a comment.

Published

Tuesday, 03 January 2012 14:43:47 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!