SwadhyAI Notes

All Low-Level Design notes

SOLID · O

Open-Closed

Open-Closed says software entities should be open for extension and closed for modification. In plain words: you should be able to add a new behavior by writing a new type, without rewriting the old class that uses it.

In a parking lot, that idea shows up when fees grow by vehicle type. The lot priced cars with a switch: CAR, BIKE, EV. Then compact SUVs. Then staff vehicles. Each type reopened ParkingLot, the fee method, and the tests. Open-Closed is the habit of adding a strategy class instead of reopening that switch.

Can I add this by writing a new type?

If the answer is “I must edit the old switch,” the design is closed in the wrong direction. Hide the varying rule behind a small strategy interface. ParkingLot asks the current strategy. A new rule is a new implementation.

You do not open every class

You open the unstable part. Pricing and allocation almost always change in a parking-lot follow-up. The ticket id format probably does not. Do not invent an interface for things that will never vary. That is a premature abstraction, and it fights YAGNI.

Protected Variations is the GRASP name for the same instinct: find what will move, put a stable abstraction in front of it. Strategy is one way to build that abstraction. Open-Closed is the habit that made you look for it. Factory protects a varying concrete class. Observer protects a varying list of listeners. The pattern name comes second.

A growing switch on VehicleType inside ParkingLot is the usual smell. Every follow-up is another case. The tests must enumerate every branch. A missed case is a runtime exception in production. Hide the rule behind an interface. The lot should not know the list of types.

How a switch becomes a novel

Each vehicle type reopens the same method

  1. 1

    CAR

    First switch. Lot file is still small. Feels fine.

  2. 2

    BIKE

    Second case. Tests duplicate the branches. Still shippable.

  3. 3

    EV

    Third case, plus charger spots. Allocation switch and fee switch both reopen.

  4. 4

    COMPACT_SUV

    Fourth case. Floor size checks reopen too. You are now editing three files per type.

  5. 5

    STAFF

    Fifth case, with a free-hour rule. The fee method is a novella.

Java: the switch that would not stay closed

java
class ParkingLot {
  Money feeFor(Ticket ticket, Clock clock) {
    long hours = ticket.hoursParked(clock);
    return switch (ticket.vehicle().type()) {
      case CAR -> ticket.hourlyRate().times(hours);
      case BIKE -> ticket.hourlyRate().times(hours).times(0.5);
      case EV -> ticket.hourlyRate().times(hours).plus(chargerSurcharge);
      case COMPACT_SUV -> ticket.hourlyRate().times(hours).times(1.25);
      case STAFF -> Money.zero(); // until 18:00, then hourly — more branches coming
      default -> throw new IllegalStateException("add another case");
    };
  }
}

Airport pricing and event-day pricing will not fit this enum forever. The lot should not know the catalog of rules. It should ask a strategy. Adding event-day is then a new class plus wiring, not a sixth case and a seventh test branch inside ParkingLot.

java
interface PricingStrategy {
  Money fee(Ticket ticket, Clock clock);
}

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

class EventDayPricing implements PricingStrategy {
  private final Money flatEntry;
  private final PricingStrategy restOfDay;

  EventDayPricing(Money flatEntry, PricingStrategy restOfDay) {
    this.flatEntry = flatEntry;
    this.restOfDay = restOfDay;
  }

  public Money fee(Ticket ticket, Clock clock) {
    return flatEntry.plus(restOfDay.fee(ticket, clock));
  }
}

class ParkingLot {
  private final PricingStrategy pricing;

  ParkingLot(PricingStrategy pricing) {
    this.pricing = pricing;
  }

  Money feeFor(Ticket ticket, Clock clock) {
    return pricing.fee(ticket, clock);
  }
}

Leave these closed

  • Ticket identity and time bounds — a stay is a stay.
  • ParkingSpot occupy / free — the occupancy invariant rarely grows a second variant.
  • Ticket id format — do not invent TicketIdFactory on day one.

Keep these open

  • Fee policy — hourly, event-day, airport, staff, EV surcharge.
  • Spot choice — first-free, nearest, EV-preferred, handicapped-only.
  • Payment and notification — vendors change; the stay should not import them.

Extension is not copy-paste

HourlyPricing2 with three lines changed is not extension. It is a duplicate rule. Share the stable parts; vary only the policy. Event-day wrapping hourly in the sketch above is composition: the flat fee is new, the hourly math stays in one home. That is also DRY: one formula, not three copies.

