Hide Delegate

In this post, I want to talk about the refactoring Hide Delegate. I was prompted to write this by some code I reviewed around the time of writing this post where there were a number of instances of objects that were leaking their internal state. I did a post on Tell, Don’t Ask and what I saw in the code review got me thinking about it more. Hide Delegate is one refactoring we can use to mitigate against this situation of objects failing to encapsulate themselves. It is related to the principles Tell, Don’t Ask and the Law of Demeter, and the code smells Inappropriate Intimacy and Train Wreck.

Objects asking for information from another object in order to do some work on its behalf leads to increased coupling and fragility. Coupling increases through clients taking on responsibilities that should belong on the class where the information they’re requesting lives. Fragility creeps in when many objects are doing this, one change can ripple through many parts of the code.

I made another contrived example that models some sort of establishment with rooms that can be booked by members. Each Room controls its own availability with the help of a Bookings object that tracks which days it’s booked on.

Below is an example of 3 clients of the Room class. These are the Auditorium class and some scheduled jobs such as RecurringBookingScheduler and WaitingListBookingScheduler:

class Auditorium {
    private final Room room;

    Auditorium(Room room) {
        this.room = room;
    }

    void confirmBooking(Booking booking) {
        room.bookings.addBooking(booking);
    }
}

class RecurringBookingScheduler {
    private final Map<Room, ClubMember> recurringBookings;

    RecurringBookingScheduler(Map<Room, ClubMember> recurringBookings) {
        this.recurringBookings = recurringBookings;
    }

    void processRecurringBookings() {
        for (Map.Entry<Room, ClubMember> entry : recurringBookings.entrySet()) {
            LocalDate today = LocalDate.now();
            Room room = entry.getKey();
            ClubMember member = entry.getValue();
            if (room.bookings.hasAvailabilityOn(today)) {
                room.bookings.addBooking(new Booking(member, today));
            }
        }
    }
}

class WaitingListBookingScheduler {
    private final List<Room> rooms;
    private final Stack<ClubMember> waitingList;

    WaitingListBookingScheduler(List<Room> rooms, Stack<ClubMember> waitingList) {
        this.rooms = rooms;
        this.waitingList = waitingList;
    }

    void processWaitingList() {
        LocalDate today = LocalDate.now();

        for (Room room : rooms) {
            if (waitingList.size() >= 1 && room.availableOnDate(today)) {
                ClubMember member = waitingList.pop();
                room.bookings.addBooking(new Booking(member, today));
            }
        }
    }
}

You can see in the above example that every client is calling room.bookings.addBooking(). This means that they all know about an internal detail of the Room class, that it has a field called bookings of type Bookings and that they can use it to add a booking. This raises some questions about maintainability:

  • What if we need to add some additional functionality to make sure a booking can be added. Say auditing, logging or checking member’s eligibility to book a certain room?
  • What happens if the way bookings are added changes and the Bookings class can no longer be used?

If things stay as they are then we’d have to make changes in 3 places, either to add the new functionality or change the API to support a new way of handling bookings. This clearly isn’t a great situation to be in and it’d be nice to minimise the scope of the change to one place only. So what can we do?

The Law of Demeter says that objects should only talk to their immediate friends. If we take any client of Room, for example, Auditorium, we can see that it has a private field of type Room. Auditorium and Room are immediate friends in the sense of the Law of Demeter. However, Auditorium and Bookings aren’t immediate friends. Auditorium shouldn’t be able to go calling methods on one of Room’s fields, in this case, room.bookings.addBooking(booking) isn’t very friendly. To get the two back on good terms we could add a method to Room that Auditorium can use instead, such as room.addBooking(booking). This stops Auditorium needing to know about the existence of Bookings and by doing so the code becomes a little less rigid.

“Only talk to your immediate friends.” – C2 Wiki

Below the curious Sherlock Holmes object is the Auditorium class and he isn’t at all interested in merely talking to his immediate friend the Room class. Instead, he’s opening him up to see what friends there are inside to play with, oh mon dieu!:

If we think about objects in terms of the phrase, “the tip of the iceberg”, then the tip of the iceberg is an object’s public API. The less each object knows about every other object’s internal details the more freedom they have to change independently over time without affecting others. For example, the two icebergs below are independent entities and in this world, they have faces and talk to each other. They’re happy speaking to each other above the waterline, helping each other out, but they never take a peek below the waterline. They cooperate but they’re separate and they’re cool with that.

However, the two icebergs below have now been playing fast and loose with the rules. Sometimes they talk above the waterline but mostly they’re a bit lazy and frequently look below the waterline to see how the other does what it does. This behaviour has caught the attention of the friendly neighbourhood Kraken. Whenever one of the icebergs looks below the waterline to find out how to do something that it could have asked the other iceberg to do for it above the waterline, the Kraken wraps one of its tentacles a little tighter around the neighbouring iceberg. Unlike the icebergs above, the Kraken has brought these two icebergs closer together and threatens to crush them together until they’re so enmeshed that they’re indistinguishable from each other.

Objects should only be able to see and consume “the tip of the iceberg”, they shouldn’t be diving beneath the waterline to inspect the rest of it, there be the Kraken! To help the two icebergs caught in the Kraken’s death grip we can apply the Hide Delegate refactoring which will move all conversation back above the waterline. Then our icy friends can get back to living independently.

Back to the code example, below are the same clients but with the Hide Delegate refactoring applied to them:

class Auditorium {
    private final Room room;

    Auditorium(Room room) {
        this.room = room;
    }

    void confirmBooking(Booking booking) {
        room.addBooking(booking);
    }
}

class RecurringBookingScheduler {
    private final Map<Room, ClubMember> recurringBookings;

    RecurringBookingScheduler(Map<Room, ClubMember> recurringBookings) {
        this.recurringBookings = recurringBookings;
    }

    void processRecurringBookings() {
        for (Map.Entry<Room, ClubMember> entry : recurringBookings.entrySet()) {
            LocalDate today = LocalDate.now();
            Room room = entry.getKey();
            ClubMember member = entry.getValue();
            if (room.availableOnDate(today)) {
                room.addBooking(new Booking(member, today));
            }
        }
    }
}

class WaitingListBookingScheduler {
    private final List<Room> rooms;
    private final Stack<ClubMember> waitingList;

    WaitingListBookingScheduler(List<Room> rooms, Stack<ClubMember> waitingList) {
        this.rooms = rooms;
        this.waitingList = waitingList;
    }

    void processWaitingList() {
        LocalDate today = LocalDate.now();

        for (Room room : rooms) {
            if (waitingList.size() >= 1 && room.availableOnDate(today)) {
                ClubMember member = waitingList.pop();
                room.addBooking(new Booking(member, today));
            }
        }
    }
}

After the refactoring, the bookings field is hidden away behind a new method on Room called .addBooking(). Room now delegates to its Bookings field and clients know nothing about it. Clients now trust that Room knows all about how to add bookings. They’re no longer burdened with having to navigate through Room to get to the bookings collection in order to manipulate it themselves. We could now add logic such as auditing and eligibility checks to the new .addBooking() method, or even change how bookings are handled entirely and clients wouldn’t need to change. They don’t care, adding bookings is Room’s problem.

In conclusion, don’t have objects publicly expose collaborators and follow Tell, Don’t Ask. When objects expose delegates it invites others to use them and burden themselves with behaviour they probably shouldn’t have and likely don’t want. Hide delegates behind a public API and banish the object Kraken back from whence it came.

Buy me a coffee Buy me a coffee

😍 ☕ Thank you! 👍

Share

Leave a Reply

Your email address will not be published. Required fields are marked *

Post comment