28

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.

4
  • If you need a guideline, then this should work. – Laiv Feb 17 at 15:37
  • @laiv that's just structural thinking. That could be enforced by a static code analyzer. Java 8 Streams prove that there are good exceptions to these rules. It's best to understand why the rules exist. – candied_orange Feb 17 at 16:12
  • 5
    What about a 3-legged dog? Or an account with 2 users? – Erik Eidt Feb 17 at 17:14
  • For internal code I try to make my code follow the "worse" example. Hiding knowledge is good for consumers, but not so helpful when trying to debug or extend. – user368564 Feb 19 at 1:51
38
# 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.

18
  • 18
    @RobertBräutigam the law is clear, but it's a rule of thumb, not an actual law. The question isn't "what does the law say?", but "should I listen to the law in this case?". It should make you ask questions like this, not automatically refactor every line with two or more dots. – Jacob Raihle Feb 17 at 11:59
  • 21
    @RobertBräutigam "Calling methods on instance variables of other objects is not allowed." The Law of Demeter is not a dot counting exercise. You cannot and should not judge LOD violations based on the accessing code itself. The surrounding context matters in making the accessing code applicable or non-applicable to LOD considerations. – Flater Feb 17 at 12:05
  • 4
    @RobertBräutigam: It seems you misunderstood my answer. The implicit expectation of the reader of the example code does matter, because we (as the reader) have to infer what the intended contract of the Dog and Account classes is. Example code in questions is usually light on rigorous analysis documentation that would shed light on the things that we are now left to infer. So whether or not we designate this example code as a LOD violation or not, hinges on our implicit inference on what is the contract and what is a private implementation detail. – Flater Feb 17 at 13:28
  • 3
    This is a great answer and illustrates why dogmatic adherence to the LoD is generally misguided. – user949300 Feb 18 at 7:36
  • 3
    @Laiv: People always tersely summarize LOD as "don't talk to strangers", but I'd argue that it's more a matter of "don't handle others' privates, even if they've exposed them". But that's maybe a less SFW interpretation. – Flater Feb 18 at 10:28
26

The LoD or principle of least knowledge has nothing to do with immutability. It’s about decoupling systems by decreasing indirect dependencies.

In your dog case, you only need to know about dogs to make your chained invocations.

In your account case, when some module is working with accounts, it also needs to know about users, and perhaps even about addresses. So it’s not just about friends but also about friends of friends. This leads to complex and time-consuming change propagation: e.g. a change in the address class may impact user class and account class.

The way out is to tell objects what they should do without micromanaging the details:

account.print();  // account will invoke user.print()

This is of course easier said than done, because this may create other issues (e.g. bloated interfaces if you need printSumary() and printDetails()). And sometimes, you just need to know about friends of closer friends. In the end the design will not be about respecting all the “laws” but about balancing different principles to get an optimal fit.

4

Adding more methods to an interface is not always the solution to the Law of Demeter. You do not need to distinguish these cases, you need to find a different way to resolve them.

From the wikipedia page:

On the other hand, at the class level, if the LoD is not used correctly, wide (i.e. enlarged) interfaces may be developed that require introducing many auxiliary methods. This is due to poor design rather than a consequence of the LoD per se. If a wrapper method is being used, it means that the object being called through the wrapper should have been a dependency in the calling class.

Take your second example:

print(account.user().fullName())
print(account.user().socialSecurityNumber())

Does your caller here need to interact with an account at all? Remove account as a dependency and provide user as an argument instead:

