Alternatives to Object Oriented Programming

Jacob Bell
7 min readNov 27, 2019

--

Throughout my college education, one paradigm has stood above the rest: object oriented programming (OOP). There have been brief explorations of others, but they came with their own languages. Functional with scheme, logical with prolog, imperative with assembly. Most courses used Java, one used C++, one tried to teach OOP in C. At my internships I used C++, which tries its best to allow any paradigm under the sun, but they were still committed to OOP.

I disagree that object oriented should be treated as the answer to every problem in software engineering. There are many other approaches which I find produce results that are faster to write, easier to read and understand, and easier to maintain. This article presents a brief introduction to practical alternatives in C++, including procedurally oriented programming, data oriented programming, compression oriented programming, data driven design, and a little on functional programming. All of these can even be used along with OOP, if you would like to stay close to home.

We’ll start with an object oriented program, then rewrite it from other viewpoints.

Object Oriented Programming

I think a big reason why OOP is popular is because it’s easy to explain yet feels sophisticated. A classic example is a Dog class and a Cat class. Both of these classes inherit from an abstract Animal base class. We can have a pointer to an Animal that’s really pointing to an instance of a Dog or Cat.

#include <iostream>
#include <time.h>
class Animal {
public:
virtual void speak() = 0;
};
class Cat : Animal {
public:
void speak() { std::cout << “meow”; }
};
class Dog : Animal {
public:
void speak() { std::cout << “bark”; }
};
int main() {
Animal* animal;
// Use current system time as a random number
if (time(0)%2) animal = new Cat();
else animal = new Dog();
animal->speak();
return 0;
}

So that this code does something, we randomly instantiate a Dog or Cat, print out what it says (“meow” or “bark”), then exit the program. But as is the case with many explanations of OOP, we thought about the class hierarchy before thinking about what the program is actually doing. Let’s try looking at things the other way around.

Procedurally Oriented Programming

A procedurally oriented programmer looks at code and asks “what is this actually doing?” What task is it accomplishing? What is the CPU really going to do? Discussions involving OOP are often concerned with class hierarchies, but class designs all disappear when the code is compiled. A procedurally oriented programmer doesn’t start solving a problem by thinking about Dog, Cat, and Animal classes. Instead, they write the simplest code that gets the job done. Looking at the OOP example, all it really does is randomly print out “meow” or “bark” when you run it. So let’s write that.

#include <iostream>
#include <time.h>
int main() {
if (time(0)%2) std::cout << “meow”;
else std::cout << “bark”;
return 0;
}

This program does the same thing as the OOP version. It took less time to write and takes less time to read. For such a simple program, it would be wise to leave it here. There’s no reason to make it more complex. But we need an example program, so let’s pretend we’re working on a much bigger piece of software. Having written the basic functionality, we now know more about the implementation details, and we would be better informed when creating an OOP design, or the other designs we’ll explore next.

Data Oriented Programming

When solving a new problem, an object oriented programmer starts by asking “what are the objects?” A procedurally oriented programmer asks “what are the procedures?” A data oriented programmer asks “what is the data?” To a data oriented programmer, all a computer does is transform data, so we start by thinking about what data the program will work with.

Let’s look again at the object oriented version and think about the inputs and outputs. The program’s only input is the current time, which it uses to randomly pick Cat or Dog. The outputs are “meow” and “bark”. These strings are the only data associated with each class. To a data oriented programmer, a Cat is the string “meow” and a Dog is the string “bark”. The Animal struct has no data, therefore it has no meaning. Looking at it this way, we may notice that Cat and Dog have the same data, a string representing its voice. Why do we have two separate classes that store the same data type and are used in the same way? Similar to OOP, we can introduce a generic Animal struct that could be a Cat or a Dog, but this time we’re thinking only about the data it contains.

#include <iostream>
#include <time.h>
#include <string>
struct Animal {
std::string voice;
};
int main() {
Animal animal;
if (time(0)%2) animal.voice = “meow”;
else animal.voice = “bark”;
std::cout << animal.voice;
return 0;
}

What makes this powerful is that we can now add new animals to the program without having to make new classes or data structures.

int main() {
Animal animal;
int randomNumber = time(0)%4;
switch (randomNumber) {
case 0: animal.voice = “meow”; break;
case 1: animal.voice = “bark”; break;
case 2: animal.voice = “baaa”; break;
case 3: animal.voice = “ribbit”; break;
}
std::cout << animal.voice;
return 0;
}

Data oriented programming is also concerned with how the data is used. The way we organize data has big impacts on performance. Putting variables that are used together nearby in memory is called data locality, and can have huge performance gains thanks to CPU caching.

More information

