Imitation is the sincerest form of not being stupid.
There’s nothing as satisfying as coming up with a completely new and original solution to a problem that’s been troubling you for days—until you find out someone else solved the same problem, long before you did, and did an even better job than you did! In this chapter, we’re going to look at some design principles that people have come up with over the years, and how they can make you a better programmer. Lay aside your thoughts of “doing it your way”; this chapter is about doing it the smarter, faster way.
So far, we’ve really been concentrating on all the things that you do before you start coding your application. Gathering requirements, analysis, writing out feature lists, and drawing use case diagrams. Of course, at some point you actually are going to have to write some code. And that’s where design principles really come into play.
A design principle is a basic tool or technique that can be applied to designing or writing code to make that code more maintainable, flexible, or extensible.
You’ve already seen a few design principles in earlier chapters:
Using proven OO design principles results in more maintainable, flexible, and extensible software.
Encapsulate what varies.
Code to an interface rather than to an implementation.
Each class in your application should have only one reason to change.
Classes are about behavior and functionality.
In this chapter, we’re going to look at several more key design principles, and how each one can improve the design and implementation of your code. We’ll even see that sometimes you’ll have to choose between two design principles... but we’re getting ahead of ourselves. Let’s begin by looking at the first of our design principles.
Our first design principle is the OCP, or the Open-Closed principle. The OCP is all about allowing change, but doing it without requiring you to modify existing code. Here’s how we usually define the OCP:
Suppose you have a class with a particular behavior, and you’ve got that behavior coded up just the way you want it. Make sure that nobody can change your class’s code, and you’ve made that particular piece of behavior closed for modification. In other words, nobody can change the behavior, because you’ve locked it up in a class that you’re sure won’t change.
But then suppose someone else comes along, and they just have to change that behavior. You really don’t want them messing with your perfect code, which works well in almost every situation... but you also want to make it possible for them to use your code, and extend it. So you let them subclass your class, and then they can override your method to work like they want it to. So even though they didn’t mess with your working code, you still left your class open for extension.
You probably didn’t realize it, but we were using the Open-Closed Principle when we wrote those InstrumentSpec classes for Rick’s Stringed Instruments, back in Chapter 5:
InstrumentSpec is closed for modification; the matches() method is defined in the base class and doesn’t change.
But it’s open for extension, because all of the subclasses can change the behavior of matches().
Let’s take what we did back in Chapter 5, and look at in terms of the OCP, one step at a time.
We coded matches() in InstrumentSpec.java, and closed it for modification.
This version of matches()
works just fine, and we don’t want anyone messing with it. In other words, once we’re done coding InstrumentSpec
and this version of matches()
, they shouldn’t change.
But we needed to modify matches() to work with instrument-specific spec classes.
Even though matches()
works great for other InstrumentSpec
objects, it doesn’t quite do what it should for guitars and mandolins. So even though matches()
is closed for modification, we need a way to extend and change it... otherwise, InstrumentSpec
isn’t very flexible, which is a big problem.
So we extended InstrumentSpec, and overrode matches() to change its behavior.
We don’t want to change the code in InstrumentSpec, but we can extend it, with GuitarSpec
and MandolinSpec
, and then override matches()
in each of those classes to add instrument-specific behavior.
The OCP is about flexibility, and goes beyond just inheritance.
It’s certainly true that inheritance is a simple example of the open-closed principle, but there’s a lot more to it than just subclassing and overriding a method. Anytime you write working code, you want to do your best to make sure that code stays working... and that means not letting other people change that code.
But there are going to be times when that code still needs to be changed, maybe for just one or two particular situations. Rather than just diving into your code and making a bunch of changes, the OCP lets you extend your working code, without changing that code.
There are lots of different ways to accomplish this, and while inheritance is often the easiest to implement, it’s certainly not the only option. In fact, we’ll talk about another great way to achieve this later in the chapter, when we talk about composition.
Next up is the Don’t Repeat Yourself principle, or DRY for short. This is another principle that looks pretty simple, but turns out to be critical in writing code that’s easy to maintain and reuse.
Don’t Repeat Yourself
Avoid duplicate code by abstracting out things that are common and placing those things in a single location.
You’ve seen the DRY principle in action, even if you didn’t realize it. We used DRY back in Chapter 2, when Todd and Gina wanted us to close the dog door automatically after it had been opened.
Using DRY, we first need to take the code that’s common between Remote
and BarkRecognizer
, and put it in a single place. We figured out back in Chapter 2 the best place for it was in the DogDoor
class:
Abstracting out duplicate code is a good start to using DRY, but there’s more to it than just that. When you’re trying to avoid duplicate code, you’re really trying to make sure that you only implement each feature and requirement in your application one single time.
In the dog door we just looked at, the feature we were trying to implement was automatically closing the door.
Originally, though, we implemented that single feature in two places: Remote.java
and BarkRecognizer.java
.
By using DRY, we removed the duplicate code. But more importantly, we moved the implementation of this requirement, automatically closing the door, into one place, instead of two places:
The SRP is all about responsibility, and which objects in your system do what. You want each object that you design to have just one responsibility to focus on—and when something about that responsibility changes, you’ll know exactly where to look to make those changes in your code.
Single Responsibility Principle
Every object in your system should have a single responsibility, and all the object’s services should be focused on carrying out that single responsibility.
You’ve implemented the Single Responsibility Principle correctly when each of your objects has only one reason to change.
Most of the time, you can spot classes that aren’t using the SRP with a simple test:
On a sheet of paper, write down a bunch of lines like this: The [blank] [blanks] itself. You should have a line like this for every method in the class you’re testing for the SRP.
In the first blank of each line, write down the class name; in the second blank, write down one of the methods in the class. Do this for each method in the class.
Read each line out loud (you may have to add a letter or word to get it to read normally). Does what you just said make any sense? Does your class really have the responsibility that the method indicates it does?
If what you’ve just said doesn’t make sense, then you’re probably violating the SRP with that method. The method might belong on a different class... think about moving it.
Once you’ve done an analysis, you can take all the methods that don’t make sense on a class, and move those methods to classes that do make sense for that particular responsibility.
Next up in our design principle parade is the Liskov Substitution Principle, or the LSP. It’s definition is as simple as it gets:
The LSP is all about well-designed inheritance. When you inherit from a base class, you must be able to substitute your subclass for that base class without things going terribly wrong. Otherwise, you’ve used inheritance incorrectly!
Suppose that Gary’s Games has a new client who wants to use their game system framework to create World War II air battles. They need to take the basic Board
base type, and extend it to support a 3-dimensional board to represent the sky. Here’s what they’ve done:
At first glance, it may seem like subclassing Board
and using inheritance is a great idea. But look closer, there are lots of problems that this approach creates:
The 3DBoard class is not substitutable for Board, because none of the methods on Board work correctly in a 3D environment. Calling a method like getUnits(2, 5) doesn’t make sense for 3DBoard. So this design violates the LSP.
We already said that LSP states that a subtype must be substitutable for its base type. But what does that really mean? Technically, it doesn’t seem to be a problem:
But when you start to actually use that instance of 3DBoard
like a Board
, things can get confusing very fast:
So even though 3DBoard
is a subclass of Board
, it’s not substitutable for Board
... the methods that 3DBoard
inherited don’t have the same meaning as they do on the superclass. Even worse, it’s not clear what meaning those methods do have!
It might seem like this isn’t such a big deal, but code that violates LSP can be confusing, and a real nightmare to debug. Let’s think a bit about someone who comes to use the badly designed 3DBoard
for the first time.
They probably start out by checking out the class’s methods:
It’s hard to understand code that misuses inheritance.
When you use inheritance, your subclass gets all the methods from its superclass, even if you don’t want those methods. And if you’ve used inheritance badly, then you’re going to end up with a lot of methods that you don’t want, because they probably don’t make sense on your subclass.
So what can you do to avoid this? First, be sure your subclasses can substitute for their base types, which is just following the LSP. Second, learn about some alternatives to using inheritance in your code...
It’s not enough to just know that inheritance isn’t the answer... now we’ve got to figure out what we should have done. Let’s look at the Board
and 3DBoard
classes again, and see how we can create a 3-dimensional board without using inheritance.
So what options are there besides inheritance?
You’ve already seen that delegation is when one class hands off the task of doing something to another class. It’s also just one of several alternatives to inheritance.
Delegation is when you hand over the responsibility for a particular task to another class or method.
Delegation was what we used to solve the 3DBoard
problem we’ve been looking at, without resorting to inheritance:
Delegation is best used when you want to use another class’s functionality, as is, without changing that behavior at all. In the case of 3DBoard
, we wanted to use the various methods in the Board
class:
Since we don’t want to change the existing behavior, but we do want to use it, we can simply create a delegation relationship between 3DBoard
and Board
. 3DBoard
stores multiple instances of Board
objects, and delegates handling each individual board-related task.
If you need to use functionality in another class, but you don’t want to change that functionality, consider using delegation instead of inheritance.
Sometimes delegation isn’t quite what you need; in delegation, the behavior of the object you’re delegating behavior to never changes. 3DBoard
always uses instances of Board
, and the behavior of the Board
methods always stay the same.
But in some cases, you need to have more than one single behavior to choose from. For example, suppose we wanted to develop a Weapon
interface, and then create several implementations of that interface that all behave differently:
Now we need to use the behavior from these classes in our Unit
class. One of the properties in our properties Map
will be “weapon”, and the value for that property needs to be an implementation of the Weapon
class. But a Unit
might change weapons, so we don’t want to tie the weapon property to a specific implementation of Weapon
; instead, we just want each Unit
to be able to reference a Weapon
, regardless of which implementation of Weapon
we want to use.
When we reference a whole family of behaviors like in the Unit class, we’re using composition. The Unit
’s weapons property is composed of a particular Weapon
implementation’s behavior. We can show this in UML like this:
Composition is most powerful when you want to use behavior defined in an interface, and then choose from a variety of implementations of that interface, at both compile time and run time.
Composition allows you to use behavior from a family of other classes, and to change that behavior at runtime.
Pizza is actually a great example of composition: it’s composed of different ingredients, but you can swap out different ingredients without affecting the overall pizza slice.
[note from marketing: how will anyone take us seriously if we compare programming principles to pizza?]
There’s one important point we haven’t mentioned so far about composition. When an object is composed of other objects, and the owning object is destroyed, the objects that are part of the composition go away, too. That’s a little confusing, so let’s take a closer look at what that actually means.
Here’s our Unit
class again, which has a composition relationship to the Weapon
interface and its implementations:
Suppose we create a new Unit
, and assign its weapon property to an instance of Sword
:
What happens if this Unit
is destroyed? Obviously, the pirate
variable is trashed, but the instance of Sword
referenced by pirate is also thrown away. It doesn’t exist outside of the pirate
object.
What happens when you want all the benefits of composition—flexibility in choosing a behavior, and adhering to the LSP—but your composed objects need to exist outside of your main object? That’s where aggregation comes in.
Aggregation is when one class is used as part of another class, but still exists outside of that other class.
The ice cream, bananas, and cherries exist outside of a banana split. Take away that fancy container, and you’ve still got the individual components.
We’ve been using aggregation already, in Rick’s Stringed Instruments, from Chapter 5:
It’s easy to get confused about when you should use composition, and when you should use aggregation. The easiest way to figure this out is to ask yourself, Does the object whose behavior I want to use exist outside of the object that uses its behavior?
If the object does make sense existing on its own, then you should use aggregation; if not, then go with composition. But be careful! Sometimes the slightest change in the usage of your objects can make all the difference.
We started out this section talking about the LSP, and the basic idea that subclasses must be substitutable for their base classes. More importantly, though, now you have several ways to reuse behavior from other classes, beyond inheritance.
Let’s take a quick look back at our options for reusing behavior from other classes, without resorting to subclassing.
Delegate behavior to another class when you don’t want to change the behavior, but it’s not your object’s responsibility to implement that behavior on its own.
You can reuse behavior from one or more classes, and in particular from a family of classes, with composition. Your object completely owns the composed objects, and they do not exist outside of their usage in your object.
If you favor delegation, composition, and aggregation over inheritance, your software will usually be more flexible, and easier to maintain, extend, and reuse.
When you want the benefits of composition, but you’re using behavior from an object that does exist outside of your object, use aggregation.
All three of these OO techniques allow you to reuse behavior without violating the LSP.