DEV Community

Jonathan Kuhl
Jonathan Kuhl

Posted on

On Automobiles and OOP

Introduction

Stand at any street corner and watch the traffic for a while. A Subaru Crosstrek. A Ford Escort. Now Dodge Ram. A Subaru WRX with a loud rumbling engine. A Yugo held together by duct tape and desperation.

All of these, are cars. They have the same basic structure. Four wheels, an engine, a steering wheel, runs on gasoline or diesel. Yet, they vary greatly in color, horsepower, shape, features, and even the type of gasoline they might use. Every busy street is a cacophony of different car shapes, yet most of the vehicles we see, everyone will agree, is a car. And yes, depending on how you define it, your Dodge Ram could be considered "a car."

This in turn is what Object Oriented Programming (OOP) is about. An object is a bundle of individual properties that is based on some basic template. Each individual car is a bundle of individual features based on some basic template. Of course, some templates are templates of templates. There is no "basic car", it is an abstract idea, a collection of properties and methods every derivative car must implement, but every derivative is free to implement as they desire within the confines of that abstraction. We have a general idea of what a car is, and then expanded into different types of cars. A Subaru Outback is an station wagon. A GMC Yukon is an SUV. A Ford Fusion is a sedan and no one really knows what a Honda Fit is. From these types, station wagon, SUV, sedan, etc, you can find even more specific ideas, like the vehicles I listed as an example. Ultimately, you get to an abstraction specific enough to build individual cars from.

Car
    Sports Utility Vehicle
        GMC Yukon
    Station Wagon
        Subaru Outback
    Sedan
        Ford Fusion

These are called classes.

Classes

If you walk into a Subaru factory, you won't find a group of crazed engineers trying to hobble together a car willy-nilly. That of course, would be ludicrous, inefficient and lead to the creation of cars that are not uniform.

Artistically, non-uniformity is typically valued, but in the programming world, it causes problems. Objects don't fit their required role. Testing is nearly impossible and therefore quality assurance goes right out the window. Mass production is slowed.

Instead what you find is a long procedural assembly line of workers (most of whom are now robots) following a precise set of instructions to build a car based on a previously drawn out schematic.

This schematic is precisely what a class is. Classes themselves are not the objects they create, but rather a template for creating objects. If I were to pull the schematics of a 2017 Subaru Outback, it tells me in detail where every nut and bolt goes and how every functionality works and is implemented.

Constructors

Of course, there are variations in how individual cars are created. While, for some reason, orange Subaru Crosstreks (I too enjoy driving pumpkins down the street!) seem to dominate the freeways, obviously, one has a lot of leeway in color and numerous other minor features.

The constructor sets properties and initializes them. It allows us to set the individual details for an individual instance of the object. Then, it creates the object.

Constructors, and initializers, come in many different flavors. And before I continue, there is a clear difference between a constructor and an initializer. A constructor creates the object in memory (the Subaru factory plops out a new Outback) and the initializer sets its properties (the new Outback gets painted with optional features plugged in.) Some languages do this at once, others do it separately.

// Java
class MyObject {
    String param;
    MyObject(String param) {
        this.param = param;
    }
}
// JavaScript
class MyObject {
    constructor(param) {
        this.param = param;
    }
}
# Python
class MyObject:
    self.__init__(self, param):
        self.param = param

