The Law of Demeter

John Meschke | 2019-02-14

Background

Have you ever been programming something that seems pretty standard, but the more you work with fleshing out the design and getting it to work with other code, you begin to get a feeling that something isnít quite right? You try to figure out what it is, put it into words, then search to see if anybody else has had the same feeling. Eventually you discover that there is a design pattern or work-around to handle similar cases, but why didn't you hear about this method before?

This is exactly how the Law of Demeter had entered into my programming tool belt. Some of my objects were exposing too much of the inner workings to the outside world. By limiting the access to objects and taking a "ask, don't tell" approach, the pieces began to fit together much more securely.

This design pattern may work with many modern programming languages, but it is most understandable and applicable in C-like languages. Java provides a great opportunity for studying this concept because of its pure object-oriented nature. So let's use Java for the rest of this discussion.

Explanation

To explain Law of Demeter in the easiest way I can, it's allowing outside objects to be passed via method parameter to do a task instead of exposing implementation details to do the same task. Other explanations from around the web strip it down to "using only one dot" when invoking methods, as to not have a massive chain of back-to-back method calls. I think this is too basic, and we'll see why later.

Various resources around the web have a more complete definition, but we want to keep things simple by looking at examples that work and some that don't. I highly suggest researching other sources to get a more structurally sound understanding and to hear the thoughts of other programmers.

Violating the Law

If you're like me, a simple definition still won't suffice in understanding a concept. Visual learning lends itself to better comprehension. Let's take a look at the example of a bare-bones computer class.

package com.onwardideas.hardware;

public class Computer
{
    private final HardDrive hardDrive;
    private final Memory memory;
    private final Processor processor;
    private boolean active;

    public Computer()
    {
        hardDrive = new HardDrive();
        memory = new Memory();
        processor = new Processor();
        active = false;
    }

    public HardDrive getHardDrive()
    {
        return hardDrive;
    }

    public Memory getMemory()
    {
        return memory;
    }

    public Processor getProcessor()
    {
        return processor;
    }

    public void powerButton()
    {
	active = !active;
        processor.power(active);
        hardDrive.power(active);
        memory.power(active);
    }

    public boolean isOn()
    {
        return active;
    }
}

And here is a method of a user class that operates the computer when the it is turned on.

public void operate(Computer computer, Task task)
{
    if (computer.isOn())
    {
        computer.getHardDrive().loadProgram(task);
        computer.getMemory().loadData(task);
        computer.getProcessor().run(task);
        computer.getMemory().saveData(task);
    }
}

There are some problems with this design. First, it allows outside code to modify the state of the object from a distance, meaning we can grab a component of the object and modify that component which in turn affects how the object works. This has potential to become a debugging nightmare.

Anybody anywhere can modify the internal state without much thought. Imagine what would happen if some code elsewhere decided it would be a good idea to shut down the hard drive to save power. The computer is still flagged as on, but when the user attempts to operate their task, they've got a hard drive that isn't ready.

The second problem is that the object is letting the outside world have too much control. The components are only needed for specific purposes, so why do we grant access to them at all? The interface is more complicated than it needs to be.

Observing the Law

The computer with all its components is supposed to act as a cohesive unit that does certain jobs. What we previously had was nothing of the sort. This is one way the Law of Demeter comes to help. By obeying the law, we can rectify the major problems. We ask the object to do something rather than telling its components what to do.

package com.onwardideas.hardware;

import com.onwardideas.process.Task;

public class Computer
{
    private final HardDrive hardDrive;
    private final Memory memory;
    private final Processor processor;
    private boolean active;

    public Computer()
    {
        hardDrive = new HardDrive();
        memory = new Memory();
        processor = new Processor();
        active = false;
    }

    public void powerButton()
    {
	active = !active;
        processor.power(active);
        hardDrive.power(active);
        memory.power(active);
    }

    public boolean isOn()
    {
        return active;
    }

    public void operate(Task task)
    {
        if (active)
        {
            hardDrive.loadProgram(task);
            memory.loadData(task);
            processor.run(task);
            memory.saveData(task);
        }
    }
}

The original computer class was little more than a bag of data, but it now combines its data with behavior. This is much more compact and secure. It also follows the idea of having only one dot to complete a complex operation. The design is also much less anemic, a programming paradigm to talk about for a another day.

Exceptions to the Rule

