Pragmatic Objects
  • Pragmatic Objects
  • Design
    • What's an Object ?
    • Thinking Object
    • Domain Modeling
    • Information Management
  • Practices
    • The Pragmatic Practices
    • No Getters & Setters
    • Inherit Wisely
    • Wrap Null
    • Model the Domain
    • Wrap Collections
    • Expose Snapshots
    • Abandon Composed Names
    • Don't Check Types
    • Minimize Knowledge
    • Immutability
    • Separate Commands & Queries
    • Abandon Statics
    • Declare Dependencies
    • Separate Instantiation from Usage
  • Patterns
    • Always Valid
    • Wrapper
    • Command
    • Procedural Object
    • Compute Object
    • Snapshot
    • Value Object
    • Observability
  • Examples
    • Celsiuses / Fahrenheits
Powered by GitBook
On this page
  • The Commoner View
  • The Tech-Savvy View
  • The Non Developer View
  1. Design

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 recognise 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 by 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 + 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.

Anyway, 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", optimise 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... blah, blah blah.

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 most 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, optimised and made digestible for humans to use.

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

That's to say these tools will help you do 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 TypeScript is obviously 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 GameEvents {
  tooBig(): void;

  tooSmall(): void;

  match(): void;
}

class MagicNumber {
  private value: number;

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

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

interface AttemptSource {
  nextAttempt(): Promise<number>;
}

class ConsoleAttemptSource implements AttemptSource {
  constructor() {
    prompt.start();
  }

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

class Guess {
  private value: number;

  constructor(private readonly attemptSource: AttemptSource) {}

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

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

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

class Game implements GameEvents {
  private running = true;
  private attemptSource = new ConsoleAttemptSource();
  private magicNumber = new MagicNumber(this);

  run() {
    while (this.running) {
      const guess = new Guess(this.attemptSource);
      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.attemptSource);
    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 attemptSource: AttemptSource) {}

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

  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 an AttemptSource. What's an AttemptSource ? We don't know and we don't care. All Guess needs is a collaborator whose responsibility is to provide a number, an attempt. It's not his job to do it, he has enough work to do protecting the value given back. After all, a guess is a value.

So what's the job of Guess ? It's to compare itself to a number and tell the caller wether the thing it protects is greater or lower than a given number. He's part of an object society and has a very well defined responsibility.

What's a MagicNumber ? Another citizen of our object society.

class MagicNumber {
  private value: number;

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

  compare(guess: Guess) {
    if (guess.isGreaterThan(this.value)) {
      this.gameEvents.tooBig();
    } else if (guess.isLessThan(this.value)) {
      this.gameEvents.tooSmall();
    } else {
      this.gameEvents.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 concept of our domain with rich behavior. Like all objects citizen, it only exists to serve another object. Keep in mind that an object always exist in the context of another object.

Same goes for MagicNumber.

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 GameEvents {
  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 GameEvents {
  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.

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, values 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.

To be more rigorous, every one of these are candidates for being objects. It turns out that the very first big idea behind object was to model the real world in our code in order to reduce the complexity of understanding and manipulating a software system. It turns out, when a software model align exactly with the mental model of a user, using that software becomes awfully easy.

However, that's just one part of the scene. For pragmatic, technical reasons, many constraints lead us to adapt our software design and to get rid of some objects, or to give birth to objects that are not necessarily part of a user's mental model but make sense as part of the software design.

The other main big idea of object-orientation, as guided by Domain-Driven Design, is to align the mental model of the software stakeholders (including the Domain Experts) and the software model. That means, creating a ubiquitous language and use that language as much as possible in the code to reflect that shared understanding.

That's how you make software that is more aligned with the needs of its users.

PreviousPragmatic ObjectsNextThinking Object

Last updated 11 hours ago

That's a very important computer-science concept called . They not objects at all but they're useful to mimic them to some extent.

Abstract Data Types