Under the Hood with OOP

Ben Zelinski
The Startup
Published in
11 min readDec 14, 2019

Some quick notes before you dig in:

  • All of the code snippets and vocabulary equivalents are based on the Ruby language.
  • For the sake of readability, this is a higher-abstraction level post that will not be dealing with syntax, but clumsily assumes you can read it.
  • If there’s an OOP concept you’d like plugged into this analogy, hit me with it in the comments.
Look at that sexy @engine

The purpose of this post is to correlate the concepts of object oriented programming with a concrete, physical example in the hopes that it will help bring what is a very nebulous, abstract concept into clearer focus. That example is a Ford factory. My team (users) and I want to build a mode of transportation. I’m the one making the plan (classes), and my team will be the ones doing the building (instances).

Classes as Objects

Before we begin, smarter people than I have taught me to think of classes as molds for objects. I’ll use the word “plans” below, because it fits my analogy better, but I mention this now because it’s perfect for conceptualizing classes as objects. Sure, a class is a mold for the objects it defines, but that mold itself is also an object. You can pick up a toy you just made with a mold, but you can also pick up the mold itself.

In Ruby at least, that mold was made from another mold, which was made from another mold, and on and on until we reach Ruby’s parent class, BasicObject.

Class Creation

To start, I’ll begin with a theoretical concept called a Vehicle (superclass). As mentioned above, even this concept is an object. Think of it as the physical definition of vehicle in the dictionary. Now, what do vehicles have? They have a state, which is made up of attributes like@current_speed and @maximum_occupancy (instance variables). They also have necessary abilities (behaviors). Those are the ability to move, stop, and steer (instance methods). It’s important to note the class Vehicle itself doesn’t have those behaviors and state. Rather, a Vehicle has the state and behaviors defined in the class definition.

I’ve decided that the Vehicle I want to make is a Car (subclass). This is also an object. In this example, think of it as the physical plans for a car. Car is not an instance of Vehicle, it merely inherits the state attributes and behaviors from it. Car plans themselves don’t have a@maximum_occupancy or the ability to move, but they will pass those attributes to any Car.new that gets made.

Because Caris a subclass of Vehicle, it inherits all of Vehicle's attributes. This means that any Car has a @current_speed and a @maximum_occupancy, as well as the ability to move, stop, and steer (inheritance). Because Car automatically includes the state and behaviors defined inVehicle, I don’t need to specify those things again in my plan for a Car. This makes my code DRYer, which makes it easier to understand and safer to update.

But a Car needs things that all Vehicles don’t. Namely, it needs an @engine, @transmission, @wheels, and@steering. To do that, I define those instance variables in my Car class.

Now, did I just create an actual@engine object? No.

Declaring an instance variable doesn’t create the object. It merely prescribes that instance variable as an attribute of any instance of the class. I didn’t create a physical @engine, I specified in my plan that “all Cars have a spot for an@engine.”

Now we’ll deal with a Car's instance methods. What does a car do? Remember, because of inheritance, it is already able to move, stop, and steer. For now, that covers all of a Car's basic behaviors. I’ll add some to the plan as the post dictates.

Initialize

So what exactly is initialize? initialize is a specific instance method that automatically runs whenever an instance is created. In fact, it’s a type of method called a constructor, which happens to be perfect for this analogy. But what does it really do?

For the purposes of our Ford factory, think of initialize as two things: first, think of it as a list of the minimum requirements for a Car to be created. Second, think of it as a description of how any car my team builds will leave the factory.

Let’s take the @engine :

  • If I write my program so that initialize requires an engine argument, I’m telling my team that whatever they make is not a Car until they put an engine in it. Not only that, but it means that every Car will roll off the assembly line with an @engine.
  • If I give the engine parameter a default value, say "v6", I’m telling my team that unless they specify otherwise, every Car they build will automatically be built with a "v6" @engine. As with the previous version, every Car will roll off the assembly line with an @engine, "v6" or otherwise.
  • If I don’t include @engine in my initialize method, I’m telling my team that they are building a Car without an @engine, and the @engine will need to be put in later if desired. Not only that, but every Car they build will come off the assembly line without an @engine, and theCar can and will exist without one until either my team or the owner puts one in.

