SwadhyAI Notes

All Low-Level Design notes

GRASP · Expert

Information Expert

Information Expert says: give a responsibility to the object that already has the information to fulfill it. Suppose someone asks “who calculates parking fees?” and puts the formula in ParkingManager. That Manager then asks Ticket for entry time, Spot for type, Lot for the rate card, and Clock for now. The Manager knows everyone. Nobody owns the job.

Which object already has what this job needs — or which two should collaborate?

Information Expert is the first GRASP question: where does the job live? Give a responsibility to the object that has the information to fulfill it. Do not invent a helper that phones every field in the system. Sometimes one object has everything. Sometimes the data is split, and a thin controller asks two experts rather than stealing fields from both.

Duration is on Ticket; policy is not

Ticket.hoursParked(clock) belongs on Ticket because Ticket holds entry and exit. Pulling ticket.getStart() and ticket.getEnd() into a Manager so the Manager can subtract them strips the expert. Ask the object; do not empty its pockets.

GST and event-day flat fees are not on Ticket. They are a policy that will change when finance changes the tax rules. That policy is its own expert: PricingStrategy. Neither should steal the other’s knowledge. Ticket should not import tax tables. The strategy should not store entry timestamps. A controller asks both at exit.

A Manager that only forwards getters is a fake expert. It has no information of its own. Prefer moving the method to the object that already stored the fields. If you still need a helper, it is probably a domain service coordinating two experts — not a dumping ground. See rich versus anemic models: anemic tickets plus a 400-line Manager is Expert inverted.

Investigate: who has which fact?

Before you name a class, list the facts the job needs. Then list who already stores each fact. The expert is the overlap. When the overlap is two objects, you have a collaboration, not a missing Manager.

Fact the fee needsWho already has itTherefore
Entry instantTicketTicket.parked(clock)
Exit instant / nowTicket + ClockPass Clock in; do not call Instant.now() inside Ticket
Vehicle type / hourly rateTicket (copied at enter)Ticket answers; Lot is not asked again
GST percent, event flat feePricing policyPricingStrategy.fee(...)
Whether the spot had a chargerSpot, or a flag copied onto Ticket at enterCopy at creation if exit must not walk the graph

Java: the expert has the data; the strategy has the policy

java
class Ticket {
  private final Instant enteredAt;
  private Instant exitedAt;
  private final VehicleType vehicleType;
  private final Money hourlyRate;
  private final boolean usedCharger;

  Duration parked(Clock clock) {
    Instant end = exitedAt != null ? exitedAt : clock.instant();
    return Duration.between(enteredAt, end);
  }

  VehicleType vehicleType() { return vehicleType; }
  Money hourlyRate() { return hourlyRate; }
  boolean usedCharger() { return usedCharger; }
}

interface PricingStrategy {
  Money fee(Duration parked, VehicleType type, Money hourly, boolean usedCharger);
}

class HourlyWithChargerSurcharge implements PricingStrategy {
  private final Money chargerPerHour;

  public Money fee(Duration parked, VehicleType type, Money hourly, boolean usedCharger) {
    long seconds = parked.getSeconds();
    if (seconds < 0) throw new IllegalArgumentException("negative stay");
    long hours = Math.max(1, seconds / 3600 + (seconds % 3600 == 0 ? 0 : 1));
    Money base = hourly.times(hours);
    return usedCharger ? base.plus(chargerPerHour.times(hours)) : base;
  }
}

class ExitService {
  private final PricingStrategy pricing;

  Money checkout(Ticket ticket, Clock clock) {
    return pricing.fee(
        ticket.parked(clock),
        ticket.vehicleType(),
        ticket.hourlyRate(),
        ticket.usedCharger());
  }
}

The fake expert

This Manager has no information of its own. Every line is a getter from someone else. That is the smell Expert exists to prevent. It is also a Law of Demeter wreck waiting to happen: once the Manager knows Spot, it will walk getFloor().getLot().

java
class ParkingManager {
  Money fee(Ticket ticket, ParkingSpot spot, ParkingLot lot, Clock clock) {
    Instant start = ticket.getEnteredAt();
    Instant end = clock.instant();
    long seconds = Duration.between(start, end).getSeconds();
    if (seconds < 0) throw new IllegalArgumentException("negative stay");
    long hours = Math.max(1, seconds / 3600 + (seconds % 3600 == 0 ? 0 : 1));
    Money rate = lot.getRateCard().rateFor(ticket.getVehicle().getType());
    if (spot.hasCharger() && ticket.getVehicle().needsCharger()) {
      rate = rate.plus(lot.getRateCard().chargerSurcharge());
    }
    return rate.times(hours);
  }
}

