Let’s look at the Open-Closed Principle in object-oriented programming and how can it help you develop better applications and software.
Making cats, dogs and rabbits
Yesterday I wrote about the Single Responsibility Principle and how to use it to code better dogs.
Imagine I’ve created an Animal class with a few methods for most animals do, such as sleep and making sounds.
class Animal {
constructor(type, color) {
this.type = type;
this.color = color;
}
sleep() {
console.log(`The ${this.color} ${this.type} is sleepy.`);
setTimeout(() => {
console.log(`The ${this.color} ${this.type} took a nap.`);
}, 3000);
}
makeSound() {
console.log(`The ${this.color} ${this.type} made a sound.`);
}
}
Now that I’ve created this Animal class, I can create some child classes that will inherit its methods and properties.
class Cat extends Animal {
constructor(color) {
super("cat", color);
}
}
class Dog extends Animal {
constructor(color) {
super("dog", color);
}
}
class Rabbit extends Animal {
constructor(color) {
super("rabbit", color);
}
}
Finally I can create instances of the Cat, Dog and Rabbit classes.
const cat = new Cat("orange");
const dog = new Dog("black");
const rabbit = new Rabbit("white");
So far so good. If I want either the cat, dog, or rabbit to sleep or make a sound, I can just call either the makeSound or sleep methods.
Animals don’t make the same sounds
Currently, the makeSound method only allows me to “make a sound.” But in real life, most cats meow, most dogs bark, and most rabbits don’t make a sound at all. So this method isn’t really doing what I want it to do.
I could fix this by adding a conditional statement to the makeSound method like this:
makeSound() {
if (this.type === "cat") {
console.log(`The ${this.color} ${this.type} said meow.`);
} else if (this.type === "dog") {
console.log(`The ${this.color} ${this.type} said woof.`);
}
}
Now if the animal is a “cat” type, it will meow, but if it’s a “dog” type, it will woof.
Notice I didn’t put a condition for a “rabbit” type. Also, what if I want to create more animal types like kangaroos, orangutans, and horses? Am I going to have to make a condition for each type?
Open-Closed Principle
According to Wikipedia’s definition of the Open-Closed Principle, software entities such as classes and functions “should be open for extension, but closed for modification”.
What does that mean? It’s a bit tricky to explain in abstract terms, but looking at the classes we currently have, I think we’re trying to modify the parent Animal class with the makeSound method, when we should rather just extend the Animal class with specific methods for making a sound.
Let’s refactor our current classes to fix this.
Refactoring to support the Open-Closed Principle
So here’s a new Animal class:
class Animal {
constructor(type, color) {
this.type = type;
this.color = color;
}
sleep() {
console.log(`The ${this.color} ${this.type} is sleepy.`);
setTimeout(() => {
console.log(`The ${this.color} ${this.type} took a nap.`);
}, 3000);
}
sniff() {
console.log(`The ${this.color} ${this.type} sniffed.`);
}
}
There now is no makeSound method. But I have added a sniff method, since most animals sniff the same.
And the updated Cat, Dog and Rabbit classes:
class Cat extends Animal {
constructor(color) {
super("cat", color);
}
meow() {
console.log(`The ${this.color} cat said meow.`);
}
}
class Dog extends Animal {
constructor(color) {
super("dog", color);
}
bark() {
console.log(`The ${this.color} ${this.type} said woof.`);
}
}
class Rabbit extends Animal {
constructor(color) {
super("rabbit", color);
}
}
const cat = new Cat("orange");
const dog = new Dog("black");
const rabbit = new Rabbit("white");
cat.meow();
dog.bark();
rabbit.sniff();
cat.sleep();
dog.sleep();
rabbit.sleep();
So now, instead of attempting to call the makeSound method, each animal has a method specific to the sound it makes. Cats meow. Dogs bark. And rabbits don’t make any sounds, except maybe squeak if you accidentally step of them.