You can extend by wrapping (decorator-shaped strategies), by implementing the same interface, or by a factory that picks the implementation. You cannot extend by duplicating ParkingLot into ParkingLotAirport. That reopens every other habit at once.

In an interview, say the variation out loud before you draw the interface. “Pricing will change; allocation will change; ticket ids will not.” Then introduce two strategy interfaces, not ten. See Strategy when the algorithm varies, and Factory Method when the concrete class varies.

Allocation follows the same habit

Fees get the textbook Strategy example. Allocation is the follow-up that actually lands. First-free works until “prefer chargers for EVs” or “nearest to the elevator.” Hide that walk behind a strategy interface so ParkingLot does not grow a second switch.

java
interface SpotAllocationStrategy {
  Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle vehicle);
}

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

class EvPreferredStrategy implements SpotAllocationStrategy {
  public Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle vehicle) {
    if (vehicle.needsCharger()) {
      Optional<ParkingSpot> charger = free.stream()
          .filter(ParkingSpot::hasCharger)
          .filter(s -> s.fits(vehicle))
          .findFirst();
      if (charger.isPresent()) return charger;
    }
    return new FirstFreeStrategy().choose(free, vehicle);
  }
}

When an interface is premature

Open-Closed is not “interfaces for everything.” A strategy interface is earned when the follow-up asks for a second policy, or when you can already name two. Until then, one HourlyPricing class is honest. If the interviewer then asks for event-day, you extract the interface in thirty seconds and look prepared rather than over-abstracted.

That tension with YAGNI is the skill. Protected Variations is right when something will vary. YAGNI is right when it will not. In parking-lot interviews, pricing and allocation will vary. Ticket id formatting will not. Bet accordingly.

How to explain this in an interview

Do not say “I will make it Open-Closed.” Say “allocation and pricing will change, so I am putting SpotAllocationStrategy and PricingStrategy in front of those rules. Ticket ids will not change, so I am leaving them as a class.” That sentence shows you know which abstractions to introduce.

If the interviewer then asks for airport pricing, you add AirportPricing implements PricingStrategy and wire it in the constructor or a small factory. You do not reopen feeFor. That demo is the habit. The letters on the whiteboard are optional.

Watch for a second switch hiding in serialization or in the UI. Open-Closed fails if ParkingLot is closed but TicketJsonWriter still switches on VehicleType. Every place that named the catalog of types is a place that will reopen. Hunt those lists. Collapse them into the strategy or the vehicle’s own answers: fits, hourlyRate, needsCharger.

Vehicle answers, so the lot does not switch

Sometimes the varying rule is a question the vehicle can answer: needsCharger, size, permit. Then you do not need a type enum at all. Compact SUV is a Vehicle with a larger size, not a new case in ParkingLot. The lot stays closed because it never named the catalog.

java
class Vehicle {
  private final SpotSize size;
  private final boolean needsCharger;
  private final Money hourlyRate;

  boolean needsCharger() { return needsCharger; }
  boolean fitsIn(SpotSize spot) { return size.ordinal() <= spot.ordinal(); }
  Money hourlyRate() { return hourlyRate; }
}

class FirstFreeStrategy implements SpotAllocationStrategy {
  public Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle vehicle) {
    return free.stream().filter(s -> s.fits(vehicle)).findFirst();
  }
}
// Adding compact SUV is new Vehicle(...), not a new switch case.

Related

Staff vehicles without a sixth switch case

Staff can be a PricingStrategy that returns zero until 18:00, then delegates to hourly. ParkingLot still does not know the word staff. The vehicle may carry a flag staff=true that the strategy reads from Ticket — data copied at enter — rather than a VehicleType enum the lot switches on. Extension stays in the policy class.

java
class StaffPricing implements PricingStrategy {
  private final PricingStrategy afterHours;
  public Money fee(Ticket ticket, Clock clock) {
    if (ticket.staff() && clock.instant().atZone(ZoneOffset.UTC).getHour() < 18) {
      return Money.zero();
    }
    return afterHours.fee(ticket, clock);
  }
}

Takeaway

Add a fee rule or a spot rule by writing a new type. Do not reopen ParkingLot’s switches. Introduce strategy interfaces only in front of what will actually move. Extension is a new implementation, not a copied class with three lines changed.