Say what, not how
Encapsulations and Refactoring
What does “encapsulation” mean? A workable definition could be:
💡 To put what's on the outside on the inside.
What does “refactoring” mean? A workable definition could be:
💡 Restructuring software to improve it, usually without changing its outward functionality.
Developers are familiar with a kind of “general refactoring” where we try to break up chunky functions or classes into smaller parts.
It’s this kind of refactoring we’ll explore. To think a bit deeper about why we do this kind of refactoring so that we can do it wittingly.
The Three Spirits of Encapsulation
There are two primary ways to do encapsulation in most modern languages:
- Functions
- Classes
We won’t get into their differences right now.
Currently we’re interested in an important way in which they are similar:
💡 They both let you put stuff that's outside, on the inside.
And both have three primary aspects:
- An overall responsibility or purpose
- A strategy used to fulfill that purpose
- A specific implementation of that strategy
That a function or class has a purpose is straight-forward enough!
But that it has both a strategy and an implementation of that strategy, as separate things; that’s more interesting.
Strategy and Implementation
What makes a strategy and an implementation of that strategy different things?
Here’s a way to think about it:
💡 For any strategy, there are multiple implementations of that strategy
So, they’re not necessarily the same thing.
For trivial cases, they can be — but trivial cases don’t cause us trouble.
Understanding Strategy
Consider this:
💡 No implementation of the wrong strategy is helpful.
You must first have a strategy which accomplishes the right goal, that fulfills a component’s responsibility.
If we have the wrong strategy, the correctness of its implementation is a red herring.
Understanding Implementation
Consider this:
💡 To judge the strategy employed by some code, you must first understand its implementation.
Given some encapsulation, knowing its responsibility, we routinely need to determine:
- Are we taking the right strategy to fulfill the component responsibility?
- Does the implementation correctly carry out the strategy?
Yet, we can only know which strategy is even employed by first working to understand the implementation. And we can only know if the implementation is correctly carrying out the strategy, once we know what strategy its trying to take.
The implementation, the code as written, is by far the largest barrier between us and an understanding of our software.
Expressivity
Expressivity is one of those terms that gets thrown around a lot and everyone has their own definition.
But let’s just consider a very particular form of expressivity:
💡 How readily some code says what its strategy is.
Writing Expressive Code
How can we address the tension between strategy and implementation?
How can we write code that says what its strategy is, without the implementation getting in our way?
Distillation
If the strategy is “what is done” and implementation is “how it is done”, then distillation is:
💡 Reducing the amount of detail regarding the how of what is done.
Luckily, our programs are not limited to a single component.
Delegation
We can achieve the distillation we’re after through delegation:
💡 Using some subordinate encapsulation to do something
By throwing those implementation details into some secondary encapsulation we can then delegate to it.
By delegating to the encapsulation we replace the implementation details with a call to that delegate.
Assuming we have good naming convention and style, what is left over is an artifact saying what we’re doing but not how.
The “how” has been hidden inside the delegate encapsulation until needed.
Squinting
How do we know which bits of implementation to group together and delegate out into an encapsulation?
Well, in general, we want to capture the very highest-level description of the local strategy.
However, you can take this too far…
If you’re trying to tell someone how to draw an owl, giving them the single step of “1. draw the owl” isn’t very helpful. It’s too coarse of a description.
At the opposite end of the spectrum we have the implementation itself; all the atomic steps needed to carry out the strategy.
What we want is something that approaches the former without reaching it. We want to express the minimal number of steps but enough to actually differentiate this strategy from the alternatives.
Unfortunately I know of no generalized method for deriving what level of description best conveys the nature of a given strategy.
You’ll just have to squint, heh.
Summary
In this chapter we explored:
- The nature of encapsulations
- What makes code expressive
- The tension between strategy and implementation
- How to distill an implementation using delegation
In distilling our implementations, by delegating the “how”, we leave only the salts of the “what” remaining.
This allows us to more readily understand what the components in our software are trying to do so we can judge whether they are:
- Even trying to do the right thing
- And that they’re actually doing it
In the next chapter, we’ll deal with how our components acquire their delegates.