Oct 28 2020

Law of Demeter: Always Encapsulate

Exposing internal data structures are bound to lead to a messy situation. When a method caller knows too much about an underlying system, there is a nasty kind of coupling. This dependency issue is relates to the Law of Demeter. Some people call it “too many dots syndrome” because it leads to code.accessing.attributes.very.deeply. The whole point is that the caller should not have knowledge of the deep attribute.

I recently worked on a project that dealt with parsing numbers. Lots of projects do this, right? Another common use-case is to provide some error messages based on the type of incorrect input. Unfortunately, as the business requirements grew, our code lagged behind. This technical debt slowed future velocity and introduced some errors. Eww.

The data structure began something like this:

const product = {
  id: 1,
  name: 'Supreme Vacuum Cleaner',
  quantity: 3,
  price: 100
};

The above structure represents what a user might have in their shopping cart for checkout. It does feel like quantity isn’t actually part of a product and that it should be normalized somehow. Regardless, this was the structure that the team came to a consensus on.

As it turns out, the quantity attribute needs to be a little more complicated. You see, a user might want to input a quantity in a text box. So at this point you might be saying: “Well just make quantity a string”. Sounds good, right? Let’s go ahead and roll with it.

const product = {
  id: 1,
  name: 'Supreme Vacuum Cleaner',
  quantity: '3',
  price: 100
};

Now the user is able to edit the quantity. However, changing the type from a number to a string leads to an issue. Now we cannot calculate the total as easily anymore. So at this point you might be saying: “Just parse the number”. Hmm… there’s a little voice in the back of my head warning me. Eh, whatever. Let’s go ahead and throw this change in too.

const product = {
  id: 1,
  name: 'Supreme Vacuum Cleaner',
  quantity: '3',
  price: 100
};

const total = Number(product.quantity) * product.price;

You’ll also notice that we are adding some scope in. We are fetching the total by parsing the quantity and multiplying by the price. I think now the little voice is speaking up:

“Should it be the job of the caller to calculate the price?”

If two product attributes are being exclusively accessed, maybe we should have a separate module to encapsulate this logic. The module could be named Product, and it could hold a function called getTotal().

In the favor of moving fast, we ended up not creating a module dedicated to product operations. I would say that this was the first substantial warning sign that went unnoticed.

Alright, so now what happens when the user inputs garbage like “asdf” into the text box?

> Number('asdf');
NaN

It’s not the best UX to be displaying NaN as a product total. It’s probably a good call to add a little more safety around quantity. This will allow us to report to the user that they’ve made a mistake.

const product = {
  id: 1,
  name: 'Supreme Vacuum Cleaner',
  quantity: {
    raw: '3',
    value: 3,
    isValid: true,
  },
  price: 100
};

let total;
if (product.quantity.isValid) {
  total = Number(product.quantity.value) * product.price;
} else {
  total = "$--";
}

The const total assignment is getting larger. This doesn’t look too good.

At this point, the total calculation was abstracted away into a function, but it still was not apart of a module specific to a product. This lead to several places in the codebase with similar quantity parsing. As a result, we still did not have a single source of truth.

In the meantime, there’s an edge case that we didn’t cover yet. Can you find it? It’s not quite possible to purchase negative vacuum cleaners. Additionally, there are only so many vacuum cleaners in stock. Therefore, there needs to be some kind of minimum/maximum validation. As a result, we added a validation object to quantity.

const product = {
  id: 1,
  name: 'Supreme Vacuum Cleaner',
  quantity: {
    raw: '3',
    value: 3,
    validation: {
      isValid: true,
      errMessage: ""
    }
  },
  price: 100
};

let total;
if (product.quantity.validation.isValid) {
  total = Number(product.quantity.value) * product.price;
} else {
  total = "$--";
}

let error = null;
if (!product.quantity.validation.isValid) {
  error = product.quantity.validation.errMessage;
}

I am omitting the actual validation logic, but we can pretend that the errMessage will be populated with the expected value. As we can see, our data structure is nowhere near as simple as it once was.

This is where things started breaking. The attribute product.quantity.isValid has now moved to product.quantity.validation.isValid. I believe we had about 10-15 callers trusting the integrity of the product data structure. This is where the lesson is learned.

Never trust the integrity of a data structure.

This Law of Demeter violation could be pretty easily patched with a module dedicated to the product. By having a module along side the complex data structure, we can derive a product’s attributes without requiring knowledge of the underlying implementation.

Furthermore, by following the Single Responsibility Principle, a product can be further split into a couple of concepts:

The first step would be to define some interfaces representing these concepts. The interface ought to be unchanging – serving as a contract. This will ensure stability for the 10-15 areas which utilize this code.

interface Validator {
  isValid: () => bool,
  getErrorMessage: () => string
}

interface Quantity {
  isValid: () => bool,
  getRaw: () => string,
  getValue: () => number,
}

interface Product {
  id: number,
  getQuantity: Quantity,
  setQuantity: (string) => void,
  getPrice: () => number,
  getTotal: () => number,
}

Take a peek and notice Quantity.isValid(). From an implementation standpoint, Quantity would just defer to validation.isValid(). In the case that business requirements become more difficult, additional logic can be added as necessary.

As a result, the 10-15 function calls can let out a sigh of relief. They no longer need to know how exactly a quantity is being validated. All the need to know is product.getQuantity().isValid(). Easy right?

One other thing worth noting, we now have a getTotal() on Product! Now there is a single source of truth for computing a price. :D

Why is encapsulation so powerful? It is adaptive to change.

All the while, each of the 10-15 callers would be none-the-wiser on how these attributes are computed! The interface remains a steadfast contract!

So next time you see code.accessing.attributes.very.deeply, let’s stop for a moment to refactor. Encapsulation is a quick win. Yes it might be a little more verbose, but you will thank yourself down the road.