What's an Object ?

Even if you can't state it properly, you probably have a good idea of what object-orientation is about.

The non-developer will identify objects as "things" capable of "doing things" such as a payment gateway or a clock.

The common developer will usually think of objects as a thing that groups together variables and functions.

The tech-savvy will smugly say it's about passing message, invoking behavior on objects polymorphically, encapsulating the implementation details and abstracting part of the complexity away from the current concern.

Who is right ? The response might be surprising.

The Commoner View

The every-day developer has a good technical definition of what makes an object. I mean, technically, it's really about grouping variables and functions together. There's nothing to argue about, here.

Or is there ?

If that definition is technically accurate, it lacks the profound ideas that make objects... well, objects.

Have you ever programmed in C ? If you did, you might recognize the following code.

struct money {
    int value;
    const char *currency;
};

That's a struct. Technically, a struct is a group of variables clumped together under a common name, usually because they're treated as a single unit.

Still in C, you can create functions to manipulate this structure.

void money_create(int, const char*)
void money_add(const money *, const money *); // The sum of two "money"
void money_print(const money*); // print the money

Nothing new under the sun, just simple functions peacefully enjoying their life.

Unless...

Well, you see, there's something of unsuspected importance happening here. C, being the bare-metal language it has always been, wasn't built with object-orientation in mind at all. No one in their right mind would even qualify C as an object-oriented language (and for good reasons !).

Yet, this code features object-thinking more than most Java or C# code I've seen in my life.

Impossible, you say ? Ah, such innocence !

You see, this design treats "money" as an object because it exposes a well defined contract (the methods operating on the structure) and this contract exposes a real behavior.

The behavior of "money" (adding two "moneys" together and printing it) is constrained into functions encapsulating the mechanic of manipulating "money" away from the clients using it.

Even better, it is possible in C to obfuscate the structure and prevent clients from knowing anything about the internals. I can literally build the C code into machine code, provide a header with the public interface (the functions and the fact that a structure named "money" exists, without the definition of "money") and have anyone use my code while giving as little detail as possible.

In other words, it is possible to use "money" objects without ever knowing what it's made of ! You can sum "moneys", print "money" and do whatever you want with "moneys", blissfully unaware of the implementation details.

As the implementor, I can change "money", optimize it, add or remove new fields, everything I want without breaking client code.

That's the heart of object thinking.

And we're in C.

Think again.

The Tech-Savvy View

Indeed, OOP is about dynamic dispatch, polymorphism, abstraction, encapsulation, inheritance, interfaces, late-binding, whatever floats your boat.

Do you think C has any of this ?

No, it doesn't.

It has tools to mimic and approach these features which makes OO possible in C, but it's far from convenient.

And that's why we really don't like doing OO in C : it's dangerous. It's unsafe. The language has no built-in constructions to protect you from yourself. You become your biggest enemy.

Modern OO languages feature all of these. They're considered minimal requirements. They're the seat belt preventing you from crashing yourself.

And they're nothing more than that : belts. Tools.

They describe how you can safely do OO using language constructs, optimized and made digestible for humans to use.

But I've just shown you can do OO without these constructions, at least partially.

That's to say these tools will help you doing OO, but they provide no guarantee whatsoever that your code will be in OO form.

Let's take a simple program as an example : the magic number.

I believe you can derive what the program does from its algorithm. It generates a random number between 1 and 100 and prompt the user to find it.

import prompt from 'prompt';

async function magicNumber() {
  prompt.start();
  console.log('Welcome to the magic number !');

  let magicNumber = Math.floor(Math.random() * 100) + 1;
  let userNumber = -1;

  while (userNumber !== magicNumber) {
    const { guess } = await prompt.get(['guess']);
    userNumber = parseInt(guess as string, 10);

    if (userNumber > magicNumber) {
      console.log('Too big');
    } else if (userNumber < magicNumber) {
      console.log('Too small !');
    } else {
      console.log('You won !');
    }
  }
}

magicNumber();

