Principle of least knowledge

Author pic

Written By - Garvit Maloo

29 October, 2024

Getting to work on a codebase that is easy to maintain and extend is a dream of every software engineer. But how often do we get to work on such codebase? I have experience of working on both the types of codebase - ones which are easy to maintain and extend and the ones which are not. And sometimes, its the small things which can make a codebase easy to maintain and extend and the law of demeter is one of those small things. So, let's understand the law of demeter in detail in this blog.

The law of Demeter is known by many names like "The principle of least knowledge" and "Don't talk to strangers" and even something like "Shy Programming". This law defines how an object should interact with another object. This law states two main things - A method in a class should only interact with -

  1. Objects that are passed as arguments to the methods.
  2. Properties of the class.

It is also allowed to work with objects made within that method or if absolutely necessary, global objects as well. By implementing this, we make sure that objects know as little about other objects as possible. This makes them independent and changes in other classes are less likely to affect this class.

Let us understand the problem and how this law can solve it with the help of an example. Consider a scenario of an ECommerce platform where an order is placed by a user, payment is made and a confirmation email is sent to the user about the order.

class PaymentService {
    public void charge(String creditCardNumber, double amount) {
        System.out.println("Charging " + creditCardNumber + " for $" + amount);
    }
}

class NotificationService {
    public void sendEmail(String email, String message) {
        System.out.println("Sending email to " + email + ": " + message);
    }
}

class User {
    private String email;
    private String creditCardNumber;

    public String getEmail() {
        return email;
    }

    public String getCreditCardNumber() {
        return creditCardNumber;
    }
}

class Order {
    private User user;
    private double amount;

    public User getUser() {
        return user;
    }

    public double getAmount() {
        return amount;
    }
}

class OrderProcessor {
    private PaymentService paymentService = new PaymentService();
    private NotificationService notificationService = new NotificationService();

    public void processOrder(Order order) {
        // Violates the Law of Demeter
        String creditCard = order.getUser().getCreditCardNumber();
        paymentService.charge(creditCard, order.getAmount());

        // Another violation: reaching into User to get email
        String email = order.getUser().getEmail();
        notificationService.sendEmail(email, "Your order has been processed.");
    }
}

Just to make it easy to remember, we want to avoid drilling into other objects by chaining methods as in this example. This makes OrderProcessor class vulnerable to changes in the User class. Suppose, the User class grows and we decide to change the structure and split the User class such that email is now stored in a separate class called ContactInfo as shown here -

class ContactInfo {
  private String email;
  private String phoneNumber;

  public String getEmail(){
    return this.email;
  }

  public String getPhoneNumber(){
    return this.phoneNumber;
  }
}

class User {
  private ContactInfo contactDetails;
  private String creditCardNumber;

  public ContactInfo getContactDetails {
    return this.contactDetails; // access email through this object
  }

  public String getCreditCardNumber() {
    return creditCardNumber;
  }
}

If we re-structure the User class like so, we will have to make changes in the OrderProcessor class also, because now getEmail() method is not available in the User class. Hence, this is not the best way. So, let's see how the law of demeter solves this problem.

class PaymentService {
    public void charge(String creditCardNumber, double amount) {
        System.out.println("Charging " + creditCardNumber + " for $" + amount);
    }
}

class NotificationService {
    public void sendEmail(String email, String message) {
        System.out.println("Sending email to " + email + ": " + message);
    }
}

class User {
    private String email;
    private String creditCardNumber;

    // expose behavior
    public void chargeUser(PaymentService paymentService, double amount){
      paymentService.charge(this.creditCardNumber, amount);
    }

    // expose behavior
    public void notifyUser(NotificationService notificationService, String message){
      notificationService.sendEmail(this.email, message);
    }
}

class Order {
    private User user;
    private double amount;

    public void makePayment(PaymentService paymentService){
      user.chargeUser(paymentService, amount);
    }

    public void sendNotification(NotificationService notificationService, String message){
      user.notifyUser(notificationService, message);
    }

    public double getAmount() {
        return amount;
    }
}

class OrderProcessor {
    private PaymentService paymentService = new PaymentService();
    private NotificationService notificationService = new NotificationService();

    public void processOrder(Order order) {
        order.makePayment(paymentService);
        order.sendNotification(notificationService, "Your order has been processed.")
    }
}

Now, even if we make any structural changes in the user class as we made above, it won't affect anything else. Let's try this with the new code -

class ContactInfo {
  private String email;
  private String phoneNumber;

  public String getEmail(){
    return this.email;
  }

  public String getPhoneNumber(){
    return this.phoneNumber;
  }
}

class User {
    private ContactInfo contactInfo;
    private String creditCardNumber;

    public void chargeUser(PaymentService paymentService, double amount){
      paymentService.charge(this.creditCardNumber, amount);
    }

    public void notifyUser(NotificationService notificationService, String message){
      notificationService.sendEmail(this.contactInfo.getEmail(), message);
    }
}

That's it! We really need not to change anything else.

But the big question - How we managed to make a drastic structural change without breaking anything elsewhere? That's because the OrderProcessor class only knows about the behavior and not the actual details of the User class. the chargeUser method in the User class is the behavior and rest all the details of the User class are well-encapsulated within the class itself and is not being used anywhere else, which makes it easier to make changes in the User class without breaking anything else.

So, to summarize the story so far, please don't let one class know too much details of the other class. Do not chain methods of different classes to drill some information. This will pay off when the codebase grows and you have to refactor the code.

That's it for this blog guys! I hope I was able to make it clear what is this law about and why it is important. Stay tuned for much such content!

Liked the content? Share it with your friends!
Share on LinkedIn
Share on WhatsApp
Share on Telegram

Related Posts

See All