A logbook for curious waters
← Software Engineering Field Guide

Message Chains

Before you begin: Objects that contain other objects, properties, and method calls.

A Message Chain makes a caller navigate through several objects to reach what it needs. An expression such as customer.Office.Address.City teaches the caller not just how to obtain a delivery city, but how the customer happens to be organized internally.

If that route changes, every caller following it may need an edit. A focused operation can hide the route while preserving the useful answer. This correction is often called Hide Delegate. 1

The shipping code takes a tour of the customer

A customer in this small example receives deliveries at an office address. Every object in the chain exists; missing addresses and alternate destinations are outside the example.

Original version

public sealed record Address(string City);
public sealed record Office(Address Address);
public sealed record Customer(Office Office);

public static class Shipping
{
    public static string CityFor(Customer customer)
    {
        return customer.Office.Address.City;
    }
}

C# records give these simple objects constructors and properties. Shipping.CityFor knows that a customer contains an office, which contains an address, which contains the city.

from dataclasses import dataclass


@dataclass
class Address:
    city: str


@dataclass
class Office:
    address: Address


@dataclass
class Customer:
    office: Office


def delivery_city(customer):
    return customer.office.address.city

The dataclasses provide constructors for these small data objects. delivery_city knows that a customer contains an office, which contains an address, which contains the city.

Refactored version: ask the customer for its delivery city

public sealed record Address(string City);
public sealed record Office(Address Address);

public sealed record Customer(Office Office)
{
    public string DeliveryCity()
    {
        return Office.Address.City;
    }
}

public static class Shipping
{
    public static string CityFor(Customer customer)
    {
        return customer.DeliveryCity();
    }
}
Customer customer = new Customer(new Office(new Address("Tucson")));
Console.WriteLine(Shipping.CityFor(customer)); // Tucson

Shipping now asks for DeliveryCity. The route through the office remains inside Customer, where a future choice between office and home delivery can be decided without teaching every shipping caller the route.

from dataclasses import dataclass


@dataclass
class Address:
    city: str


@dataclass
class Office:
    address: Address


@dataclass
class Customer:
    office: Office

    def delivery_city(self):
        return self.office.address.city


def delivery_city(customer):
    return customer.delivery_city()
customer = Customer(Office(Address("Tucson")))
print(delivery_city(customer))  # Tucson

The outside function now asks the customer for delivery_city. The route through the office remains inside Customer, where a future choice between office and home delivery can be decided without teaching every shipping caller the route.

The chain did not disappear by magic

Some code still has to know where the address lives. We moved that knowledge to a place responsible for answering the delivery question. A method named merely GetNextObject would not have accomplished the same thing; it would still make callers navigate the structure themselves.

Do not create a forwarding method for every possible field. A customer with hundreds of pass-through methods can become a Middle Man with little purpose beyond hiding other objects. Prefer operations that correspond to what clients actually need.

Dots are not the smell

A fluent builder or collection pipeline may intentionally chain operations through a documented public API. The problem is not a punctuation count. It is having callers depend on an internal object route that is not part of the capability they need.

For a transparent data document whose structure is the intended contract, direct navigation may be entirely reasonable. Use this smell to examine ownership and change costs, not to forbid every multi-step expression.

Sources

  1. Martin Fowler — Hide Delegate. The delivery-city example is original.