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 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 CommandFactoryreally 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:
At the other extreme, a class that parses only punctuation in a URL is unlikely to be coherent, because it doesnt' represent a whole concept.
– Growing Object-Oriented Software Guided by Tests, page 12
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 Commands.
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.
As software developers, we see errors every day. They manifest as exceptions or segfaults or error codes, telling us that our code has gotten into a state we didn't expect. Their appearance often portends bugs. Though we groan at an unexpected stack trace, we should see an error as a form of automated feedback . Feedback can be fast or slow. We can put the discovery of errors onto a timeline. Errors appear at many points over the lifetime of the code, starting at the moment it is compiled. The further to the right that an error appears, the longer it takes for the feedback to appear. Compile time Compile time is the earliest we can receive feedback about an error. The compiler automatically does a number of checks to make sure the code makes some semblance of sense. The most powerful tool for compile-time feedback is the type-checker. The type-checker makes sure that only values of the expected type are passed around to the places that expect them. This guards
Sometimes when developing code, I run into problems. Ok, a lot of the times I run into problems, but there's a certain kind of problem I run into pretty often. The problem is that I want a thing that doesn't exist. I am writing code, and it is just getting gnarlier and more tangled around a pile of logic. At some point, I hit a wall with it. It is too complicated to really reason about, and I don't want to make things any worse. I've found a trick to solving these problems, though. I take those problems and give them a name. Once I do that, I can get a handle on them, manipulate them, or even sequester them off into a corner. In almost every case, this drastically reduces the complexity back down to a reasonable level, and I often get the side benefit of offering extensibility I didn't know I needed. To demonstrate, let's say we are working on an e-commerce site. The company is based in Alabama, and sells to customers in all 50 states in the US. We have a met
Magic numbers are considered such a scourge upon code bases that there tools out there to automatically find them and root them out. After all, who knows what setValue(6); really means? There are also many approaches to giving better names to magic numbers. Not all of them are good, though. For example, we have a callback that sets a completion percentage on a status: void callback(Status status) { status.updateProgress(0, "Starting"); // Do some stuff... status.updateProgress(25, "Some stuff done"); // Do some more stuff status.updateProgress(50, "More stuff done"); // Reticulating splines status.updateProgress(75, "Splines reticulated"); // Validate everything status.updateProgress(100, "Complete"); } Those percentages would get flagged as magic numbers by the automatic tools. The developer would need to extract those out to constants to make that tool happy. A thoughtful developer would create
Comments
Post a Comment