Object-oriented – Law of Demeter and over-wide interfaces

code smelldesignlaw-of-demeterobject-oriented

The Law of Demeter makes sense in some obvious cases.

# better
dog.walk()

# worse
dog.legs().front().left().move()
dog.legs().back().right().move()
# etc.

But in other cases it seems to lead to an interface that is too wide, and doesn't really feel like it "hides" any knowledge.

# Is this worse?
print(account.user().fullName())
print(account.user().socialSecurityNumber())

# Is this better?
print(account.userFullname())
print(account.userSocialSecurityNumber())

It feels like account has methods for things that really aren't anything to do with accounts now, just to avoid a technical violation of the Law of Demeter. Instictively, account.userFullName() is pretty "code smelly".

Is anyone aware of any more specific guidelines or refinements of the LoD that helps account for the difference between the "dog" cases where the principle clearly makes sense, and the cases where it doesn't? And how do you avoid the LoD leading to over-wide interfaces?

One principle I have heard is that it matters less in a context of immutability, but many have disputed this.

Best Answer

# better
dog.walk()

# worse
dog.legs().front().left().move()
dog.legs().back().right().move()
# etc.

There are two reasons why the second is worse. The first, not really directly LOD-related, is that your walk logic isn't reusable, which is a problem in the case where there are multiple places in your codebase where a dog must walk.

The LOD-related reason why this code is bad is because it forces the current consumer to know that DogLeg exists and how to operate it. The unspoken expectation here is that your consumer only knows about a Dog, and that it can be made to move around, but how that Dog moves around isn't something the consumer cares about (that's up to the Dog to manage for themselves).

However, that is not necessarily the case for your other example.

account.user().fullName()

If Account and User are both domain objects of which your consumer has public knowledge, then there's no issue with asking them to handle a User object directly.
The expectation here is that "the account refers to its owner" is part of the Account interface, and therefore returning the owner (represented by a User object) is fair game.

Comparatively, your Dog interface is not expected to include "the dog has legs", but rather "the dog is able to move around", and the legs are just an implementation detail so the dog is able to fulfill its contract (i.e. moving around). The interface itself doesn't specify the existence of legs, and therefore the consumer of Dog shouldn't be relying on the existence of legs.

In essence, a DogLeg is considered a private implementation detail, whereas a User (class) is publically known. This means that there's significantly less issue with expecting your consumer to handle a User than there is with expecting them to handle a DogLeg.

That being said, if account.user() was actually an AccountUser object which would also be considered a private implementation detail, then the same principle applies as it does for DogLeg.

This is what makes LOD so tricky to pinpoint. It's not something that is objectively true based on your code alone, it hinges on subjective context and expectation of interfaces/contracts. By renaming the code, you change the reader's implicit expectation, which can change whether something is considered an LOD violation.

dog.legs().front()
account.user().fullName()

Technically, it's the same code. But what changes is our expectation of how acceptable it is to force a consumer to directly handler a DogLeg vs forcing them to handle a User.