Refactoring From Inheritance To Composition To Data
“Prefer composition over inheritance” is a popular saying amongst programmers. In this article, I want to demonstrate what that looks like.
We’re going to refactor some Rails classes from inheritance to composition. Then we’ll refactor again from composition to something even better: data.
Inheritance (The Original Code)
In an attempt to show “real” code, I’ve taken this example from the Rails codebase.
These classes originally came from action_view/helpers/tags
.
However, I have simplified the code by removing some of the unrelated parts.
This implementation starts with DatetimeField
:
class DatetimeField
def render(date)
value = format_date(date)
type = self.class.field_type
"<input type='#{type}' value='#{value}' />"
end
def format_date(date)
raise NotImplementedError
end
def self.field_type
'datetime'
end
end
This class is used to render HTML input
tags that contain dates/times, for example:
DatetimeField.new.render(Time.now)
#=> "<input type='datetime' value='2017-06-26T20:28:20' />"
The above code will not work, however, because DatetimeField
is an abstract base class.
Notice the NotImplementedError
raised in format_date
.
This class is designed to be inherited from, and the subclasses must override format_date
.
There are four subclasses that inherit from DatetimeField
:
class DatetimeLocalField < DatetimeField
def self.field_type
"datetime-local"
end
def format_date(value)
value.try(:strftime, "%Y-%m-%dT%T")
end
end
class DateField < DatetimeField
def format_date(value)
value.try(:strftime, "%Y-%m-%d")
end
end
class MonthField < DatetimeField
def format_date(value)
value.try(:strftime, "%Y-%m")
end
end
class WeekField < DatetimeField
def format_date(value)
value.try(:strftime, "%Y-W%V")
end
end
So the output of render
depends on which subclass is being used.
This is essentially the template method design pattern.
Refactoring To Composition
This refactoring involves separating the base class from all the subclasses. Each subclass will be turned into a collaborator – a separate object that the base class uses. Where there used to be a single object, there will now be two different objects.
The first question to ask is: how does the base class use the subclasses?
- The base class calls
format_date
to turn a date into a string. - The base class allows subclasses to override the default
field_type
using a class method.
This will be the interface between the two objects.
Secondly, we need a name for these collaborator objects. It appears to me that the subclasses are date formatters, not date fields, so let’s call these collaborators “formatters”.
If we were using a statically-typed language, this is the point where we would define the interface. For example, this is what the interface would look like in Swift:
protocol DatetimeFieldFormatter {
func format(date: DateTime) -> String
var fieldType: String { get }
}
But this is Ruby, and we don’t have interfaces, we have duck types instead. A duck type is a kind of implicit interface. We still have to conform to the interface by implementing all the expected methods, but the expected methods haven’t been declared with code.
Knowing the interface, we can refactor the base class to use a collaborator called formatter
:
class DatetimeField
attr_reader :formatter
def initialize(formatter)
@formatter = formatter
end
def render(date)
value = formatter.format_date(date)
type = formatter.field_type || 'datetime'
"<input type='#{type}' value='#{value}' />"
end
end
The collaborator is provided as an argument to initialize
, and stored in an instance variable.
Later on, it is used to format dates, and possibly override the tag type.
This is an example of dependency injection.
Now lets refactor all the subclasses into classes that implement the formatter interface:
class DatetimeLocalFormatter
def format_date(value)
value.try(:strftime, "%Y-%m-%dT%T")
end
def field_type
"datetime-local"
end
end
class DateFormatter
def format_date(value)
value.try(:strftime, "%Y-%m-%d")
end
def field_type
nil
end
end
class MonthFormatter
def format_date(value)
value.try(:strftime, "%Y-%m")
end
def field_type
nil
end
end
class WeekFormatter
def format_date(value)
value.try(:strftime, "%Y-W%V")
end
def field_type
nil
end
end
Notice how all the classes have been renamed to Formatter
, and they don’t inherit from anything.
Now rendering the output is done like this:
DatetimeField.new(WeekFormatter.new).render(Time.now)
#=> "<input type='datetime' value='2017-06' />"
Bonus Refactor: Singletons
Let’s take a short detour into functional programming town.
The formatter classes don’t have any instance variables. To put it another way, they contain only pure functions and no state. All instances of the same class will be identical, so we only ever need to have one. This is why they don’t need to be classes.
In this situation, I prefer to use singleton modules:
module WeekFormatter
extend self
def format_date(value)
value.try(:strftime, "%Y-W%V")
end
def field_type
nil
end
end
Now WeekFormatter
is an object that contains all the methods, and you don’t ever need to use new
.
It is essentially a namespace of functions.
End of detour.
Refactoring To Data
Savvy readers may have noticed that the refactored formatter objects don’t contain much code.
There is duplication too, because every formatter contains value.try(:strftime, ...)
.
In fact, only two strings are differ between each formatter class: the date format string and the field_type
string.
When a class contains data, but doesn’t contain any behaviour, it doesn’t need to be a class.
It could just be a Hash
instead.
If we turn all the formatter classes into plain old Hash
s,
and pull all the behaviour up into the DatetimeField
class,
it would look like this:
class DatetimeField
FORMATS = {
local: { strftime_format: "%Y-%m-%dT%T", field_type: "datetime-local" },
date: { strftime_format: "%Y-%m-%d" },
month: { strftime_format: "%Y-%m" },
week: { strftime_format: "%Y-W%V" },
}
attr_reader :format
def initialize(format_key)
@format = FORMATS.fetch(format_key)
end
def render(date)
value = date.try(:strftime, format.fetch(:strftime_format))
type = format.fetch(:field_type, 'datetime')
"<input type='#{type}' value='#{value}' />"
end
end
Rendering the output is done like this:
DatetimeField.new(:week).render(Time.now)
#=> "<input type='datetime' value='2017-06' />"
This is the best implementation, in my opinion. Five classes were reduced to a single class. It requires less code. Everything is closer together, instead of being spread across multiple files.
Not to be confused with domain-driven design (DDD).
This is an example of what I call “data-driven design” or just “thinking in data”. All the specific details are described in data: numbers, strings, arrays, hashes, structs, etc. All the behaviour (i.e. the implementation) is generic, and controlled by the data.
I really like this approach. You can use it any situation where the details can be represented as pure data, with no behaviour. But when different cases require significantly different behaviour, collaborator objects are the better choice.
Got questions? Comments? Milk?
Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).