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
  • Primitives are storage units
  • Strings
  • Numbers
  • Dates
  • Expressivity
  • Performances
  1. Practices

Model the Domain

Create objects that represent domain concepts.

PreviousWrap NullNextWrap Collections

Last updated 7 hours ago

We write programs first and foremost as a communication medium for programmers, and eventually for computers to execute them.

Why do we create higher-level languages ?

It is, arguably, not for performances : languages that add layers on top of the machine are decoupled from the machine and take power from the hands of the developer, such that he cannot write code that will outperform equivalent code written in lower-level language. That's pure logic, but that's measurable.

Hence, when us developers, as an industry, create high-level languages on top of one another, we do it for a very simple reason : enhance expressivity.

Expressivity and higher-level concept allow us to enhance our plane of thinking. When we program in C, we're constantly reasoning in term of memory management and have very little place for thinking about the high-level concepts. When we program in Java, we're relieved from memory management and we raise the level of abstraction.

Same goes for concurrency problems. Dealing with threads is hard. Dealing with coroutines is much easier. It's adapted for a wide range of use cases and help reduce the technical complexity of the product. It leaves less room for errors.

But now, since we have objects that are easy to create, develop and maintain, and who help us create high-level abstractions that communicate behavior, shouldn't we use and abuse it at will and at need ?

Primitives are storage units

Strings

Let's look at some code.

const emailAddress: string = "johndoe@gmail.com"

What's the problem with this code ? It's between the ":" and the "=".

Yes you, string, i'm looking at you.

What the hell are you doing here ? This isn't a place for you to be.

Why ? Because a string has no meaning for us. It only has a meaning for the computer.

Thus, our email address is terribly weak. It's treated as a simple string, yet it is anything except a simple string. An e-mail address is a with rich behavior.

Don't believe me ? Then tell me...

  • How would you validate that e-mail address and make sure it has the proper format ?

  • How would you extract the local part (johndoe) ?

  • How would you extract the domain ?

  • How would you ensure the e-mail belong to a specific domain ?

See, it's more than a string, much more.

The same can be applied to many other common values represented as strings :

  • Urls

  • Passwords

  • Names

  • ISBNs

The list goes on.

In each of these cases, string is a terrible type choice because it's a storage unit, not a type, and certainly not an object.

Have you ever heard of business people talking of strings ?

I certainly haven't.

Hence, emailAddress is dying to be an object. Let's satisfy its needs.

class EmailAddress {
    constructor(private readonly value: string) {
        // validate
    }
    
    function localPart(): string;
    function canonicalAddress(): string;
    function domain(): string;
}

const emailAddress = new EmailAddress("johndoe@gmail.com");

Two patterns are at work here. This is a Value Object maintaining its validity using the Always Valid pattern.

This is elegant, satisfying and very useful. That object can be reused in any program, just as is, and be extremely useful for a wide range of use cases.

Numbers

Look at this code.

const durationInSeconds: number = 60;

See what this pesky number is forcing us to do ? We know have to use a composed name because that number has absolutely no meaning at all. We can't Abandon Composed Names in this case because otherwise, we'd have no idea what that value stands for !

Moreover, a duration is more than just a number. It has a lot of behaviors inside :

  • Arithmetic

  • Comparisons

  • Conversions

I don't want to add more useless cognitive load to my programming activity. Translating business requirements into code is already a complicated job in itself.

So I want to push that value in an object and forget about it. I want a reliable object to take the burden for me, to grab my shoulder and to tell me "don't worry buddy, I've got this".

Here's what I would do.

class Duration {
    private constructor(private readonly seconds: number) {
        // can't be < 0
    }
    
    // Constructors
    static fromSeconds(seconds: number);
    static fromMinutes(minutes: number);
    
    // Arithmetic
    add(duration: Duration);
    sub(duration: Duration);
    
    // Comparison
    eq(duration: Duration);
    gt(duration: Duration);
    
    // Conversions
    toSeconds(): number;
    toMinutes(): number;
}

const duration = Duration.fromSeconds(60);

I don't want to open the constructor, because the internal representation of the duration ought to be private. Maybe one day I'll change my mind and decide to use milliseconds instead of seconds. Should all my code break because of an internal change ? Certainly not.

Dates

Look at this guy.

const departureAt = new Date()

What's departureAt ? A date.

A date ? Any Date ? So I can set a date in the past, or a date in year 3000 ?

See how demure is that code ? It's so timid it harshly says anything. All I know is that it represent a departure date, without telling me what is a departure date.

What's the cost of increasing the knowledge in our code ? Barely creating an object.

class DepartureDate {
    private constructor(private readonly value: Date) {}
    
    static create(date: Date, now: Date) {
        if (date < now) {
            throw new Error("Departure Date cannot be in the past");
        }
        if (date > now.addMonths(12)) {
            throw new Error("Departure Date cannot be too far in the future");
        }
    }
}

// somewhere else
const departureAt = DepartureDate.create(desiredDate, Date.now());

By adding a dozen lines we documented an astonishing amount of behavior (without exaggerations) and gathered the rules of the concept inside an object representing the concept. That's Domain Modeling for you.

Did you notice how the DepartureDate.create methods takes the current date as the second parameter ? That's because we Declare Dependencies. Declared dependencies makes reasoning about the code simpler and enable testability.

Expressivity

By rising the level of abstraction in our program, by which I really mean reasoning in term of high quality objects, we create a domain language.

That domain languages documents the software and express what's possible in our universe and what's not.

It allows us to gather the rules of each specific concept as close as possible to the object, releasing other objects from that knowledge, evermore spreading the responsibilities between objects and keeping knowledge as low as possible. Each object becomes really simple to use and work with.

Do not underestimate the main responsibility of code : to communicate about the domain, to communicate about it's intent, to document the software, to help programmers understand the limits of the system and to reduce their cognitive load.

All of that matters much more than any other considerations.

Performances

Some astute software developers may argue :

what about the performances ? By adding layers of indirections, we're at risk of cluttering the call stack !

How mindful to think about the call stack. But here's a question for you : what's the cost of software that's hard to maintain and evolve, hard to understand, for which a lot more developers are required than should be ? How many softwares didn't even reach the light before collapsing under it's own weight ?

What's the cost of a few lost call-stack on machines that can run hundred of gigabytes of RAM ?

Let the numbers talk, and measure before optimizing.

How would you get the canonical address (getting rid of ) ?

That bill Probably trillions.

very complex structure
plus addressing
amounts to millions of dollars.