It feels strange to write about software design today. The most important things that we know about design have been known for decades. In theory, every developer should be able to design an application from scratch using object-orientation or some other design approach. Oddly, though, many can't. In a way, they are victims of our own success in the industry. We've developed libraries and frameworks that make it easy to add code to existing projects. When the key abstractions are in place, it often feels like there isn't much design to do. In some applications, classes are models, controllers, helpers, or views. People often don't step outside of the categories our frameworks give us. Over time, we end up with large classes and methods that have grown by accretion. Looking at them, they don't seem intentionally designed.
We expect programmers to refactor code to keep design tidy, but what do we do when it has already gone sourwhen the design is unrecognizable? Luckily, we can move forward, but it takes a bit of work. It helps if we can uncover some design hidden in the code.
A Program Is A Graph
The class that we have in Figure 1 isn't very well structured. With some poking around, we can figure out what it does it's a class for a rental car reservation system and it does many things.
It records a reservation, searches for inventory, does some pricing, and calculates an awards-for-loyalty program. If we were writing this code from scratch, we'd probably come up with a design like the one shown in Figure 2 a design that is factored.
But right now, we're stuck with the original design unless we rewrite it. Yes, we can refactor, but what are the chances that the code will ever look like our ideal design? In many cases, we're better off giving up on that vision. We might be able to get there from here, but if the class is complicated, it won't be easy Moreover, it might not be worth the effort relative to other things that we can do to make the code easier to deal with.
Let's take a look at a graph representation of the code. (Figure 3) We can use circles to represent each of the fields and methods, and use arrows to show how methods use fields and other methods.
The graph in this figure gives some information that isn't readily apparent in the code. In particular, it appears that many fields and methods are only accessible through the
processPayment methods. We could look at this as being an accident (and sometimes it is), but the graph show us that there is some natural encapsulation happening in the class. We can now consider what our code would be like if we took that cluster and extracted it as a class.
In Figure 4, the
processPayment methods would have to become public, and the original class would have to have to hold an instance of the one we are extracting.
Here's the clincher, though: Is this new class a good abstraction? When we look at the names of its fields and methods, it seems less than ideal. We might be better off if we were able to split the new class into a
UserService like we had in our ideal design. Further, it's hard to imagine what we should call this new class, as it seems to have mixed responsibilities. We could generalize a bit and call it
AccountServices, but the name is less than ideal in the organization this software serves, where quoting and payment processing are not seen as accounting services. Ideally, they would be separate services.
At this point, we have two alternatives. We can extract
ReservationServices, or we can move toward extracting
UserService. The important point to consider is the relative cost of both of these moves.