It’s good to be SOLID. Of the principles that make up SOLID, I’ve found the Open/Closed Principle (OCP for short) to be one of the more difficult. There are plenty of explanations around the web, but while identifying the presence or lack open/closed is easy, it’s tougher to achieve.
I also use OCP from the micro to macro scale. It can be applied to classes or it can be applied at a module/library level. Most material on the web covers the easier class level OCP. I’d like to cover the latter using some changes made recently to the Moe state machine library.
On my current project, I encountered a great place to use a state machine to simplify a bunch of code. The usage of the state machine itself was interesting and I hope to post about it soon. From a Moe standpoint, I found that the way state actions were implemented didn’t suit my needs. First, I didn’t want to centralize enter/exit actions, particularly at the time of construction. Beforehand, you would add entry and exit actions when you built your state machine. This would mean they were all in a single chunk of code, or you would have to pass the builder around. Not ideal. Second, I also wanted to swap out these actions on demand. When the state machine wasn’t in use, I wanted to disconnect state actions so windows didn’t appear based on wayward events (ie, a late arriving error event from a webcam we were using).
I revisited Moe to add the functionality I wanted. What I envisioned was a convention for state actions. Any class could have methods following the form “OnEnter” and “OnExit”. These methods would auto-magically be invoked upon entering and exiting states. These classes could also be added or removed as listeners at any time. I decided to call this functionality state delegates.
While working on Moe previously, I had done a lot of work to support the OCP and this can be seen in the Moe.StateMachine.Extensions library. In that library you can find several helpful additions for Moe, such as logging, state watchers, timers, and asynchronous state machine execution. I specifically wanted that functionality to exist outside of the core state machine. Would the hard work of that refactoring pay off and allow me to add state delegates?
You can see the changes that were made in this commit. The solution was to add a traffic cop that would wire up to every state and intercept state enter/exits. When one of those happened, the StateDelegatePlugIn would run through the current listeners and delegate to any methods matching the state name.
This huge addition to functionality (in value, that is) was achieved without touching the state machine at all. This is the OCP on a larger scale. We’ve added valuable functionality to a library without modifying the library itself.
How to move towards OCP
It’s tough, if not impossible, to produce code supporting the OCP from the beginning. Before we can abstract, we need to SEE at least a few examples of usage first. If you were to go back and look at past commits, you’d find several that are focused on refactoring to the OCP after several functional additions were already in place. To begin with, the functional additions were intertwined with the state machine itself. By unwinding those dependencies, I was achieving OCP through actual usage. It would have been too much for my brain to try and see the resulting patterns and needs without already having multiple examples.
When I do something like this, I’m looking to make core classes as simple as possible, keeping as close to the Single Responsibility Principle as I can. Something that I should have done earlier was introduce the Extensions library. By forcing myself to move functional additions to a separate assembly, I would have no choice but to support OCP. When thinking of simplifying or SRP, think of the grand Unix commands. Grep, sed, sort, and so on. Have your components do one thing and do it well.