print(user.fullname())
print(user.socialSecurityNumber())
1
  • Indeed, it’s all a question of balance between the different principles and needs. In fact, adding new methods like that may even create a hidden coupling (e.g. if for privacy reasons you’d get rid of social security in the user, you’d have to change a lot of methods. – Christophe Feb 21 at 21:26
0

Decide Who Your Neighbors Are

The premise of this law is not to avoid "chaining" of function calls (or property/field access, etc.), but rather to limit the "reach" of one class of objects. In the case of the walking dog, the caller must know about the dog, a collective structure that contains all of the legs, and about an individual leg. This requires tight coupling between the consumer and the leg details, so it's better to let the dog handle its own legs.

Starting at the beginning

User Robert Bräutigam linked to the original paper in a comment to another answer. I thought it best to transcribe the actual text of the "law" here:

For all classes C, and for all methods M attached to C, all objects to which M sends a message must be instances of classes associated with the following classes:

  1. The argument classes of M (including C).
  2. The instance variable classes of C. (Objects created by M, or by functions or methods which M calls, and objects in global variables are considers as arguments of M.)

K. Lieberherr, I. Holland, and A. Riel. 1988. Object-oriented programming: an objective sense of style. In Conference proceedings on Object-oriented programming systems, languages and applications (OOPSLA '88). Association for Computing Machinery, New York, NY, USA, 323–334. DOI:https://doi.org/10.1145/62083.62113

Classes, not Instances

To apply the law, look at the classes* that are known to the consuming class C. If C stores an instance of class A, or can access an instance of A through global/hierarchical scope, or accepts an instance of A in a given method, then the damage is done. That method of C already "knows about" A, and depends on the contract it provides. That consumer method may call any method of any instance of A, no matter where it came from (or how complex it was to find it).

This is what differentiates the dog example from, say, fluent APIs. in a fluent API, you may chain a dozen function calls together, but each return value tends to be an instance of the same class! Each successive result may be a new instance, but the point is that there is only one class. You're counting classes, not instances or consecutive invocations.

*Presumably, this applies to all manner of types including specific interfaces; you don't "know about" MySqlDbConnection just because you know about DbConnection.

The Spirit of the Law

Does the dog example break the Law of Demeter? We technically don't know, but it probably does. We don't have much information about the consuming context; does it "know about" dog legs because of some instance field we can't see? If so, the law permits the consumer to call any method of any leg.

Thus, the problem with the dog example isn't that the consumer accesses a deeply nested function (a direction for one particular among a set of legs on a dog), but that the consumer has to understand the concepts of a dog and of a set of legs and of individual legs. You don't fix it by hiding method calls, you fix it by decoupling from the classes that contain those methods.

The Law of Demeter doesn't reduce coupling so much as it it keeps coupling explicit. From there, you can more easily detect and address issues related to coupling.

What about Wallets?

A popular analogy (apparently created by David Bock) when talking about the Law of Demeter is that of a paper boy that is owed money by a customer. The paper boy doesn't ask for a wallet so that he can grab the money, he just asks for the money. Who cares if the customer keeps it in a wallet, and why should the paper boy have to get the money from it anyway?

Interestingly, if the paper boy has a wallet of his own, this does not break the Law of Demeter! The paper boy knows how to use a wallet, so he knows how to use the customer's wallet. The real lesson of this scenario is to assign responsibilities properly and to keep implementation details (such as wallet vs. money clip vs. wad of bills) private.

Tl;dr

Here are some possibilities for the name example, vis-à-vis the Law of Demeter, assuming account.user() returns type User.

  1. Maybe there is nothing wrong with account.user().fullName()! If the consumer does other work involving Users, then the consumer has already "paid the price" of depending on the User class contract.
  2. If it's not explicit now, then make it explicit! Work with the user directly - don't go through account. Instead of calling account.user(), have the consuming method explicitly depend on one User instance as a parameter. Now, even if account.user() suddenly returns a different class, that's somebody else's problem. The consumer can focus on working with a User. Notably, this only "adds" coupling to User for the consuming method in question, not the whole consuming class.
  3. You could create a dedicated interface for situations like this. The consumer doesn't rely on a the User, but only on some abstraction that carries a smaller contract than a full User.

Not the answer you're looking for? Browse other questions tagged or ask your own question.