Summer vibes are in the air. With Grug Brained Dev's book coming out and Uncle Bob discussing software design with John Ousterhout, I, too, am getting into a philosophical mood.
One problem I have been thinking about for the past several months is how to improve software maintainability, specifically, how to write code that makes common extension scenarios as simple as possible. Where's the space for new code to go?
As an aside, this is probably because at work I've been tasked with seemingly simple updates to existing old functionalities only to come up against very steep technical debt walls - Backbone JS views preclude simple field additions (never mind discovering what the controller -> json -> JS -> Backbone view event path is); existing exporters are such a tangled mess of branching and poor cohesion that adding a column breaks everything.
Putting those woes aside, the relevant SOLID principle here is Open/Closed — focused on ease of extension. And another thread I want to pull on here is the idea of The Seen and The Unseen from economics.
👁️ Making the Unseen more seen
Space in file structure
Consider this directory:
At this stage, it's still possible to guess that the intended extension path is to define a new exporter and likely inherit/include the logic from base.rb
. Unfortunately, even at this early stage we see that the base is mixed with actual implementations and will eventually disappear completely.
I don't have a perfect solution for this, but separating the base from the actual implementations (either in a higher or more nested directory), and leaving an example file could help:
Space in code
More on the Base topic, communicating the contract for dependents (I've previously talked about this in terms of specs) is also an issue. Consider this opaque base class:
class BaseClass
def process
items.sum
end
end
How to make the reliance on summable #items
clear? This is arguably a hard reality in Ruby, but defining, documenting and speccing an example method is a start:
class BaseClass
def process
items.sum
end
private
# Override this in inheritors to return the array of items to be summed.
#
# @return Array
def items
raise("Expected to be overridden")
end
end
Clean up, then tactically place tools in places where you want future building to occur. Leave the toolbox open™.
🧹 Eliminating the Unseen?
Consider this filter class code:
filter :state, as: :string, exact: true
filter :name, as: :string
def filter_by_state
collection.where(abc)
end
def filter_by_name
collection.where(xyz)
end
How do we communicate that a new filter parameter is added by adding a macro and a correspondingly named method? One idea is to combine the two steps:
filter(:state, as: :string, exact: true) do
collection.where(abc)
end
filter(:name, as: :string) do
collection.where(xyz)
end
This is a substantial increase in code cohesion in a way I believe even SRP does not discuss.
🏠 Making room
Breathing Room
Derek Prior's "Breathing Room" idea is what started me on thinking about this admittedly abstract topic. I have two examples.
The first is from Derek - define classes for domain concepts that may be absent in code.
In my experience, doing actor analysis - focusing on the many different types of users that act within a system - often reveals under-represented aspects. In my current project Element is a god-model which turns out to be servicing at least four different actor groups - engineers, supply staff, internal manufacturers, and construction site managers. Defining new Manufacturing::Element
is a way to make room.
Elbow Room
The other example comes from a quick comment by John Ousterhout that designing classes to be slightly more generic than is initially necessary has proven to be a boon.
This aligns with my experience. I think developers tend to combine the warning against premature optimisation with laziness and end up with classes that are purpose-built to the task at hand. Future additions often hit a wall because there's no space built in for growth.
An example for my current project - a model called Api::Token
where a generic Token
would have worked and created space for any other type of token.
So, having this space offers two benefits, a mental and a practical one. First — and this is profound — it makes reasoning about related problems easier. With a well-scoped domain, we deal only with appropriate concepts. Second, it keeps the code simple and easy to find and change.
Design is back in focus — and that’s a good thing. A little extra effort to leave breathing room today makes a big difference tomorrow.