The three best reasons I can think of as to why this principle is important in practice is the following:
- Collections
- Interfaces (Liskov substitution principle)
- Factory Functions
As for how to teach these concepts to CS 101 students with the Dog
and Puppy
example, I'd go with the following.
(Forgive me if there are some bugs in the following code, as my Java is a bit rusty.)
Collections
At first glance, it might seem odd to assign a value to a variable when the value and the variable aren't the exact same type. I mean, this is just weird:
Dog dog = new Puppy("Rover");
I mean, if you know dog
is a Puppy
, why not just make the dog
variable a Puppy
type?
Ah, but what if you were working at a shelter and had more than one dog of various ages?
ArrayList<Dog> dogs = ?
All puppies are dogs, but not all dogs are puppies. Sometimes you need to give a dog special treatment based on whether it's an adult or just a puppy. On the other hand, you still want to keep track of them all, and it would be inconvenient to have a different list for every type of dog.
This is where inheritance comes in. Because every Puppy
is a Dog
, you can have puppies and adult dogs in the same list with no issue. And thanks to polymorphism, you can even give them their special treatment.
class Dog {
...
public void feed() {
System.out.println("Gave " + this.getName() + " 1 can of regular adult dog-food.");
}
}
class Puppy extends Dog() {
@override
public void feed() {
System.out.println("Gave " + this.getName() + " 1 can of special Puppy-Gro food.");
}
}
ArrayList<Dog> dogs = new ArrayList<>() {{
add(new Dog("Fido"));
add(new Dog("Rover"));
add(new Puppy("Fido"));
add(new Dog("Max"));
add(new Puppy("Chief Masterson"));
}};
There's no reason why you can't extend this concept to other types of dogs, too. In addition to Dog
and Puppy
, you can have OldDog
, ThreeLeggedDog
, SmallDog
, SuperFullOfEnergyDog
, and Labradoodle
, with each one extending and overriding Dog
in their own way to ensure that each kind of dog gets its special treatment but can still happily co-exist with the other dogs in the ArrayLst
.
Interfaces (LSV)
The more nebulous concept of interfaces might be a bit beyond a 101 student, but their usage should still be understandable. Imagine we had another class that did something with a Dog
that was given to it:
class DogFeeder {
public void feed(?) {
?
}
}
In order to feed every dog the food it needs, we would need to create a function that took each kind of dog and treated it accordingly:
public void feedPuppy(Puppy puppy) {
System.out.println("Gave " + puppy.getName() + " 1 can of Special Puppy-Gro food.");
}
public void feedOldDog(OldDog oldDog) {
System.out.println("Gave " + oldDog.getName() + " 1 can of Goodest-Boi Max Comfort food.");
}
public void feedLabradoodle(Labradoodle labradoodle) {
System.out.println("Gave " + labradoodle + " 1 can of Finest Imported French Cuisine For Dogs food.");
}
This quickly gets cumbersome. We would need to define a new function for every type of dog that existed.
However, looking at the strings that get printed, there is also a definite pattern. We can utilize that and once again employ polymorphism to greatly simplify our feed methods down to just a single function:
class Puppy extends Dog {
...
@override
String getFavoriteFood() {
return "Special Puppy-Gro";
}
}
class OldDog extends Dog {
...
@override
String getFavoriteFood() {
return "Goodest-Boi Max Comfort";
}
}
class Labradoodle extends Dog {
...
@override
String getFavoriteFood() {
return "Finest Imported French Cuisine For Dogs";
}
}
public void feedDog(Dog dog) {
System.out.println("Gave " + dog.getName() + " 1 can of " + dog.getFavoriteFood() + " food.");
}
Factory Functions
Now a student might look at this:
Dog dog = new Puppy("Rover");
And wonder why we couldn't just do this:
Puppy puppy = new Puppy("Rover");
And sure, we could do that. But what if you had a method that returned a Dog
and you didn't know what kind of Dog
it returned?
? dog = Shelter.findDog("Rover");
What kind of dog is Shelter.findDog
going to return? Can you assume it's a Puppy
? Not really, since they might have an older dog named "Rover". But if we don't know it's a puppy, how are we supposed to know if we can treat it like a puppy?
Now imagine in this scenario you are the manager of the shelter. It's 6:00 PM, and it's time to feed all the dogs. You have a lot of dogs, though, so you need to delegate. You get one of your subordinates and assign them a dog named "Rover" to feed, reading off the next name on your checklist.
The dog might be a puppy, an old dog, or a labradoodle, and the kind of dog determines how they need to be fed. But as the manager, you don't really care about those specifics. All you care about is ensuring the dog gets fed. Let the employee figure out how to actually feed it.
In that case, it doesn't matter what type of dog it is. You can just say it is a Dog
:
Dog rover = Shelter.findDog("Rover");
employee.assignToFeed(rover);
In this scenario, rover
could be Puppy
, OldDog
, or any number of other subclass of Dog
. But as the manager, you don't care what kind of dog it is. You just fetch the dog from the Shelter.findDog
method and delegate it off to some employee
that can worry about the specifics.
Afterword
At the end of the day, the student is right. There isn't much reason to do this directly:
Dog dog = new Puppy("Rover");
But that specific example isn't important to the lesson. What's important is the student understands that you can assign a Puppy
value to a Dog
variable. Because once they understand that, it opens up a whole slew of other practices and concepts that wouldn't be possible without that one simple fact.
Many times, programmers write code that doesn't care about the specifics of how some class operates (and, in fact, this is by design if the programmer is following SOLID principles). If the object looks like a Dog
, eats like a Dog
, and barks like a Dog
, then often times there isn't a good reason to worry over whether it's a Puppy
or a Labradoodle
.
This is a fundamental concept that forms the foundation of abstraction, and abstraction is a big part of modern development.