Compression Oriented Programming

You may have taken issue with the lack of encapsulation in the data-oriented example. main now has to know the internals of the Animal struct in order to use it. To address this, we can think like a compression oriented programmer.

#include <iostream>
#include <time.h>
#include <string>
struct Animal {
std::string voice;
};
Animal createAnimal(std::string voice) {
Animal animal;
animal.voice = voice;
return animal;
}
void speak(Animal animal) {
std::cout << animal.voice;
}
int main() {
Animal animal;
int randomNumber = time(0)%4;
switch (randomNumber) {
case 0: animal = createAnimal(“meow”); break;
case 1: animal = createAnimal(“bark”); break;
case 2: animal = createAnimal(“baaa”); break;
case 3: animal = createAnimal(“ribbit”); break;
}
speak(animal);
return 0;
}

We look for places where the user has to know the internals of a struct and “compress” those details into functions. We can further compress to make the code more simple and readable.

#include <iostream>
#include <time.h>
#include <string>
struct Animal {
std::string voice;
};
Animal createAnimal(std::string voice) {
Animal animal;
animal.voice = voice;
return animal;
}
Animal createRandomAnimal() {
int randomNumber = time(0)%4;
switch (randomNumber) {
case 0: return createAnimal(“meow”);
case 1: return createAnimal(“bark”);
case 2: return createAnimal(“baaa”);
default: return createAnimal(“ribbit”);
}
}
void speak(Animal animal) {
std::cout << animal.voice;
}
int main() {
Animal animal = createRandomAnimal();
speak(animal);
return 0;
}

Now main is a few short lines that are easy to read and understand. The idea of compression oriented programming is to look for common patterns and pull out the code into a function. In a larger program, look for duplicate code or two procedures that do a very similar job, then call the new function from both places.

More information

Casey Muratori’s walk through of compression oriented programming in a real-world GUI.

Data Driven Design

This approach is about determining a program’s behavior by data rather than code. So far we’ve been hard coding the animal sounds, which requires recompiling the program and possibly introducing bugs when we add a new one. To make this program data driven, let’s move the data out to a file that can be read at runtime.

meow
bark
ribbit
baaa
moo

This is a plain text file called animals.txt. We can add a procedure, loadRandomAnimal, that reads in this file with strings separated by whitespace.

#include <iostream>
#include <fstream>
#include <time.h>
#include <string>
#include <vector>
struct Animal {
std::string voice;
};
Animal createAnimal(std::string voice) {
Animal animal;
animal.voice = voice;
return animal;
}
Animal loadRandomAnimal(std::string fileName) {
std::vector<std::string> voices;
std::ifstream file(fileName);
std::string next;
while (file >> next) {
voices.push_back(next);
}
int randomNumber = time(0)%voices.size();
return createAnimal(voices[randomNumber]);
}
void speak(Animal animal) {
std::cout << animal.voice;
}
int main() {
Animal animal = loadRandomAnimal(“animals.txt”);
speak(animal);
return 0;
}

Anyone, even someone with no programming experience, can add new animals by editing the animals.txt file.

I like to approach data driven design by thinking about data polymorphism. In OOP we think about code being polymorphic: an Animal can become a Cat or Dog by changing what class instance it points to. In data oriented programming, an Animal can become a Cat or Dog by changing its data. Think about how images work: an image file is a collection of pixels (data). A program that can read a .png file can read any .png file. By changing the data, we can make two images look completely different, but we don’t have to write any new code.

To learn more, I recommend checking out modding tools for games that support them. These tools don’t change the engine’s source code; instead, they let you change level layouts, character stats or 3D models. All of these are data that the engine interprets to create a game world.

Functional Programming

The essential idea of functional programming is that a procedure should take input parameters and produce output with no “side effects.” This means it should not modify any variables that already exist. Our createAnimal procedure is functional: it takes a string as input and produces an animal as output. The following would be non-functional as it modifies an input to the procedure.


void changeVoice(Animal& animal, std::string newVoice) {
animal.voice = newVoice;
}

If a procedure is functional, then there are no surprises; everything is visible to the caller. The drawback is that getting real work done often requires modifying state. Still, I’m always on the lookout for when it makes sense for procedures to be functional.

More information

John Carmack’s thoughts on functional programming in game engines

Many Ways of Thinking

If there’s one thing programmers love to do, it’s disagreeing about best practices. If you followed the “more information” links, you probably saw some strong opinions that contradict things you’ve heard before. There are many approaches to programming, and I don’t believe that any one of these paradigms should be followed rigorously. Each of them are tools to help you think about designing code. They can be mixed together, and each one has its strengths in different situations. Experiment with them and see which ones work best for you!

--

--