Building an Instance

So now my plan is done. I pass it to my team (user), and they start to build. Based on the plan I gave them, they build the following:

taurus = Car.new("4 cyl", “automatic", "power steering")

What just happened?

Well, my team just built a Car! Not only that, they built the actual objects pointed to by @engine, @transmission, and @steering, which now comprise that instance's attributes. Initializing those items, through the argument of the Car.new call in this example, is how the objects themselves are created.

This is the distinction that inspired this post. Even though they’re outlined in the class definition, the objects pointed to by the instance variables belong to the instance object itself, not the class.

This is a key relationship to understand: the state and behaviors outlined within the class definition are merely a prescription, a plan, a description of what an actual instance will look like. The actual creation of objects occurs when an instance of a class is created, and any of that instance’s objects have a state is made up of those objects. It’s clearer in our example: while the Car plans include an @engine, the physical @engine isn’t a part of the Car's plan, but a part of the taurus that got built.

Getter and Setter

What if I want my team, or the eventual owner, to be able to change the @engine after a year? If I want that, I need to create a setter method for @engine. This is essential if I don’t include @engine in my initialize method. If I don’t want anyone to be able to change the original @engine, I should not include an @engine setter method (or I should hide it, as discussed below).

What about getter methods? Well, what if our salesperson needs to show a potential buyer what type of @engine it has? A getter method is the ability to pop the hood.

Public, Private, and Protected

Public methods are simple to understand, but because any user can perform those actions, the less you use the safer your code. Public methods are things like open_door or change_gear. Stuff anyone who owns a car needs to be able to do. Some methods, however, are best left out of everyone’s reach.

Let’s take the @transmission. My team only needs to know how to install it. They, and especially the eventual owner, don’t need to know how the @transmission changes gear, and they sure as heck shouldn’t be able to mess with that method, right? To do that, I’ll hide the actual gear changing process as a private method which is called by the public method change_gear, and all anyone needs to know is that when they tell the car to change_gear, that process happens.

How about protected methods? Say I’ve built a public method can_we_substitute_engines?, and protected the engine getter method it calls. Now if my team wants to compare a taurus.engine with a mustang.engine, they can see if they could swap engines, but because I’ve protected the getter method, my team won’t be able to use it as if it was private, and nobody can go to a competitor and say “our taurus has a "4 cyl" @engine.” Of course, in this scenario, nobody can ever pop the hood.

Overriding and the Method Lookup Path

Ok, I’ve got plans in place for my Car, and they include an instance method called open_door. When a user calls open_door, the door opens like most cars, hinged at the front of the door. But let’s say I want to make a subclass of Car with gull-wing doors (like the Delorean from Back to the Future), which we’ll nameGullWing.

Within the GullWing class definition, I’ll define open_door again, using the same name, and have it open the door hinged on the top of the door instead. I’ve just overridden the open_door method in the Car class, and now my GullWing's doors open awesomely from the roof while still leaving the open_door method set traditionally for all the other subclasses of Car.

How and why does that work? It’s made possible by something called the method lookup path. When asked to implement an instance method, Ruby will look first in the class of which the calling object is an instance, then that class’ superclass, then that superclass’ superclass, and on and on until it either finds the method or runs out of classes to inspect. Overriding happens because the search stops as soon as that method is found, leaving any methods that share that name unfound.

Say you’re looking at one of my GullWing.news. There’s no sign on it that says “this will move and stop!” But you know it’s a Car, and you know a Car is a Vehicle, so you know it’s able to move and stop, because that’s what Vehicles do.

So you walk up, and call open_door, and the @door opens from the roof (again, awesomely). Do you keep searching for another @door that opens the same way other Car's @doors open? No. You get in and move on to whatever is next.

Now, that understanding of the GullWing < Car < Vehicle method lookup path isn’t as innate when it comes to writing and reading code, so make friends with a built-in Ruby method called ancestors, which will list the method lookup path for its calling class, all the way back to BasicObject. It won’t tell you where a method is, just everywhere it could look. Picture a sign at every production line in my factory saying “if you need to know something, look here, then here, then here, and so on, in that particular order.”

Class Constants

Now we want to make two subclasses of Car: Sedan and Coupe, which are defined by the number of doors. To simplify things for my team, and to avoid mistakes, when I create my Sedan class I’ll simply define a NUMBER_OF_DOORS = 4 class constant variable, and NUMBER_OF_DOORS = 2 for the Coupe class. This way, whenever my team makes a Sedan or Coupe, the number of @doors is appropriately fixed. Also, if I define NUMBER_OF_WHEELS = 4 in the Car class, any type of car will inherit the requirement for four wheels. Once again, DRYer code!

A word of caution: class constant variables can be tricky when it comes to inheritance. If I defined WHEELS = 4 and an instance method install_wheels in my Car class, and one day wanted make a style (subclass) of Pickup called Dually (WHEELS = 6), I would be shocked to discover that my first Dually.new would roll off the line with four wheels, instead of the desired six.

I could get around this by repeating install_wheels in my Dually subclass to override Car.new.install_wheels. Since the two methods both perform the same function it’s not the DRYest solution, but it might be worth that compromise to avoid accidentally making a five-wheeled Car. Only you can decide that, and the more you code in OOP the better you’ll be at making those decisions.

Modules

Think of modules as additional capabilities. We’ll call our example moduleTowable. When I define the module, I’m building the assembly line that installs the parts of a towing package. Not every vehicle can tow something, nor can every car, but if I want my team to make an SUV or Pickup subclass of Car, then plans for Towable are already in place, and the ability to tow is easily mixed in. Mixing the module Towable in to the SUV and Pickup classes is like routing all SUV.news and Pickup.news down the towing package line, which gives any instance of the SUV or Pickup classes the physical ability to tow.

So Why OOP?

Before writing this post, I have to admit I felt about object oriented programming the same way I felt about learning cursive in elementary school. “Sure, people are telling me this is better, but I already know how to write. What good is a second, more confusing way of writing?”

Let’s answer that with our example. Remember, the users will end up being not only my build team, but the eventual owners. My job, as the one writing the plan, is to make that plan as foolproof and easy to follow as possible.

Forget for a second that you know you’re building a car. You’re on my build team, and I hand you one of the following “plans”:

“Before we start, if you have any questions, you’ll need to dig around and hope the answer you find is the appropriate one.

Build some doors. Put in a v6 or something else. Four wheels. No matter what the final product is. Build a thing that changes gears. You can delete this piece here, and that may or may not render the entire object useless. It can change directions.

Build more doors. Put in a v6 or don’t. The owner will probably change the engine anyway. Hopefully they pick one that works. Again, four wheels. Build another thing that changes gears. If you deleted the piece last time, remember to put it back this time. Build a way to steer. It might be the same way as before, but maybe not.

Build more doors again. Put in anything but a v6. Why are there only two wheels?! Did someone build something different yesterday?! Build a thing that changes gears. What do you mean you can’t find that piece anymore? Build a way to steer. The owner can delete it if they don’t like it. The plans for the towing package are somewhere around here. You can show our engine to our competitor if you want. Please don’t.”

and the OOP version:

“Before we start, if you have any questions, look for the answers in these places, in this order. The first answer you find will be the right one.

Build a coupe, a sedan, and a pickup. Being vehicles, they'll by definition be able to move, stop, and steer, so don’t worry about that. Make sure each car has an engine, a transmission, four wheels, and a steering wheel. Unless you say otherwise, each car will have a v6 engine, which can’t be changed after installation. The transmission will change gears, and you don’t need to know how. The sedan line installs four doors and the the pickup routes through the tow package line, all automatically. You can compare the three engines with each other, but you can’t tell our competitors about them, even if you want to.”

Which plan is more likely to succeed?

--

--

Ben Zelinski
The Startup

I’m having one of those things. You know, a headache with pictures!!