Let's Talk About Object Oriented Programming!
Let's Talk About Object Oriented Programming!
Ellie Strejlau | Senior Developer
July 31, 2018
My thing has kind of always been learning foundations before learning frameworks. So, I decided to write a little bit today about analogies I really want to share that may help others better understand the fundamentals of object oriented programming (OOP).
First, let’s talk a little tech. If you need to, come back and read this part after reading through the analogies in the latter half of this post.
In programming, we have primitive variable types such as a String, Integer, Boolean and so on. We also have more advanced types like an Object or an Array. When we talk about Object Oriented Programming, we are referring to a design pattern that heavily utilizes the Object variable type. Objects are useful because they can contain multiple properties of any type—even other Objects—and methods, usually performing some operation on the object itself.
Utilizing properties on objects to hold data in our application helps us make sure we’re not mixing up any data, compared to the situation in which we try to use separate primitive variable types outside of an object. It also enables us to theoretically work with as many data points as possible. You should think of Objects as a collection of data for one entity. (There is a way in many programming languages to create variable names on the fly and use them, but that can get messy.) For example, if you’re working with data about people, you don’t want to have variables like name1, name2, birthday1 and birthday2. This would be a very static, unreadable and difficult to work with. However, Objects are not necessary for every data point. If you only need to throw a single value of a single type into a variable, primitive types are the better option.
In many languages, Objects can be created on the fly. However, in those same languages, we also have the ability to create a template or definition, called a Class. Objects can then be created from those Classes which provide expectations on what properties should be in the Object. It’s more often that you see methods inside of the Class definition so that we properly follow the DRY (Don’t Repeat Yourself!) philosophy.
If you’re a little confused, here’s a tangible, albeit very technical example: let’s say we have a Car Class and it has properties make (String), model (String), year (Integer), condition (Integer, 0 - 100) and a method called performMaintenance which operates directly on the condition property. I create an Object called myCar that has a make of ‘Ford’, model of ‘Mustang’, year of ‘2005’ and condition of 60, assuming our Mustang is in working order, but could use some maintenance. Then, let’s create yourCar, a 2016 Chevrolet Volt, which might mean you had annual maintenance performed on your car earlier this year. Now let’s create your uncle's car: a 1976 Ford Pinto with a condition of 20, because he hasn’t brought that rust bucket into the shop since 1985.
The logic of our performMaintenance method, inherited by these three Objects, could be such that if the car’s condition is 70 or greater, it prints the message You again? and does not perform any maintenance tasks. If the car’s condition is greater than or equal 40, but less than 70, it prints Right on time!, performs required maintenance, and adds 30 to the condition. Finally, if the condition is less than 40, it prints What took you so long?! because we really want to guilt-trip people when they bring in their car when the engine is just about to seize. In this case, we add 60 to the condition because the car needs a complete overhaul. Then, we want to print the car’s final condition.
Here's what the code could look like in JavaScript ES6 syntax:
class Car {
constructor(make, model, year, condition) {
this.make = make;
this.model = model;
this.year = year;
this.condition = condition;
}
performMaintenance() {
if (this.condition >= 70) {
console.log('You again?');
} else {
if (this.condition >= 40) {
console.log('Right on time!');
this.condition += 30;
} else {
console.log('What took you so long?!');
this.condition += 60;
}
console.log('Maintenance complete.');
}
console.log('Car condition is ' + this.condition + '%.');
}
}
const myCar = new Car('Ford', 'Mustang', 2005, 60);
myCar.performMaintenance();
const yourCar = new Car('Chevrolet', 'Volt', 2016, 90);
yourCar.performMaintenance();
const hisCar = new Car('Ford', 'Pinto', 1976, 20);
hisCar.performMaintenance();
You get the following output when you run node car.js in the terminal:
Right on time!
Maintenance complete.
Car condition is 90%.
You again?
Car condition is 90%.
What took you so long?!
Maintenance complete.
Car condition is 80%.
So, the Mustang gets a tune up, the Volt is turned away and the Pinto is overhauled.
If you’re still a little confused, let’s put it this way: We have a blueprint for a house in a cookie-cutter neighborhood. The blueprint itself is our Class. The actual houses that are built from that definition are the Objects. The Object is the actual implementation or instance of the Class.
Now, let’s talk about inheritance. Let’s create a really generic House class that takes the number of bedrooms, bathrooms, floors, whether or not it has a basement and whether or not the house is for sale. However, we don’t want to be able to create a generic House Object. What we want here is to create an Abstract Class. These Classes are useful for defining properties and functionality that multiple child Classes should inherit but we should only be able to create new Objects of the child types. (This is more inherent in other languages where supplying a keyword abstract before class provides the necessary protection, but ES6 does not have this.)
We’ll also create 3 different new Classes that are extensions of the House class. A Ranch, CapeCod and HighRanch with different default properties depending on the typical style of the house. (I’m making assumptions based on personal experience, so this is atypical.)
Here’s what that code might look like, again in JavaScript ES6 syntax:
class House {
constructor(numBeds, numBaths, numFloors, hasBasement, forSale) {
// Here is where we enforce the abstract-ness. We want to throw a fatal error if someone tries to create a new House object.
if (new.target === House) {
throw new TypeError('Cannot construct House instances directly.');
}
this.numBeds = numBeds;
this.numBaths = numBaths;
this.numFloors = numFloors;
this.hasBasement = hasBasement;
this.forSale = forSale;
}
listforSale() {
this.forSale = true;
console.log('Listed!');
}
sell() {
if (!this.forSale) {
console.log('This house is not for sale!');
return;
}
this.forSale = false;
console.log(Congratulations on selling your ${this.constructor.name}!);
}
showSpecs() {
console.log(This ${this.constructor.name} contains: ${this.numBeds} bedroom(s), ${this.numBaths} bathroom(s), and ${this.numFloors} floor(s). ${(this.hasBasement ? 'It also has a basement!' : 'It does not have a basement.')});
}
}
class Ranch extends House {
constructor(numBeds, numBaths, forSale) {
super(numBeds, numBaths, 1, true, forSale);
}
}
class HighRanch extends House {
constructor(numBeds, numBaths, forSale) {
super(numBeds, numBaths, 2, false, forSale);
}
}
class CapeCod extends House {
constructor(numBeds, numBaths, forSale) {
super(numBeds, numBaths, 2, true, forSale);
}
}
const myHouse = new Ranch(3, 2, false);
myHouse.showSpecs();
myHouse.sell();
const momsHouse = new CapeCod(4, 2, false);
momsHouse.showSpecs();
momsHouse.listforSale();
momsHouse.sell();
const auntsHouse = new HighRanch(4, 2.5, false);
auntsHouse.showSpecs();
auntsHouse.sell();
Here’s our output:
This Ranch contains: 3 bedroom(s), 2 bathroom(s), and 1 floor(s). It also has a basement!
This house is not for sale!
This CapeCod contains: 4 bedroom(s), 2 bathroom(s), and 2 floor(s). It also has a basement!
Listed!
Congratulations on selling your CapeCod!
This HighRanch contains: 4 bedroom(s), 2.5 bathroom(s), and 2 floor(s). It does not have a basement.
This house is not for sale!
I went a little nuts with this example. I hope you get the idea now though.
One more concept that I haven’t used too much are final classes. Final Classes are classes that may or may not be extended from others but they themselves can no longer be extended. Think of it like the end of a tree branch. Using our house example, we can change HighRanch to a final class. Here is how that might look in ES6:
class HighRanch extends House {
constructor(numBeds, numBaths, forSale) {
// This if statement makes sure nothing extends this class.
if (new.target !== HighRanch) {
throw new TypeError('Cannot extend HighRanch class.');
}
super(numBeds, numBaths, 2, false, forSale);
}
}
Now if we try to create a new type of house class called SplitLevel that extends HighRanch—it should probably extend House, but bear with me—and we try to create a new SplitLevel, we’ll see our error message Cannot extend HighRanch class.
class SplitLevel extends HighRanch {
constructor(numBeds, numBaths, forSale) {
super(numBeds, numBaths, 3, false, forSale);
}
}
const splitLevel = new SplitLevel(4, 2, true);
Our output:
/Users/estrejlau/Desktop/houses.js:44
throw new TypeError('Cannot extend HighRanch class.');
^
TypeError: Cannot extend HighRanch class.
at HighRanch (/Users/estrejlau/Desktop/houses.js:44:13)
at SplitLevel (/Users/estrejlau/Desktop/houses.js:59:5)
at Object.<anonymous> (/Users/estrejlau/Desktop/houses.js:76:20)
at Module._compile (module.js:556:32)
at Object.Module._extensions..js (module.js:565:10)
at Module.load (module.js:473:32)
at tryModuleLoad (module.js:432:12)
at Function.Module._load (module.js:424:3)
at Module.runMain (module.js:590:10)
at run (bootstrap_node.js:394:7)
These aren’t exactly real-world examples, but it’s a basic example of the power of object oriented programming. (These could be real-world examples if you’re working on a car inventory or real estate application, respectively!) When working in an information system, you must be able to accurately and efficiently manage and alter data. Using Classes and Objects is a great way to be able to work with complex data points and keep associated data together while staying DRY.