Object-oriented programming is a beautiful concept with a bad reputation. Many programmers, especially those without formal training, avoid it, because they believe that objects are complicated and abstract; in addition, many programmers don't have a clear understanding of what objects are good for. Why do you need objects at all? Can't you do everything with functions and loops?1
Object-oriented programming serves several purposes. The one that you hear most is that it 'reduces code redundancy'; that is, it avoids you from having to type the same thing twice. While this is true, this is mostly relevant for large projects, in which duplicate code causes all kinds of problems. But object-oriented programming serves another important purpose, one that is useful for large and small projects alike: It's a way to think about programming that resembles how we think about the world.
To introduce object-oriented programming, let's start with an example that embodies its very opposite:
i = sqrt(9)
Here we apply a function (square root, usually written as
sqrt) to a number (9). The result is another number (3), which is stored in a variable (
i). This example illustrates a clear distinction between the data (the number 9 and the variable
i) and the function that is performed with this data (
sqrt()). This data-function distinction is characteristic of mathematics and traditional, non-object-oriented programming.
But now consider this:
We immediately recognize these objects as kittens. And we roughly know what properties and behaviors they have: kittens meow; they have names; they may scratch you; etc. But what's the data here? And what are the functions that can be applied to this data? Is 'meowing' a function that can be applied to kittens, just like a square root can be applied to numbers? Does the following make sense?
# Bad: Doesn't map onto how we think! do_meow('Minou')
Well ... with some training, you can indeed learn to think about kittens in this way, and write programs in the style of the line above; here, the string 'Minou' is the data that corresponds to the kitten, and
do_meow() is the meowing function that is applied to this kitten. Many programs are written like this—and they work. But the important point is that this way of thinking is unnatural; this is not how we think about the world, and it requires a leap of abstraction that makes our brain hurt.
So what's a more intuitive way to think about kittens? Let's start with the basics: Kittens are things that meow and have names:
In Python, this simplified kitten-concept can be defined as follows:
class Kitten: def __init__(self, name): # Is called when a new kitten is created (see below). It # stores the name of the kitten as an attribute. self.name = name def meow(self): # Prints out '[name of kitten] meowed!' print(self.name, 'meowed!')
(If you're not familiar with Python: Don't worry about the syntax, and just focus on the logic.) Here we have defined a
Kitten class; that is, we have defined the concept of a kitten, without creating any actual kittens. If this sounds abstract, think of houses: A house is an abstract concept (the general concept of 'a house') that has different instances (your house, that of your parents, etc.).
So let's create two instances—two actual kittens—which we will call Minou and Muffin:
minou = Kitten(name='Minou') muffin = Kitten(name='Muffin')
And let's hear them meow!
minou.meow() # Will print 'Minou meowed!' muffin.meow() # Will print 'Muffin meowed!'
This illustrates object-oriented programming. First, we defined a class that corresponds to how we think of kittens in general: They have a name and they can meow. Next, we created two instances of this class—Minou and Muffin—and we let them meow. In this simple program, meowing is not a function that is applied to a kitten; rather, meowing is kitten-behavior. And this, I think, corresponds to how we think about kittens.
Now let's make the world a bit more complicated:
More cuteness. Source (Puppy, CC-by license): Wikimedia.
Again, we immediately recognize two types of objects: a kitten and a puppy. Kittens and puppies are different, yet they also have things in common; for example, both are pets that have names. This hierarchical thinking is characteristic of how we think about the world in general: When we think of an object (your father, say), we know that it is part of a broader class of objects (family), which in turn is part of a broader class (people), again part of broader class (animals), and so on—practically ad infinitum.
The beauty of object-oriented programming is that we can incorporate this hierarchical thinking, which comes natural to us, in our programming. Let's say that, in our program, kittens and puppies are both pets with a name, but have different behaviors: kittens meow, whereas puppies bark.
We can express this hierarchy as follows.
class Pet: def __init__(self, name): # Is called when a new pet is created. It # stores the name of the pet as an attribute. self.name = name class Kitten(Pet): # Kitten is a subclass of Pet def meow(self): # Prints out '[name of kitten] meowed!' print(self.name, 'meowed!') class Puppy(Pet): # Puppy is a subclass of Pet def bark(self): # Prints out '[name of puppy] barked!' print(self.name, 'barked!')
We first define
Pet, which has a name. Then we define
Puppy, which are subclasses of
Pet; that is, they are specific kinds of pets, with all the properties that pets have. But, in addition, a kitten can meow, whereas a puppy can bark.
Just like before, we have only defined the concepts of
Puppy, without creating any instances—no actual kittens and puppies yet. So let's (re)create Minou, a kitten, and Duke, a puppy:
minou = Kitten(name='Minou') duke = Puppy(name='Duke')
And let's hear them!
minou.meow() # Will print 'Minou meowed!' duke.bark() # Will print 'Duke barked!'
If you have been able to follow this post so far, you understand the concept of object-oriented programming. The details can be complicated—very complicated—but the general idea is not. Object-oriented programming is a way to structure your program so that it corresponds to how you think about the world. And objects allow you to define hierarchical relationships that, again, correspond to how you think about the world. Therefore, object-oriented programming is not abstract; on the contrary, when used well, it makes your code as concrete as possible.
Once you understand object-oriented programming as a way to structure code so that humans can understand it, it also becomes clear when you should, and when you shouldn't, use it. Let's consider this example (which assumes that a
Number class has been defined, and has a
# Bad: Doesn't map onto how we think! nine = Number(9) i = nine.sqrt()
Does this correspond to how we think about numbers? No, not for me; for me, a square root is a function that is applied to a number, and not a behavior of the number itself. Therefore, this is an example of where you shouldn't use object-oriented programming (or at least not in this way).
Or let's consider this example, in which Minou goes to see Dr. Doolittle (a vet, assuming that a
Vet class has been defined):
# Suboptimal: Doesn't map onto how we think! dr_doolittle = Vet(name='Dr. Doolittle') minou = Kitten(name='Minou') minou.see(dr_doolittle)
Written like this, seeing a vet is kitten-behavior. To me, this is odd; rather, I would say that seeing a kitten is vet-behavior:
dr_doolittle = Vet(name='Dr. Doolittle') minou = Kitten(name='Minou') dr_doolittle.see(minou)
Good object-oriented oriented programming is about making such choices, because there are always many ways to structure a program. What should be an object, and what shouldn't be (should a number be an object)2? If two objects interact, which object is the actor (does Minou go see Dr. Doolittle, or the other way around)?
I admit: The examples from this post are easy, because kittens and puppies are physical, familiar objects of which we have clear mental models to base our program's structure on. In many cases, programs deal with abstract things (databases, url requests, image processing, etc.) of which we don't have clear mental models. But, even then, a well-written program has a structure that maps onto how we think.
So think about writing a program as you would about writing a text. A well-written program is more than just syntactically correct, just like a well-written text is more than just free of grammar mistakes. A well-written program, just like a well-written text, corresponds to how we think about the world, as consisting of objects with behaviors, properties, and relations with other objects. And object-oriented programming is one way (of many) to accomplish this.
In this blog post, I assume a basic understanding of programming; that is, I assume that you're familiar with things like variables, functions, and for-loops. ↩
Technically, in Python everything is an object. But that doesn't mean that your code has to be explicitly structured into objects; in Python, too, you can write entirely with functions and loops. ↩