Think of constructors like the assembly line at a Subaru factory. You start it up by telling the robots what colors and features you want (okay, it probably doesn't actually work like that in the real world factories but bear with me) and the robots then, based on the class template they're given, build the car with your desired parameters.

At the end of the line is a nice shiny Outback with all the customization you wanted, but it has enough Outback features that it is as much an Outback as any other Outback you encounter. Even the old blocky ones.

Interfaces and Abstract Classes

However, not every object needs to be constructed. As I said before, the basic car idea is not a concrete object, but an abstract idea. The basic car is just a list of features that every car must implement. If you don't implement these features, whatever your building is not a car. Maybe it has wheels and an engine, but it is not a car. If a semi comes out of the other end of your factory, it is not a car because it has too many wheels. If an M1 Abrams Main Battle Tank comes out of the other end, it's not a car because it has treads, instead of wheels, and the DOD would like to know why you're building military tanks.

An interface is a contract. It guarantees that whatever comes out the other end will be a car.

To drop the car analogy for a moment, imagine you have a function that takes an object as one of its parameters and within that function, it calls one of that object's methods.

class Person {
    String name;
    Person(String name) {
        this.name = name;
    }
    public void greet() {
        System.out.printf("%s says 'Hello'", this.name);
    }
}

// assume we're in class main
public static void personGreet(Person person) {
    person.greet();
}

public static void main(String ...args) {
    personGreet(new Person("Jim"));
}

In the above example, we have a method that takes a Person and calls its greet method. This works fine of course, but there's a problem. What if I have multiple person types, and inheritance is a model doesn't work to my favor? What if I have Employee and Dog, all of which might have a greeting, but maybe don't work as a subclass of Person? Employee might, but Dog wouldn't! Maybe the structure of my code is such that Employee works out better with composition rather than inheritance. We can ensure that both classes work in the personGreet() method with an interface:

interface Greetable {
    public void greet();
}

class Employee implements Greetable {
    // ...
    public void greet() {
        System.out.printf(
            "%s says 'Hello', I work for %s as %s", 
            this.name, this.job, this.jobTitle
        );
    }
}

class Dog implements Greetable {
    // ...
    public void greet() {
        System.out.printf("%s barks 'Arf Arf!', this.name);
    }
}

// and back to the Main class
public static void personGreet(Greetable greeter) {
    greeter.greet();
}

Now our greet class can take any object that implements Greetable because we can be confident that it implements the required methods.

It's also incredibly helpful with any modern IDE. If an IDE knows your variable implements Greetable, it will helpfully suggest methods in the Greetable interface, so you don't have to memorize how to spell them or in what order the method parameters might go.

Some languages like JavaScript don't have static typing and for those languages interfaces are meaningless. If I wanted to pass an object to the personGreet() method, there's nothing to ensure that the personGreet() has a greet method, except for me to actually program such a check. An interface is where statically typed languages shine. If you're a fan of JavaScript but also want static types, try Typescript.

In drivers ed, you were not taught how to drive a specific car. You were taught how to drive in a very generic fashion. We all understand that cars implement a specific interface. The steering wheel is on the left (unless your British), the accelerator is on the right and it makes it go. Flick the blinker up to single a right turn, and the blinker down to signal a left turn. Cars all implement the same interface. If you can drive one car, you can drive them all.

And any vehicles that require special licensing, they might implement the Car interface, but they also implement some other interfaces the user would need to be aware of. A semi might implement some of the same details as a car. A semi is operated in the same basic way after all, but would also implement some other interfaces, since it's size, weight, number of wheels would all affect its handling and make it a different object. In fact, it might be better, to take some ideas out of car (like basic driving) and move it up into a Vehicle interface.

Perhaps it would be better if car were an abstract class rather than an interface, that implements the Vehicle interface, but also has its own properties like hasFourWheels and other things that are unique to cars, but not to vehicles.

In Java you can see this progression from the Servlet interface to the GenericServlet abstract class to HttpServlet abstract class to your own implementation of Servlet that inherits from HttpServlet or GenericServlet.

Properties and Methods

Let's go back to that road I opened this article up with. We had all those cars driving by, including that god awful orange Subaru Crosstrek. (Why are they always orange?) While they all inherit from Vehicle and Car, they all look pretty unique. People have a broad choice in shape, color, performance, radio, cabin controls and so on and so forth. When you construct an object, it is a unique data structure stored in memory. You can have any number of them (until memory runs out of course) and they might have the same basic shape, yet they're all unique.

Now before I continue, a property is defined in different languages as different things. In JavaScript, they are properties. In Python and Ruby, they are attributes. In Java, they are fields. For clarity sake, when I say "property", it applies to all of these things.

A property is simply some state applied to an individual object. A car's color or its horsepower, for example.

A method is some behavior an individual object might perform. When you hit the gas in your car, you call the accelerate() method. And even though every object has the same accelerate() method, not every car will accelerate at once! Only the car calling its own accelerate() will start to accelerate and increase its speed property. If two cars accelerate at the same time, they only modify their own internal state. Me pressing the gas in my car, only makes my car go.

Static vs Member

When people talk about static methods and properties, they often say something obscure like "static methods belong to the class, while member methods belong to an object." Once you get a handle on OOP, that's not terribly difficult of a concept to grok. But for someone brand new to programming, it might be a bit confusing. After all, don't all methods and properties belong to the class? A new programmer might not yet realize that a class and an object are not the same. As I said earlier, a class is a schematic, an object is the concrete thing the schematic designs.

Anything marked as "static" is something meant to apply to the class as a whole, something that wouldn't make sense to apply to an individual object, nor make sense to separate from the class.

To explain it, I'll continue the car analogy. Imagine you take your car on a road trip. Because you're fabulously rich, you ferry your car across the Atlantic from the United States to Europe. When you get there, you realize Europeans, who like to keep things simple, use the metric system. Fortunately, your car came with a function that swaps the miles-per-gallon display to kilometers-per-liter. Such a formula is useful for any car, but it won't change if you're driving a Subaru WRX or a Ford Fusion or a clunky Yugo held together by duct tape and desperation. MPG to KPL is the same function no matter what you drive. Even if you drive a gas guzzler, the unit conversion doesn't change. So why make the method specific for individual cars? Make it static so that all cars can share it.

The MPG-to-KPL conversion doesn't belong in a car. Certainly an individual might build in a method to display such a conversion in its own, but the actual implementation of the mathematics doesn't belong in a car. It instead can just be stored as common knowledge to all automakers and drivers out there.

class Car {
    final static double MPG_TO_KPL = 0.425144;
    public static double mpgToKpl(double mpg) {
        return mpg * MPG_TO_KPL;
    }
}

class Outback extends Car {
    public void displayMetricFuelConsumption() {
        double fuelConsumption = Car.mpgToKpl(this.mpg);
        System.out.println("Current fuel consumption in kpl: " + fuelConsumption);
    }
}

As you can see, the individual object itself can call its own method that displays the converted unit, but the unit conversion itself is static and kept at the class level. Furthermore, Outback wouldn't have to extend Car for the static method to be available, any object can call it from the Car class.

Inheritance

At this point you should have a broad idea of what inheritance is. To put it all in one place, the idea of inheritance is that subtype inherits the properties and methods of its parents.

A station wagon is a Car, therefore it has four wheels and is meant to be used by the general population. A Car in turn is a Vehicle and therefore it has a steering wheel, accelerator and brake pedals, some sort of piston engine, and so on and so forth.

A Subaru Outback is a station wagon, therefore it is a car with a hatchback with extra space in the trunk. And because it is a station wagon, it is therefore a Car, and because it is a Car, it is therefore a Vehicle.

Polymorphism

But not every subtype will implement methods in the same way their parent does! A Subaru WRX and a Subaru Outback certainly have different acceleration methods! In any OOP language, children can override their parent's methods and properties. This is done through overriding and overloading, though not every language will support overloading. JavaScript, for example, does not.

An override is when we change the implementation, but keep the method signature the same. In the Subaru WRX, maybe within our acceleration method there's a turbocharge method, something you wouldn't typically find in an Outback.

An overload is when the method signature is the same but the arguments and their types are different. You can override a method multiple times so long as you overload it with different arguments and different argument types. But again, not every language supports this. JavaScript does not.

You can emulate overloading in JavaScript by using the typeof or instanceof keywords and programmatically determining what the arguments are and what the implementation does depending on what arguments are given, but you can't have multiple functions with the same name and different parameters.

You can in Java:

public static String add(String a, String b) {
  return a.concat(b);
}

public static int add(int a, int b) {
  return a + b;
}

Both methods have the same name, but different signatures. Overloading is simply changing the implementation depending on what the inputs are.

Application Programming Interface

Before I continue, I do want to take a brief moment to explain what an API (application programming interface) is. An API is the collection of public facing properties and methods a user can use.

Your car has an API. There's a port for it to take in gas. There's a pedal that makes it go and another that makes it stop. There's a wheel that allows me to steer. These things allow you to interface with the car and change its state and behavior in a controlled and predictable way.

The API of any object can be summed up as its public properties and methods that are meant to be used by the user.

Encapsulation

When I drive down the road, when my foot is on the brake or the gas, it affects the internal state of my car. I'm slowing down or speeding up. It does not affect the other cars around me. The internal state of my car is encapsulated. As the user, I don't need to know what the engine is doing, and neither does anyone around me. I just need to know what the public API is. When I interface with that API, I don't need to be aware of what is going on inside. If I hit the gas, I don't need to know specifically which pistons must rise and which ones must lower at any given moment; the engine handles that for me. And if I were allowed to go in and modify that while the engine is running, I could break something.

This is encapsulation. A class's internal state is only exposed to the outside world through specific methods explicitly designed for use by an outside user. A singleton for example, uses encapsulation to ensure there's only one instance of itself. The constructor is only accessible through a specific public method that ensures it can only be called once. Typically, it checks if the instance is created and either creates the instance, or returns a reference to that instance, but the constructor itself, set to private, cannot be called from the outside.

Abstraction

Now let's return to that driver's ed course again. Remember that you were not taught to drive a specific car, but a generic idea of a car. We know that any car you do drive will inherit from Car and Vehicle, so we know it will have things like an accelerator, a brake pedal, a steering wheel, turn signals, and everything else in more or less the same place and the same configuration (but maybe on the other side for you British types!)

It doesn't matter how those methods are implemented, but only that they are, and in a predictable manner. Abstraction is related to encapsulation, in that you don't know or care about the internals. What you do know, is that if you call a method, predictable behavior should occur. If I hit the gas, the car should move forward. If I hit the gas and the blinkers come on, that . . . that would be weird.

You know how to drive any car because you were given an abstract idea of what a car is and what its methods are. You know every car will have these methods in a similar API you can use. The actual implementation of the gas pedal or the turn signal or whatever is black boxed from you. If you want to accelerate, you just hit the gas. There's no need to understand how pressing that pedal causes gas to flow into your engine, which makes the pistons rise and fall and allows them to cycle through a system of intake, compression, combustion, and exhaust. Sure, that might be handy for maintenance, but you can just as easily drive a car without any clue what that big loud metal thing under the hood is actually doing.

Abstraction hides implementation details from where it is not necessary. It allows you to take that implementation and bundle it into something more meaningful and reusable. This reduces complexity on the user's end. Take for example JavaScript's library of Array methods.

const squares = [1,2,3].map(x => x**2);
//[1, 4, 9]

While JavaScript is an OOP language, it is based on a prototypal inheritance scheme rather than a classical one. That's something to keep in mind. With that said, most of what I've stated here applies to JavaScript as well, but perhaps not exactly.

Do I as the user care what map() does under the hood? No. All I care is the knowledge that it performs some function on every element. I don't care how. I don't want to implement a mapping function every time I want to change the elements of an array. Why not abstract it away into a method so I can reuse it as necessary?

That is abstraction.

Conclusion

Object Oriented Programming can be a bit confusing but I find it is best understood if you think about objects as literal everyday objects. Cars are mass produced objects that bear a similar basic shape even though there's an incredibly wide variety of types out there, from hulking SUVs to station wagons, to pick up trucks, to sedans and sportscars. If you think about how cars relate to each other in the real world, you can have a better understanding on how objects work in the programming world.

I hope this helped create a better understanding of what OOP is and what the basic concepts and terms involved in OOP are. I did not intend to really go into actual language implementation of OOP, but hopefully when you read about how different languages implement OOP, having some sort of analogy will help make the concepts clearer.

Top comments (0)