SwadhyAI Notes

All Low-Level Design notes

GRASP neighbor

Law of Demeter

The Law of Demeter says: talk to close collaborators, not down a chain of getters. The report computed a fee with ticket.getSpot().getFloor().getLot().getRateCard().hourly(). A floor refactor then broke the report, the SMS, and a test that never cared about floors. The call walked through strangers. That train wreck is the problem this habit prevents.

Am I asking a friend, or walking through objects I should not know exist?

In plain words: a method should speak to itself, to its fields, to things it created, or to things it was handed. It should not reach through a friend into the friend's friend. That is the whole rule.

ticket.getSpot().getFloor().getLot() leaks the object graph. Hide the path behind a domain method: ticket.duration(clock), lot.findSpot(criteria), pricing.fee(ticket, clock). The caller learns one idea instead of four types. This is coupling you can hear. Each extra get is another class that must stay still for you to compile.

Flattening the path often reveals the real expert — the object that should have answered in one step. You do not ban all getters. You ban navigation as a substitute for a question. If you need a floor number for a display, ask for a floor number. Do not ask for a Floor so you can rummage inside it.

Train wreck

The report now knows Spot, Floor, Lot, RateCard

ticket
getSpot()
getFloor()
getLot()
getRateCard()
hourly()

Six types in one expression. A RateCard rename breaks the report. A Floor split breaks the report. The report's job was "today's fees," not "navigate the building."

Java: ask a friend; do not walk the graph

java
// Train wreck — the report now knows Spot, Floor, Lot, RateCard
Money fee = ticket.getSpot().getFloor().getLot().getRateCard().hourly()
    .times(ticket.hoursParked(clock));

// Close collaborators
Money fee = pricing.fee(ticket, clock);

class Ticket {
  private final Instant enteredAt;
  private final Money hourlyRateAtEntry;

  Duration parked(Clock clock) {
    return Duration.between(enteredAt, clock.instant());
  }

  Money hourlyRate() { return hourlyRateAtEntry; }
}

class HourlyPricing implements PricingStrategy {
  public Money fee(Ticket ticket, Clock clock) {
    return ticket.hourlyRate().times(ticket.hoursParked(clock));
  }
}

Who may you talk to?

  • Yourself — private helpers on this class.
  • Your fields — Ticket may ask its own enteredAt.
  • Objects you created — Floor may ask the spots it constructed.
  • Objects you were handed — ExitService may ask the Ticket parameter and the PricingStrategy field.

Who you may not rummage

The friend's friend. Spot's floor. Floor's lot. Lot's rate card. Vehicle's owner's phone. If you need the phone, pass a Notifier or copy the phone onto the ticket at enter. Do not walk the owner graph at exit.

Hiding the chain in a helper that still walks it

Fees.fee(ticket) that internally calls getSpot().getFloor()... only moved the train wreck. Give Ticket or PricingStrategy a method that answers the question without returning the path. The wreck is the navigation, not the line count in the caller. Encapsulation is the parent habit: objects answer questions; they do not hand you their internal structure. See encapsulation.

java
class Fees {
  static Money fee(Ticket ticket, Clock clock) {
    return ticket.getSpot().getFloor().getLot().getRateCard().hourly()
        .times(ticket.hoursParked(clock));
  }
}
// Callers look clean. The graph is still leaked — now inside Fees.

Copy at the boundary so exit does not walk

At enter, the lot already knows the spot and the rate card. Copy hourly rate, spot id, and usedCharger onto the ticket. Exit then talks only to Ticket and PricingStrategy. If the lot later moves cars or changes posted rates, historical stays still price with the snapshot. That is Demeter plus honest accounting.

java
class ParkingLot {
  Ticket enter(Vehicle vehicle, ParkingSpot spot, Clock clock) {
    Money rate = rateCard.rateFor(vehicle.type());
    Ticket ticket = new Ticket(
        ids.next(),
        vehicle,
        spot.id(),
        clock.instant(),
        rate,
        spot.hasCharger() && vehicle.needsCharger());
    spot.occupy(vehicle);
    tickets.add(ticket);
    return ticket;
  }
}

// Exit never calls ticket.getSpot().

Getters are allowed; navigation is the smell

getSpot() on Ticket may be needed for a map display that honestly wants a spot id. Returning Floor from Spot so strangers can keep walking is how the wreck grows. Return what the caller's question needs: a floor number, a label, a Money. If the caller keeps chaining, you returned the wrong type. Prefer a value — SpotId, FloorNumber, Money — over a live entity the caller will open.

