Objects responsible for too many things are a problem. Because their complexity is high, they are difficult to maintain and extend. Decomposition of responsibility is what we do in order to break these overly complex objects into smaller ones. I see two types of this refactoring operation: vertical and horizontal. And I believe the former is better than the latter.
Let’s say this is our code (it is Ruby):
class Log
def initialize(path)
@file = IO.new(path, 'a')
end
def put(text)
line = Time.now.strftime("%d/%m/%Y %H:%M ") + text
@file.puts line
end
end
Obviously, objects of this class are doing too much. They save log lines to the file and also format them—an obvious violation of a famous single responsibility principle. An object of this class would be responsible for too many things. We have to extract some functionality out of it and put that into another object(s). We have to decompose its responsibility. No matter where we put it, this is how the Log
class will look after the extraction:
class Log
def initialize(path)
@file = IO.new(path, 'a')
end
def put(line)
@file.puts line
end
end
Now it only saves lines to the file, which is perfect. The class is cohesive and small. Let’s make an instance of it:
log = Log.new('/tmp/log.txt')
Next, where do we put the lines with formatting functionality that were just extracted? There are two approaches to decompose responsibility: horizontal and vertical. This one is horizontal:
class Line
def initialize(text)
@line = text
end
def to_s
Time.now.strftime("%d/%m/%Y %H:%M ") + text
end
end
In order to use Log
and Line
together, we have to do this:
log.put(Line.new("Hello, world"))
See why it’s horizontal? Because this script sees them both. They both are on the same level of visibility. We will always have to communicate with both of them when we want to log a line. Both objects of Log
and Line
are in front of us. We have to deal with two classes in order to log a line:
To the contrary, this decomposition of responsibility is vertical:
class TimedLog
def initialize(log)
@origin = log
end
def put(text)
@origin.put(Time.now.strftime("%d/%m/%Y %H:%M ") + text)
end
end
Class TimedLog
is a decorator, and this is how we use them together:
log = TimedLog.new(log)
Now, we just put a line in the log:
log.put("Hello, world")
The responsibility is decomposed vertically. We still have one entry point into the log
object, but the object “consists” of two objects, one wrapped into another:
In general, I think horizontal decomposition of responsibility is a bad idea, while vertical is a much better one. That’s because a vertically decomposed object decreases complexity, while a horizontally decomposed one actually makes things more complex because its clients have to deal with more dependencies and more points of contact.