fee(ticket.getStart(), ticket.getEnd(), ticket.getType(), lot.getRates()) is the same Manager in disguise even after you delete the class. Ask the objects; do not strip them into a parameter list of primitives.

Expert is not “the biggest class”

ParkingLot has a lot of fields. That does not make it the fee expert. The expert is whoever has the specific data for this job. Lots know floors. Tickets know time. Strategies know tax. Size is not expertise.

Copy facts at creation if needed

At enter, copy hourly rate and usedCharger onto the ticket. Exit then asks Ticket, not the live spot. The lot can reassign spots without rewriting history. The expert for the stay holds a snapshot of what the stay needs.

Allocation has an expert too

“Who chooses the spot?” Spot knows whether it is free and whether it fits. The strategy knows the policy (nearest, EV preferred). The lot knows the list of spots. Expert for “fits” is the spot. Expert for “which policy” is the strategy. The lot hands the free list to the strategy; it does not become the policy.

Find the expert before you invent a helper class. If you still need a helper, name the collaboration: ExitService coordinates duration and policy. That is a use case, not ParkingUtils.

Occupancy has a different expert

“Is this spot free?” is not a fee question. ParkingSpot holds occupied/free and size. Ticket does not. A Manager that asks ticket.getSpot().isFree() to decide allocation has stolen the spot’s job and walked a graph. Let the spot answer isFree and fits. Let the strategy walk a list of spots. Two experts, two jobs, no Manager in the middle collecting getters.

java
class ParkingSpot {
  private Vehicle occupant;
  private final SpotSize size;

  boolean isFree() { return occupant == null; }

  boolean fits(Vehicle v) {
    return isFree() && size.fits(v.size());
  }

  void occupy(Vehicle v) {
    if (!fits(v)) throw new IllegalStateException("cannot occupy");
    occupant = v;
  }
}

class FirstFreeStrategy implements SpotAllocationStrategy {
  public Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle vehicle) {
    return free.stream().filter(s -> s.fits(vehicle)).findFirst();
  }
}

When two experts must both speak

Charger surcharge needs Ticket.usedCharger() and a policy rate. Neither object should absorb the other. The controller (or the strategy method signature) is the meeting point. That is still Expert: you did not invent a third object that re-fetches both facts through getters. You passed the facts the experts already exposed as honest questions.

If the meeting point grows a fourth fact, and a fifth, you may be looking at a small value object — FeeRequest with duration, type, and charger flag — built by Ticket. That object is a snapshot the stay already knew, not a Manager. Expert still holds: Ticket assembled what it had.

Related

Anemic tickets force a fake expert

If Ticket is only fields and getters, every job flees to ParkingManager. That Manager then looks like the expert because it is the only place with methods. It is not: it borrowed the data. Give Ticket parked() and close(). Give Spot occupy() and fits(). The Manager shrinks into a controller. Expert and rich models are the same move seen from two sides.

java
// Anemic — Expert has left the building
class Ticket {
  Instant enteredAt;
  Instant exitedAt;
  VehicleType type;
}

// Rich — Expert stayed with the data
class Ticket {
  private Instant enteredAt;
  private Instant exitedAt;
  Duration parked(Clock clock) {
    Instant end = exitedAt != null ? exitedAt : clock.instant();
    return Duration.between(enteredAt, end);
  }
  void close(Instant at) {
    if (exitedAt != null) throw new IllegalStateException("already closed");
    exitedAt = at;
  }
}

Hours parked is never the lot’s question

ParkingLot has many tickets, so it is tempting to write lot.hoursParked(ticketId). That method will fetch the ticket and subtract times — a fake expert again. Find the ticket, then ask it. The lot may find; it should not compute duration. Expert is specific: this job, this data, this object.

Rate card is not the lot’s fee expert

A RateCard object may hold posted hourly numbers. That still does not make ParkingLot.fee the right method. At enter, copy the applicable rate onto the ticket. At exit, the strategy reads the snapshot. The card can change tomorrow without rewriting yesterday’s stays. Expert for “what did this stay cost per hour” became the ticket at the moment of entry.

Ask the object, even in a one-liner

ticket.parked(clock) is shorter than subtracting two getters in the caller, and it keeps the rule next to the fields. Expert often looks like a tiny method. Size is not the test. Whether the data already lived there is.

Takeaway

Give the job to the object that already has the information. Ticket owns duration. PricingStrategy owns the formula. A controller asks both. A Manager that only forwards getters is not an expert — it is a missing method on the object that stored the fields.