If we follow this design to every possible case, we will eventually find something causing more trouble than its worth. Consider the container below. The more objects needing to operate on the data in the container, the more cumbersome and bloated it becomes. The number of dependencies also increases.

package com.onwardideas.automobiles;

import com.onwardideas.clean.CarWasher;
import com.onwardideas.paint.Sprayer;
import com.onwardideas.service.Dealer;

public class AutoGroup
{
    private final Auto[] autos;
    private int size;

    public AutoGroup(int capacity)
    {
        autos = new Auto[capacity];
        size = 0;
    }

    public void add(Auto auto)
    {
        if (size < autos.length)
        {
            autos[size] = auto;
            size++;
        }
    }

    public void wash(CarWasher washer)
    {
        for (int i = 0; i < autos.length; i++)
        {
            Auto auto = autos[i];
            washer.wash(auto);
        }
    }

    public void paint(Sprayer sprayer)
    {
        for (int i = 0; i < autos.length; i++)
        {
            Auto auto = autos[i];
            sprayer.paint(auto);
        }
    }

    public void changeOil(Dealer dealer)
    {
        for (int i = 0; i < autos.length; i++)
        {
            Auto auto = autos[i];
            dealer.changeOil(auto);
        }
    }

    // More methods added as more objects need the container.
}

This is where we need an exception to the rule, or at least a more defined set of rules for the law to be practical. The following premise is proposed: if we allow access to the object's data, we should not be able to mutate the state of the object with this data.

Containers are one such example where this premise is almost ubiquitous. They serve as a collection of data to whatever system needs them, but remain ignorant about the objects that need to use their data.

public void updateTrackingNumber(int number)
{
    for (int i = 0; i < equipmentList.size(); i++)
    {
        // This is fine. The container is not modified.
        equipmentList.get(i).setTrackingNumber(number);
    }
}

We can do whatever we want with the data in the container, but it won't modify the actual state of the container. We can also use multiple dots safely, so the basic description mentioned earlier really doesn't think of all cases.

Let's imagine another example using rigid bodies for a physics system. The bodies have a transform (position, rotation, scale) and an attached immutable shape object that can be shared among other bodies so we aren't leaving such a large memory footprint. There's no way to know ahead of time how many different areas of the program will use this information, so having methods with other objects in their signatures for all expected behavior is impractical. We allow these parts of the rigid body to be implemented as a data bag.

package com.onwardideas.physics;

public class RigidBody
{
    private final Shape shape;
    private Transform transform;

    public RigidBody(Shape shape)
    {
        this.shape = shape;
        transform = new Transform(0.0F, 0.0F, 0.0F, 0.0F);
    }

    public void setTransform(Transform transform)
    {
        this.transform = new Transform(transform);
    }

    public Transform getTransform()
    {
        return new Transform(transform);
    }

    public Shape getShape()
    {
        return shape;
    }

    public Rectangle2D getBoundingBox()
    {
        float minX = transform.position.x - shape.getHalfWidth();
        float minY = transform.position.y - shape.getHalfHeight();
        float maxX = transform.position.x + shape.getHalfWidth();
        float maxY = transform.position.y + shape.getHalfHeight();

        return new Rectangle2D(minX, minY, maxX, maxY);
    }
}

As long as our shape object is immutable, there's nothing to fear. We won't be able to modify the internal rigid body state from a distance. We can give the shape to other systems that would best know how to use it, and they won't be able to make changes. The method to calculate a bounding box should always give us the expected result.

public Rectangle2D getBiggerBoundingBox(RigidBody body1, RigidBody body2)
{
    boolean firstIsLarger = compareArea(body1.getShape(), body2.getShape());
    Rectangle2D rectangle1 = body1.getBoundingBox();
    Rectangle2D rectangle2 = body2.getBoundingBox();

    return (firstIsLarger) ? rectangle1 : rectangle2;
}

This is the most practical approach for our rigid body design. When we need to detect collisions between bodies, we're better off passing them to a collision dispatcher to find the appropriate algorithm to determine the concrete shape types and to run the appropriate algorithms.

Conclusion

Use the Law of Demeter to hide functionality that doesn't need to be exposed or shared. Containers that will be passed around and used by other objects are a notable exception. Allowing access to its contents will not modify the container's state. There are certainly other exceptions, so you will have to decide if this design principle will simplify or complicate things.