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 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 belong in the server class where the information lives and fragility creeps in when many objects are doing this, one change can ripple through many parts of the code.

I made another contrived example for this post called The Codemasons Clubhouse, you can find the repo here https://github.com/Squaretechre/codemasons-clubhouse. It models a fictional clubhouse with rooms that can be booked by club 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 Room, the Auditorium of the clubhouse and some scheduled jobs, 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 our Sherlock Holmes object is the Auditorium and he isn’t interested in only talking to his immediate friend Room, he’s opening him up to see what friends there are inside to play with:

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 objects know about each other’s internals 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 play 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 code kraken back from whence it came.

Share

Leave a Reply

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

Post comment