This code is in TypeScript. Nobody will ever assert that TypeScript isn't an OO language, right ? Well, who knows. I've seen how far people can go in their delusions, it's quite surprising.

But we're responsible and sane people, so we'll accept TypeScript as an OO language. Thank you very much.

Now... what's the problem with this code ?

There's none. It works, it's simple to understand. It's predictive. It's a procedural programmer's wet dream.

But it's not object-oriented.

Silly you, Anthony ! There's no way to make this code object-oriented. How can you make objects out of it ?!

Turns out I can. Now, keep in mind this is a very contrived example in which the procedural code will be much, much simpler and smaller than its object-oriented equivalent, because it's a very small and straight-forward program. Now, behold...

import prompt from 'prompt';

interface IEventListener {
  tooBig(): void;

  tooSmall(): void;

  match(): void;
}

class MagicNumber {
  private value: number;

  constructor(private readonly eventListener: IEventListener) {
    this.value = Math.floor(Math.random() * 100) + 1;
  }

  compare(guess: Guess) {
    if (guess.isGreaterThan(this.value)) {
      this.eventListener.tooBig();
    } else if (guess.isLessThan(this.value)) {
      this.eventListener.tooSmall();
    } else {
      this.eventListener.match();
    }
  }
}

interface IPrompt {
  prompt(): Promise<number>;
}

class Prompter implements IPrompt {
  constructor() {
    prompt.start();
  }

  async prompt(): Promise<number> {
    const { guess } = await prompt.get(['guess']);
    return parseInt(guess as string, 10);
  }
}

class Guess {
  private value: number;

  constructor(private readonly prompt: IPrompt) {}

  async start() {
    this.value = await this.prompt.prompt();
  }

  isGreaterThan(value: number): boolean {
    return this.value > value;
  }

  isLessThan(value: number): boolean {
    return this.value < value;
  }
}

class Game implements IEventListener {
  private running = true;
  private prompter = new Prompter();
  private magicNumber = new MagicNumber(this);

  run() {
    while (this.running) {
      const guess = new Guess(this.prompter);
      this.magicNumber.compare(guess);
    }
  }

  tooBig(): void {
    console.log('The number is too big');
  }

  tooSmall(): void {
    console.log('The number is too small');
  }

  match(): void {
    console.log('You won');
    this.running = false;
  }
}

new Game().run();

Good heavens ! Have you lost your mind ? What the hell did you smoke ?

Don't worry, that's an expected reaction. Actually, it's a sane one. This code looks bloated, over-engineered and overly complicated. For this kind of program, maybe it is.

But i'm sure you're gonna appreciate what comes next.

First, what's in a "game" ? It boils down to four lines of code.

run() {
  while (this.running) {
    const guess = new Guess(this.prompter);
    this.magicNumber.compare(guess);
  }
}

Nothing more, nothing less. The user takes a guess, the magic number checks if it's correct, and eventually our "game" will be notified of the result and stop the game if the magic number is found.

Four lines of code describe the "game". Just four lines.

How do we generate that magic number ? We don't know. It can come from a file, or from the network, we don't care. All we need is to have a magic number.

How do we take a guess ? We don't give a damn. The user may type it, or maybe we plugged-in an IA or a program we developed. It can come from the network, for all I know.

As far as we know, we'll end up receiving a guess and feed it to our magic number. How and where are none of our business.

Our "game" isn't procedurally controlling the guess mechanism nor the magic number mechanism. It create these objects, make them talk to one another and let them live their life independently. Game isn't an abusive father asking his kids where they're going when they leave the house at 8 PM. Game is the cool dad telling these kids "there, take 50$ and call me if anything goes wrong". Game is nice. Be like Game.

MagicNumber and Guess both do their work happily because they love what they do. They were created for this very moment to happen and worked all their life to shine. They're glad no one is asking them how to do their work and coordinating them. They're grown-ups, they can coordinate themselves.

class Guess {
  private value: number;

  constructor(private readonly prompt: IPrompt) {}

  async start() {
    this.value = await this.prompt.prompt();
  }