How it shows up next to the other habits

Demeter is how coupling sounds in a method. Expert is how you decide who should have answered. SRP is why Ticket should not also know RateCard. Protected Variations is why pricing.fee is a strategy interface instead of a walk to RateCard. You can fail all four with one train wreck. You can fix all four by copying the rate at enter and asking a strategy at exit.

The SMS walk is the same wreck

ticket.getVehicle().getOwner().getPhone() teaches ExitService about Vehicle, Owner, and Phone. A future corporate account with no single owner breaks the chain. Copy the phone onto the ticket at enter, or pass a Notifier that already knows how to reach the driver. Exit asks notifier.ticketClosed(ticket, due) and never rummages an owner.

java
class Ticket {
  private final String notifyPhone;
  String notifyPhone() { return notifyPhone; }
}

class ParkingLot {
  Ticket enter(Vehicle vehicle, ParkingSpot spot, Clock clock) {
    return new Ticket(ids.next(), vehicle, spot.id(), clock.instant(),
        vehicle.notifyPhone(), rateCard.rateFor(vehicle.type()));
  }
}

class TwilioNotifier implements Notifier {
  public void ticketClosed(Ticket ticket, Money due) {
    client.send(ticket.notifyPhone(), "Pay " + due);
  }
}

Why Demeter is not “no getters”

Ticket.id() is a getter and a good one. The caller asked for an id. Ticket.getSpot().getFloor() is a walk. The difference is whether the return type invites another get. Ids, Money, Duration, enums, and strings usually stop the chain. Entities and collections of entities usually continue it. Prefer stopping types at the boundary of a method.

Lists are a gray area. lot.spots() that returns a live mutable list invites callers to occupy spots without going through allocate(). Return an unmodifiable view, or do not return the list at all — expose allocate(vehicle) instead. That is Demeter applied to collections: do not hand out the internal list and then hope callers do not rearrange it.

Related

Reports that only need numbers

A daily total does not need Floor. It needs Money. Ask pricing.fee for each closed ticket and sum. If the report also wants a floor column for a map, ask ticket.spotId() and let a separate lookup in the lot translate ids to labels — in a query object, not in the report's fee loop. Mixing "how much" with "where in the building" is how the train grows a seventh car.

java
class DailyTakings {
  private final PricingStrategy pricing;

  Money total(List<Ticket> closed, Clock clock) {
    Money sum = Money.zero();
    for (Ticket t : closed) {
      sum = sum.plus(pricing.fee(t, clock));
    }
    return sum;
  }
}

class OccupancyBoard {
  private final ParkingLot lot;
  List<SpotLabel> labels() {
    return lot.spotSummaries(); // lot answers; board does not walk Floor
  }
}

lot.spots() invites a wreck

Returning the live list lets a report call get(0).getFloor().getLot() all over again. Expose allocate, freeCount, or spotSummaries — questions — not the internal collection. If a test needs to seed spots, give the lot a constructor that takes the list once. After construction, the list is private. Demeter and encapsulation agree here: the lot is the collaborator; the spots inside it are not everyone else's collaborators.

Map displays still should not walk

A UI that draws floors may honestly need a list of summaries: id, occupied, charger, walk distance. Build that list inside the lot as a read model. The UI iterates summaries, not Spot objects. You gave the display what it asked for — labels and flags — without lending it occupy(). That is Demeter for read paths: return values, not live entities, when the caller's job is to show, not to command.

java
record SpotSummary(SpotId id, boolean free, boolean charger, int walk) {}

class ParkingLot {
  List<SpotSummary> spotSummaries() {
    return spots.stream()
        .map(s -> new SpotSummary(s.id(), s.isFree(), s.hasCharger(), s.walkToGate()))
        .toList();
  }
}

Summaries stop the chain on purpose

SpotSummary is a value. After you have it, there is no getFloor() to call. That is the point of a read model: Demeter by construction. Command paths still go through allocate and occupy. Display paths go through summaries. Mixing those paths is how the wreck returns.

Takeaway

Talk to close collaborators. Do not walk ticket.getSpot().getFloor().getLot(). Ask a domain question in one step, copy rates onto the ticket at enter, and keep RateCard behind PricingStrategy. Getters that answer a question are fine. Getter chains that navigate the building are the wreck.