Do you remember learning Object Oriented (OO) programming? You probably learned the basics:
- Object encapsulate state by defining fields.
- Object have methods that operate on that state.
- Objects can inherit behavior and state from a parent class.
This is fairly standard stuff. But looking back, I’ve noticed something about the examples used to teach OO programming: they don’t exactly follow good design principles! Are we being taught object oriented programming wrong?
The Shape Example
At some point, you probably encountered a favorite example of instructors: The shape hierarchy. There is an abstract superclass, Shape, that is the superclass for a whole hierarchy of concrete shapes: Circle, Rectangle, Triangle, etc. The class hierarchy might look something like this:
Generally speaking, this is a perfectly fine example. There’s common behavior that every shape has—area, perimeter, and draw, for example—and each subclass implements that behavior differently. I can treat a collection of Shape objects the same, regardless of what their concrete type is.
Life is good.
Single Responsibility?
However, when we look at this shape example through the lens of the Single Responsibility Principle, things start to break down.
As a reminder, the single responsibility principle states that a module or class should be responsible for a single part of the system. Robert C. Martin goes on to define a responsibility as a reason to change. That is, things that change together belong together.
Does the shape example follow the single responsibility principle?
Operations like a shape’s area or perimeter are tightly coupled to the underlying implementation of the concrete class. If I change the way a rectangle is represented internally, I will have to change the implementation of the area and perimeter methods. I won’t have to change the implementation of Circle, Ellipse or Trapezoid.
You could say that the responsibility of each class is to represent the underlying mathematical geometry of the shape it represents.
The draw
method is where things get slippery. Should the shape know where it is located in space, relative to the other objects? If I want to draw to a raster image instead of a vector image, do I have to change every object in the hierarchy? As you can see, each shape is starting to take on a lot of different responsibilities.
An Alternate Design
If the Shape hierarchy is taking on too many responsibilities, how do we redesign it so that we satisfy the single responsibility principle?
The original Shape class hierarchy had three responsibilities: maintaining shape geometry, location in space, and drawing. Let’s extract each of those aspects into separate classes.
- Shape. Maintain the shape geometry.
- Canvas. Holds a collection of shapes and the location of each within it’s bounds.
- ShapeRenderer. Draws shapes to an output device.
The new class diagram might look something like this:
The Shape
class is similar to before. It maintains the overall geometry of the shape and can calculate values like area and perimeter. It no longer is responsible for maintaining its own location or drawing itself.
The Canvas
class is just a container for shapes. It pairs each Shape
object with a set of coordinates that indicate where the shape is located in space.
Finally, the ShapeRenderer
class is an abstract class that converts the raw geometry for each shape and renders it to an output device. This might be a bitmap, such as a raster file or a computer display. It could be vector based output, such as a Post script printer. You could even get clever and write a ShapeRenderer
that outputs ASCII graphics.
With this new design, each class has a single responsibility. If I change the internal representation of a shape, the rendering class doesn’t care. Similarly, if I want to improve the rendering algorithm or support a new output device, the Shape
and Canvas
classes don’t need any modification.
Conclusion
Overall, the Shape hierarchy isn’t a terrible example for teaching the concept of a class hierarchy. But if we want developers to learn good OO design principles, let’s make sure our examples follow those principles.
It’s easy to just wave your hands and leave some things as an exercise for the student. I’ve certainly done a bit of hand waving myself, this article included. To the extent that we can include things like the single responsibility and the other SOLID principles, the better off everyone will be, for students, teachers, and mentors alike.
Question****: How would you improve upon the classic Shape hierarchy example?