SOLID++: Encapsulate What Varies

Introduction: Organizing Your Tools
Ever notice how the messiest parts of your code are often where you’re handling different cases or variations? Maybe it’s different file formats, payment methods, or shipping calculations. That’s exactly what we’re going to tackle today with one of the most practical object-oriented design principles: ‘Encapsulate what varies.’
Think of it like organizing your tools. Instead of having screwdrivers and wrenches scattered throughout the shop and house, you put them all in one toolbox. Now, when you need to change or add a tool, you know exactly where to go. That’s what we mean by encapsulation in code – taking the parts that might change and putting them in a well-defined place. Okay, yes. There is that one screwdriver that always ends up in the kitchen drawer, but sometimes a little duplication is better than an unhappy spouse.
In this article, we’ll look at how this principle can transform messy, hard-to-maintain code into something clean and flexible. I’ll show you real examples in Ruby, and by the end, you’ll have a new tool for keeping your code organized and easy to change.
The Core Principle Explained
Encapsulate what varies is all about isolating the parts of your code that might change. It’s a simple idea, but it can have a big impact on how you structure your code. By putting the things that vary in one place, you make your code more flexible and easier to maintain.
Imagine you’re working on an woodworking platform that sells many types of dressers. Each dresser can be of a different type and have a different assembly process for one kind of dresser. If you scatter these variations throughout your code, or conflate them with other code you’ll end up with a tangled mess that’s hard to understand and change.
By encapsulating what varies, you can isolate these differences in one place. This makes your code easier to read, test, and extend. When you need to add a new dresser type or change the assembly logic, you know exactly where to look.
A Practical Example
🔍 In this post, we’ll use Ruby 3.1 for code examples with endless methods for brevity, but the principles here apply to any object-oriented language.
Here’s a simple example to illustrate the concept. Suppose you have a class that builds a dresser based on the dresser type. You could write a single method that handles all these variations:
class DresserBuilder
def self.construct(type)
case type
when :armoire
dresser = Armoire.new
when :highboy
dresser = Highboy.new
when :bombe
dresser = Bombe.new
else
raise ArgumentError, "Unknown dresser type: #{type}"
end
dresser.select_wood
dresser.cut_pieces
if type == :armoire
dresser.join_traditionally
else
dresser.join_with_screws
end
dresser.finish
puts "Dresser built!"
dresser
end
end
# Usage
armoire = DresserBuilder.construct(:armoire)
# => Dresser built!
This code works, but it’s not very flexible. What if you need to add a new dresser type or change the assembly process for one kind of dresser? You’d have to modify this method, which violates the Open/Closed Principle of SOLID.
Let’s apply the ’encapsulate what varies’ principle to this code. We’ll create
a separateDresserFactory
class to handle the creation of different dresser
types. This class will encapsulate the choice of dresser type and return the
appropriate dresser object:
class DresserFactory
def self.make(type)
case type
when :armoire
Armoire.new
when :highboy
Highboy.new
when :bombe
Bombe.new
else
raise ArgumentError, "Unknown dresser type: #{type}"
end
end
end
class DresserBuilder
def self.construct(type)
dresser = DresserFactory.make(type)
dresser.select_wood
dresser.cut_pieces
if type == :armoire
dresser.join_traditionally
else
dresser.join_with_screws
end
dresser.finish
puts "Dresser built!"
dresser
end
end
# Usage
armoire = DresserBuilder.construct(:armoire)
# => Dresser built!
Now we can easily add new types without modifying the Dresser
class. This is
better, but what if the assembly process varies between dresser types? You’d
have to modify the Dresser
class again.
Strategy Pattern
Let’s take it a step further and use the Strategy pattern to encapsulate the
assembly process in separate classes. This way, each dresser type can have its
own assembly logic, and we can easily change that without changing the
Dresser
class:
class DresserFactory
BLUEPRINTS = {
armoire: Armoire,
highboy: Highboy,
bombe: Bombe
}.freeze
def self.make(type)
BLUEPRINTS.fetch(type) {
raise ArgumentError, "Unknown dresser type: #{type}"
}.new
end
end
module Assembleable
def assemble = raise NotImplementedError
end
class AbstractDresser
include Assembleable
end
class Armoire < AbstractDresser
def assemble
select_wood
cut_pieces
join_traditionally
finish_with_lacquer
end
private
def select_wood = puts "Selecting wood for armoire"
def cut_pieces = puts "Cutting pieces for armoire"
def join_traditionally = puts "Assembling armoire traditionally"
def finish_with_lacquer = puts "Finishing armoire with lacquer"
end
class DresserBuilder
def self.construct(type)
dresser = DresserFactory.make(type)
dresser.assemble
puts "Dresser built!"
dresser
end
end
# Usage
armoire = DresserBuilder.construct(:armoire)
# => Selecting wood for armoire
# => Cutting pieces for armoire
# => Assembling armoire traditionally
# => Finishing armoire with lacquer
# => Dresser built!
Now, each dresser type has its own class, and the Dresser
class delegates the
assembly process to the appropriate type. We’ve also taken this opportunity to
update the DresserFactory
class to trade the case
statement for a hash
of blueprints for each dresser type. This way, adding a new type is as simple
as adding a new entry to the hash.
Common Patterns That Apply This Principle
The ’encapsulate what varies’ principle is closely related to several design patterns that help you manage change in your code. Above, we used the Strategy pattern to encapsulate the assembly process for different dresser types and the Factory Method pattern to create instances of these types.
Tips for Identifying Varying Elements
When applying the ’encapsulate what varies’ principle, it’s essential to identify the parts of your code that are likely to change. Here are some tips to help you spot these elements:
- Look for conditional logic: If you have a lot of
if
statements orcase
blocks that handle different cases, those are good candidates for encapsulation. - Think about future requirements: Consider what might change in the future and how you can isolate those changes.
- Separate concerns: If you have different concerns mixed together in one class or method, try to separate them into distinct components.
- Code analysis: Use tools like static code analysis or code reviews to identify parts of your code that change frequently.
Common Pitfalls
While the ’encapsulate what varies’ principle is a powerful tool for managing change, there are some common pitfalls to watch out for:
- Overengineering: Don’t go overboard and create unnecessary abstractions. Keep it simple and refactor only when it makes sense.
- Premature optimization: Don’t try to predict every possible change upfront.
- Ignoring SOLID principles: Remember that encapsulation is just one part of good object-oriented design. Make sure your code follows other principles like the Single Responsibility Principle and the Open/Closed Principle.
- Not testing variations: When you encapsulate what varies, make sure to test each variation thoroughly. It’s easy to introduce bugs when you refactor code, so testing is crucial.
Keep these pitfalls in mind as you apply this principle in your code. With practice, you’ll get better at identifying the parts that might change and encapsulating them effectively.
Conclusion
The ’encapsulate what varies’ principle is a powerful tool for managing change in your code. By isolating the parts that are likely change, you make your code more flexible, maintainable, and testable. Whether you’re working on an e-commerce platform, a game engine, or a woodworking platform, this principle can help you keep your code organized and easy to extend.
And yes, sometimes that screwdriver will still end up in the kitchen drawer, but sometimes that’s just where it needs to be to maintain household harmony.
🔍 This post is part of the SOLID++ series where we are exploring principles of object-oriented design beyond the five SOLID principles.