  isGreaterThan(value: number): boolean {
    return this.value > value;
  }

  isLessThan(value: number): boolean {
    return this.value < value;
  }
}

Look at Guess. He's very protective. He won't expose his children value to the danger of the outside world. He will carefully protect its integrity and exposes only what's necessary for other objects to keep working. Because Guess knows that knowing too much is not a blessing but a curse. Good boy, Guess.

How does Guess works ? He uses a Prompt. What's a prompt ? We don't know and we don't care. All Guess needs is a method to read something from the user. It's not his job to do it, he has enough to do protecting the value given back. After all, a guess is a value.

So what's in a Guess ? A value. That's it. But a value with a meaning.

What's in a MagicNumber ? You guessed it (pun intended) : a value. With a meaning.

class MagicNumber {
  private value: number;

  constructor(private readonly eventListener: IEventListener) {
    this.value = Math.floor(Math.random() * 100) + 1;
  }

  compare(guess: Guess) {
    if (guess.isGreaterThan(this.value)) {
      this.eventListener.tooBig();
    } else if (guess.isLessThan(this.value)) {
      this.eventListener.tooSmall();
    } else {
      this.eventListener.match();
    }
  }
}

You see, MagicNumber is a gentleman (or a lady, for what matters). They don't take Guess by the collar and check what's value inside. MagicNumber politely take out its own number and ask Guess how it compares to its own. The two of them communicate like human beings would, and they proceed to live happily ever after.

So you see, a Guess isn't just a number. It's a number with a meaning (semantic) and a behavior. And a MagicNumber isn't just a number either. It also has a meaning and a behavior.

They're objects. They serve a purpose and do so independently. Anytime the logic for guessing the magic number changes, only them need to be updated. They're very close collaborators.

What about Game ? That lad's chilling. While the kids are partying outside, he's drinking a beer while enjoying some e-sport games. The kids may decide to change plans and go elsewhere, he wouldn't mind at all.

Why ? Because Game has a very simple contract with MagicNumber : if anything happens, call me.

interface IEventListener {
  tooBig(): void;
  tooSmall(): void;
  match(): void;
}

See, MagicNumber will report everything to Game, if it's of interest to him. They will do so using the cellphone, a metaphor for this interface. They won't go back home directly to report on father, but they will call whoever is on the other end of the phone. For now, it's Game, but if Game is caught snoring, it may be someone else. As far as MagicNumber is concerned, it's all the same.

And that's how Game is still in control of the overall process. Anytime he wants, he can take his car and pick up the kids. He's in control, yet he doesn't control them. That's the subtletie.

class Game implements IEventListener {
  private running = true;
 
  // Code removed for brevity

  tooBig(): void {
    console.log('The number is too big');
  }

  tooSmall(): void {
    console.log('The number is too small');
  }

  match(): void {
    console.log('You won');
    this.running = false;
  }
}

Once MagicNumber is done with their job, jazz music and Game both stop. He picks up the kids, put the beer in the trash can, turns off the lights and everybody goes to sleep. End of the day.

See how profound the implications of this design is ? That's the spirit of OO. It's all metaphoric and anthropomorphic. Actually these two concepts are central to understanding proper OO.

Because objects first exist in the real world, and then in the code.

The Non Developer View

You may have now come to realize that objects may indeed be "things" with "behavior".

And honestly, this might be the closest to object thinking spirit.

Objects are a way to see the world. The same way our world is build of atom, the foundation of molecules and so on, in an Object world, everything is an Object, and every Object is the root of another Object.

First, data are objects. A birthdate is an Object, a person is an Object, a payment is an Object. Second, relationships are objects. A collection is an Object, a binding is an Object. And last, procedures are objects. A sort algorithm is an Object, a random number generator is an Object.

You get the idea.

Everything is an object.

And everything happens through objects.

And these objects come from the real world.

Because the ultimate objective of objects is to model the real world.

To close the gap between the computer view and the human view.

And to unify them under one common language.

A ubiquitous language.

Last updated