Code quality in Rails projects can often be improved by identifying and fixing cases where inheritance is poorly implemented. These cases can be divided broadly into two groups: those where subclasses are “abusive” and those where subclasses are underused. I’ll clarify what I mean by both of these cases and give tips on what can be done to improve code maintainability and extensibility by cleaning up poor inheritance relationships. These tips are based on thoughts about writing good object oriented code from prominent figures in the software engineering community. I’ll reference some of their ideas as they can be applied to straightening out object hierarchies in Ruby projects.
It may seem to be hard to articulate beyond an intuitive impression that subclasses are stepping beyond their bounds in particular cases. In my own work, I’ve found it helpful to think through Barbara Liskov’s notion of substitutability of subclasses in creating a clean inheritance hierarchy in Rails projects. Essentially, Liskov substitution says that we should be able to replace a parent class with any subclass without changes in the interface or the exceptions that are raised. If this rule is being violated, it’s a sign that the renegade classes may need to be reigned so they adhere to the contract established by the parent, or put into a different pattern if they’re really just bursting out at the seams. There are many more implications of this rule expressed succinctly on the page linked above, so it’s worth a read as you think through this issue in your own project. Once this rule is thought through, it often exposes numerous cases where an inheritance relationship can be improved or refactored into a different form.
The Gang of Four describes how inheritance can easily be overused. I’ve frequently seen this in Rails projects. Specifically, you should watch out for cases where subclasses become entangled with the parent classes through modification of instance variables or private methods. The GoF book notes that we should tend to favor composition of simple objects to create a software ecosystem instead of relying on inheritance hierarchies. Generally the problem noted in the GoF book is that inheritance is essentially at odds with encapsulation. In Ruby, as with other dynamic languages, this problem isn’t easy to resolve, though some thought has been given to the matter at the level of language implementation. The GoF book reminds us that delegation is always an alternative to inheritance in more complex cases.
Thoughts on inheritance from GoF as well as the principle of Liskov substitution are extremely useful in identifying cases where inheritance is poorly implemented through subclasses that are doing too much (essentially stepping out of line from their parents). I’m sure that most Rails programmers reading this post will be able to think of a case that could be improved by considering these ideas.
Since abusive subclasses can take many forms, it is difficult to provide a path to clean code that works universally in all Rails projects. An approach that works well in many cases, though, is to find behavior that can be extracted in the overloaded subclasses to a pattern such as delegation. This gives the classes room to be more substantially different without having to worry about straying too far from the requirements established by the parent object. If you’re new at Rails it may be helpful to remember that you’re not locked into expanding the functionality of your project by creating increasingly complex models. In fact it’s often easier to build small classes for different functionality and project subcomponents. They can stay in the “lib” folder of your project. This also makes tests easier to maintain when all of these minor classes have their own spec files.
On the other side of the spectrum–from classes that abuse inheritance by the mechanisms above–are classes that don’t do enough. These were called “lazy classes” at least as early as Martin Fowler’s book, “Refactoring: improving the design of existing code.” Fowler defines lazy classes as classes that don’t pull their own weight. These classes can be found independent of their inheritance relationship, but Fowler talks about the special common case of subclasses that don’t do much. Since this problem is succinctly expressed by Fowler I’ll quote the short section from the Refactoring book in its entirety before discussing how Rails projects often fall into this trap:
Each class you create costs money to maintain and understand. A class that isn’t doing enough to pay for itself should be eliminated. Often this might be a class that used to pay its way but has been downsized with refactoring. Or it might be a class that was added because of changes that were planned but not made. Either way, you let the class die with dignity (Fowler 83).
I’ve lost count of the number of cases that I’ve seen where subclasses dangle uselessly off the end of an inheritance chain, and I inevitably think of applying Fowler’s object-oriented “tough love.” Remember that with ActiveRecord::Base STI it’s easy to remove the type column after pulling their behavior up into the parent classes. After that, they’re a prime candidate for the refactoring that Fowler anarchistically labels “collapse hierarchy.”
One Ruby-specific way to see from outside of a class that a class may be underused is if you find code that acts based on a class type with the method “is_a?”. For example, if you have a STI relation where Car has subclasses RedCar and BlueCar, you might have in your code somecar.is_a?(RedCar). Note that this isn’t really considered good Ruby code anyway, since most programmers prefer duck-typing to asking a class what it is. That is, asking what it respond(s)_to? is considered better as it’s more versatile if the object given is of a different type. In this case, it isn’t likely that we really need colored cars to be their own object types… giving them an attribute “color” should suffice without the confusing presence of classes that don’t do much.
As you can see, there is an abundance of material from software engineering that can be applied to Ruby and Rails projects to clean up inheritance hierarchies. In my own experience applying refactorings to inheritance hierarchies along the lines described above can really help Rails projects grow and accommodate new features. There are certainly some cases where inheritance is the correct choice. It is clear, though, that Rails and Ruby projects benefit greatly in terms of maintainability and extensibility when programmers consider whether inheritance is abused or subclasses are underused.
0 Responses
Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.