Moving Sideways with Lateral Abstractions?
When I'm doing design and refactoring, part of me worries that I'm just creating a mess of objects and classes. One of the kinds of mess that I feel I make the most are lateral abstractions. What I mean by lateral abstraction is an abstraction that is at the same level as the code I abstracted it out of. It doesn't add very much to the conversation, and it doesn't introduce a finer vocabulary than was already there. When looking over the code I wrote in Writing a Better Code Narrative, I get a slight feeling in the back of my head that some of that code is lateral abstraction.
Without re-posting all of the code (which can be found here), I started with a
Early on while reading GOOS, the authors have an aside where the talk about the classic topics of coupling and cohesion. In that aside, they also state:
Here they almost1 define a new concept that, honestly, should be an equal to coupling and cohesion: coherence, the idea that a class should represent not just a single responsibility, but all of that single responsibility. This is the Single Responsibility Principle from the other end. A class should have one responsibility, not two, and not one-half.
Now we can circle around to lateral abstractions again. Some lateral abstractions encapsulate a whole and complete idea that just happens to sit at the same level of abstraction. We can think of a file and a network socket as being at the same level, since they are both I/O resources, but they represent different ideas. Other lateral abstractions, though, disrupt coherence.
The
The
or...
The
To be honest, I can't say exactly which is the best answer here. I went with the
1 Instead, they lump it in with cohesion.
Without re-posting all of the code (which can be found here), I started with a
CommandParser
class that read a string of character commands and "executed" those commands to produce a new output string. The original code was intentionally hairy, but I refactored it down into a series of Command
classes to encapsulate the individual behaviors, and a CommandFactory
to encapsulate the algorithm to map character commands to Command
objects. The Command
class provided a nice abstraction over the individual algorithms, but I worried a bit that the CommandFactory
was a lateral abstraction. How is a CommandFactory
really different from a CommandParser
? Isn't a CommandParser
's job to map character commands to Command
objects?Early on while reading GOOS, the authors have an aside where the talk about the classic topics of coupling and cohesion. In that aside, they also state:
Here they almost1 define a new concept that, honestly, should be an equal to coupling and cohesion: coherence, the idea that a class should represent not just a single responsibility, but all of that single responsibility. This is the Single Responsibility Principle from the other end. A class should have one responsibility, not two, and not one-half.
Now we can circle around to lateral abstractions again. Some lateral abstractions encapsulate a whole and complete idea that just happens to sit at the same level of abstraction. We can think of a file and a network socket as being at the same level, since they are both I/O resources, but they represent different ideas. Other lateral abstractions, though, disrupt coherence.
The
CommandParser
above sits at one level of abstraction. It maps a list of character command strings to an output string. It uses a CommandFactory
to handle mapping individual character codes to Command
objects. The levels of abstraction are very close here, and we could go either way:The
CommandParser
and CommandFactory
sit at slightly different levels of abstraction. The CommandParser
deals with strings and Command
objects. The CommandFactory
sits at a slightly lower level and deals with characters and Command
objects. By pulling CommandFactory
out, CommandParser
can manage a higher level of abstraction of looping and piecing together the outputs of Command
s.or...
The
CommandParser
and CommandFactory
sit at the same level of abstraction, meaning CommandFactory
is superfluous. Instead of extracting a whole new class, we could have just as easily (or perhaps more easily) extracted the complicated code into a private method. The CommandParser#parseCommands
method would be just as clear as with a separate class, but without the overhead.To be honest, I can't say exactly which is the best answer here. I went with the
CommandFactory
originally, so I probably wouldn't change that, but I could just as easily see a method doing the same work. There is a case where the separate class has a distinct advantage: dependency injection. If I expected the CommandParser
to need to deal with arbitrary command policies, I would make that dependency injectable through a class. Then the commands could be swapped in and out without any changes to the code (after making the dependency injectable). That ventures into speculation, though, and we need to be pragmatic.1 Instead, they lump it in with cohesion.
Comments
Post a Comment