DEV Community

Matthew Brookson
Matthew Brookson

Posted on • Originally published at mbrookson.uk

Tell, don't ask: Domain-driven code refactoring

I bang on about domain-driven design a lot. It’s an interesting and vast topic and can be difficult to know where to start. Especially if you’re working on an existing project with lots of existing code.

Turns out there are some simple but effective ways to start implementing some DDD concepts into any codebase immediately.

One of these is “Tell, don’t ask”. Let’s discuss.

// Domain object
class Person {
  private isWearingSocks: boolean;

  canPutOnSocks() {
    return !this.isWearingSocks;
  }

  putOnSocks() {
    this.isWearingSocks = true;
    this.publish(new PersonPutOnSocksEvent());
  }

  removeSocks() {
    this.isWearingSocks = false;
    this.publish(new PersonRemovedSocksEvent());
  }
}

// Calling code
let canPutOnSocks = person.canPutOnSocks();

if (canPutOnSocks) {
  person.putOnSocks();
} else {
  throw new PersonCannotPutOnSocksError();
}
Enter fullscreen mode Exit fullscreen mode

Here we have a person instance and some logic to decide whether the person wear some socks. There is nothing wrong with this at a first glance. The code works and the rules are checked as they should. But let’s think about it a little more.

Can a person put socks on?

What if we have two places where we call the putOnSocks function? Can the person put socks on? We would probably need to check using an if statement everywhere we want to call this function in case they can’t. Code duplication isn’t always a bad thing, but this is certainly something to keep in mind.

Why? Why? Why?

Another consideration is that although we’re checking whether the person can put socks on, if they can’t we are throwing a PersonCannotPutOnSocksError error. This is also fine, but it’s not very helpful. Why can’t they put socks on?

Breaking the rules

Most importantly, what we’ve discovered here is some domain logic. More specifically, there’s an invariant which is a validation rule that must be enforced by the domain object for it to be in a valid state.

In this case we have a rule that a person cannot put socks in if they already have socks on. The rule was already there in the original code but it was outside of the person class and would need to be repeated everywhere to ensure the rules were followed. If the rules are not enforced we would publish two consecutive PersonPutOnSocksEvent events. Can you put socks on if you haven’t taken the previous socks off? (Alright smarty pants, maybe you technically could but not in my made-up scenario). And what about removing socks? Can you remove socks if you’re not wearing any? Certainly not! So we definitely shouldn’t be able to raise consecutive PersonRemovedSocksEvent events, but the code allows us to do just that. We’re essentially leaving our code open to logic bugs.

Let’s fix it.

Tell, don’t ask

Let’s refactor this code using the “Tell, don’t ask” approach.

// Domain object
class Person {
  private isWearingSocks: boolean;

  private canPutOnSocks() {
    return !this.isWearingSocks;
  }

  putOnSocks() {
    if (this.isWearingSocks) {
      throw new PersonAlreadyWearingSocksError();
    }
    this.isWearingSocks = true;
    this.publish(new PersonPutOnSocksEvent());
  }

  removeSocks() {
    if (!this.isWearingSocks) {
      throw new PersonNotWearingSocksError();
    }

    this.isWearingSocks = false;
    this.publish(new PersonRemovedSocksEvent());
  }
}

// Calling code
person.putOnSocks();
Enter fullscreen mode Exit fullscreen mode

Firstly you’ll probably notice there is less logic in the calling code. Why’s that? Essentially we’ve encapsulated our domain logic, moving it inside the domain object itself rather than calling it all from outside. This may seem trivial, but there are some major benefits.

Privacy first

Since we’re no longer asking whether the person can put on socks we are able to make the canPutOnSocks function private. This isn’t required and we could still expose this function if needed, but what it highlights is that we have more fine grained control over what behaviours and functionality we want to expose from our domain objects. Generally speaking, we should aim to expose as little as possible.

Enforcing invariants

By encapsulating the domain logic inside the person class we now have better control over the invariants and can enforce them within the class itself. For example, now we can tell the person class to put on socks without asking first. The domain object is now responsible for ensuring that the person is not already wearing socks. Now the logic is encapsulated we can more easily throw a specific error too since we know why the person cannot put on socks - because they’re already wearing some. The same logic applies to removing socks as mentioned in the previous section.

Since we’re now encapsulating this logic we’re also ensuring that the correct domain events are published. We can no longer publish consecutive PersonPutOnSocksEvent or PersonRemovedSocksEvent events. We would need to tell the person to remove their socks before putting socks on.

Testing

Before we had logic outside of our domain object. How would we test it? We’d need to test our calling code to ensure the invariants are enforced. What if we miss one? What if there’s a mistake or a conflict of rules? Now we’re refactored the code, our encapsulated domain logic is easily testable in one place.

To conclude

Hopefully this over-simplified example demonstrates why the “Tell, don’t ask” approach can be useful when writing code. It’s a simple and effective way to write concise domain objects, enforce invariants and make your code robust and testable.

It’s something you can implement whether you’re starting a project from scratch or making changes to an existing codebase. You don’t need to rewrite everything but you can introduce this pattern as small, incremental refactors.

If you found this interesting or useful then drop me a follow on Twitter @matthewbrookson.

You can find more about domain-driven design in my ebook at https://mbrookson.uk/products/dicovering-ddd. Check it out!

Top comments (0)