Under the Hood with OOP
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.
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 Car
is 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 Vehicle
s 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 Car
s 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 anengine
argument, I’m telling my team that whatever they make is not aCar
until they put anengine
in it. Not only that, but it means that everyCar
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, everyCar
they build will automatically be built with a"v6" @engine
. As with the previous version, everyCar
will roll off the assembly line with an@engine
,"v6"
or otherwise. - If I don’t include
@engine
in myinitialize
method, I’m telling my team that they are building aCar
without an@engine
, and the@engine
will need to be put in later if desired. Not only that, but everyCar
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.new
s. 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 Vehicle
s 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 @door
s 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.new
s and Pickup.new
s 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?