GRASP · Protected Variations
Protected Variations
Protected Variations means: find what will change, and hide it behind a stable interface so the rest of the design does not reopen. The interviewer smiles: “Now add airport pricing and a nearest-spot rule.” If those rules are baked into ParkingLot, the lot file opens again. Name the unstable part first, then put a strategy interface or small policy object in front of it.
What will change, and what interface keeps the rest from caring?
In plain words: find what will move, and put a stable abstraction in front of it. That abstraction is usually an interface or a small policy object. Callers depend on the interface. The moving part lives behind it as an implementation.
In parking lots the usual moving parts are pricing, allocation, and payment. In games, move validation. In caches, eviction. Name the variation point out loud before you pick Strategy, Factory, or Observer. Pattern names come second. Strategy is one way to protect a varying algorithm. Factory protects a varying concrete class. You do not protect things that will not vary. That is an unnecessary abstraction, not design.
Open-Closed is the SOLID name for living with this interface: add a new implementation, do not rewrite ParkingLot. GRASP tells you to look for the stable abstraction. SOLID tells you to extend through it instead of editing callers.
Likely follow-ups
Circle the axis, then name the stable abstraction
Airport pricing
Axis: Fee policy
PricingStrategy
Nearest-spot rule
Axis: Allocation
SpotAllocationStrategy
Pay by UPI instead of cash
Axis: Payment
PaymentGateway
Email instead of SMS
Axis: Notification
Notifier
Staff vehicles free until 18:00
Axis: Fee policy
PricingStrategy (new impl)
Two axes, two interfaces. LotPolicy that both prices and allocates will reopen for either change. That is one abstraction pretending to protect two variation points.
Java: a stable interface in front of the rule that will move
interface SpotAllocationStrategy {
Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle vehicle);
}
interface PricingStrategy {
Money fee(Ticket ticket, Clock clock);
}
class ParkingLot {
private final SpotAllocationStrategy allocation;
private final PricingStrategy pricing;
ParkingLot(SpotAllocationStrategy allocation, PricingStrategy pricing) {
this.allocation = allocation;
this.pricing = pricing;
}
Optional<ParkingSpot> allocate(Vehicle vehicle) {
return allocation.choose(freeSpots(), vehicle);
}
Money feeFor(Ticket ticket, Clock clock) {
return pricing.fee(ticket, clock);
}
}
class NearestSpotStrategy implements SpotAllocationStrategy {
public Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle vehicle) {
return free.stream()
.filter(s -> s.fits(vehicle))
.min(Comparator.comparingInt(ParkingSpot::walkToGate));
}
}
class AirportPricing implements PricingStrategy {
public Money fee(Ticket ticket, Clock clock) {
long seconds = ticket.parked(clock).getSeconds();
if (seconds < 0) throw new IllegalArgumentException("negative stay");
long hours = Math.max(1, seconds / 3600 + (seconds % 3600 == 0 ? 0 : 1));
return ticket.hourlyRate().times(hours).plus(airportSurcharge);
}
}Protect these
Pricing, allocation, payment vendor, notifier. You can already name two variants, or the interviewer will ask for a second in the next sentence. A small interface here is earned.
Do not protect these yet
Ticket id formatting, the shape of Money, whether a spot has an integer id. An interface for TicketId that never changes is noise. Wait until a second variant exists. That is YAGNI keeping Protected Variations honest.
How to find the variation in fifteen minutes
List likely follow-ups before you draw classes. New fee rule, new spot rule, new payment, new notifier. Circle the unstable part — one circle per axis. Name a stable interface. Keep the lot boring: ParkingLot asks the strategy. It does not switch on enums.
If you pick Strategy before naming the variation, you will invent an interface for something that never moves. The order is: name the change, then pick the pattern that builds the abstraction. Extensibility points in behavioral modeling are this habit applied across a whole design. See extensibility points.
One interface for two axes
LotPolicy with both chooseSpot and fee looks convenient. Airport pricing then forces you to reopen the same type as nearest-spot. Two variation points, two interfaces. Combine only when they truly change together — they almost never do in a parking lot.
// Too wide — reopens for pricing or allocation
interface LotPolicy {
Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle v);
Money fee(Ticket t, Clock clock);
}
// Two interfaces — either change leaves the other still
interface SpotAllocationStrategy { Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle v); }
interface PricingStrategy { Money fee(Ticket t, Clock clock); }Payment as a third stable abstraction
Cash, card, UPI. The stay should not import a vendor. PaymentGateway.charge(amount, ticketId) is the stable interface. A fake gateway in tests records charges. A Razorpay adapter lives at the edge. That is Protected Variations plus Dependency Inversion: the abstraction is stable, you own it, details plug in.
Different abstractions for different pattern families
Strategy protects a varying algorithm: fee, allocation. Factory protects a varying class: CompactSpot versus EvSpot. Observer protects a varying list of listeners: SMS, email, a dashboard. You pick the family after you name the variation. Naming Observer first and then hunting for an event is the cargo-cult version of this habit.
interface Notifier { void ticketClosed(Ticket ticket, Money due); }
class CompositeNotifier implements Notifier {
private final List<Notifier> sinks;
public void ticketClosed(Ticket ticket, Money due) {
for (Notifier n : sinks) n.ticketClosed(ticket, due);
}
}
class EmailNotifier implements Notifier { /* ... */ }
class SmsNotifier implements Notifier { /* ... */ }
// Adding Slack is a new Notifier plus wiring. ExitService stays closed.Keep the lot boring on purpose
After the interfaces exist, ParkingLot should read as a coordinator: hold floors, ask allocation, mint tickets, ask pricing if you insist the lot expose feeFor. The interesting ifs live in the implementations. That boredom is the design working. Interviewers sometimes poke the lot file looking for switches; an empty-looking lot with two strategy fields is a strong answer.
If a new follow-up does not fit an existing interface, cut a new one. Do not widen LotPolicy. Do not add a boolean airportMode on ParkingLot. Booleans that toggle behavior are switches in disguise and will reopen the lot for every airport in the chain.
Related
Airport mode is a boolean trap
if (lot.isAirport()) inside feeFor is a variation point you refused to abstract. Every new site — mall, hospital, airport — adds a boolean or an enum the lot must switch on. Inject AirportPricing when this lot is an airport. The lot file never learns the word airport. That is Protected Variations as wiring, not as flags.
class ParkingApp {
static ParkingLot mall() {
return new ParkingLot(new FirstFreeStrategy(), new HourlyPricing());
}
static ParkingLot airport() {
return new ParkingLot(new NearestSpotStrategy(), new AirportPricing());
}
}
class ParkingLot {
private final SpotAllocationStrategy allocation;
private final PricingStrategy pricing;
// no boolean airport; no enum SiteType
}Name the variation in the first five minutes
After requirements, say: “Pricing will change, allocation will change, ticket ids will not.” Write two interfaces and leave TicketId as a class. If the interviewer never asks for airport rates, you still look prepared: the abstraction was cheap. If they do ask, you add a class. That is Protected Variations as pacing, not as a pattern dump.
// Spoken in minute five, coded as:
interface PricingStrategy { Money fee(Ticket t, Clock c); }
interface SpotAllocationStrategy { Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle v); }
class TicketId { private final String value; }
// No TicketIdFactory. No LotPolicy. Two interfaces, one value.Follow-up that does not fit an interface yet
“Ban this plate from the lot.” That is neither pricing nor allocation. It is a new axis: admission. Cut AdmissionPolicy.allows(vehicle) rather than stuffing a banned-list into FirstFreeStrategy. Strategies that start answering unrelated questions become LotPolicy again. New follow-up, new circle, new interface — even late in the round. The lot stays the coordinator that asks allows, then allocate, then enter.
interface AdmissionPolicy { boolean allows(Vehicle v); }
class ParkVehicle {
Ticket handle(Vehicle vehicle, Clock clock) {
if (!admission.allows(vehicle)) throw new IllegalArgumentException("banned");
ParkingSpot spot = lot.allocate(vehicle).orElseThrow(LotFull::new);
return lot.enter(vehicle, spot, clock);
}
}Interfaces are allowed to be one method
PricingStrategy.fee is enough. Do not add quote, preview, refund, and explain onto the same interface “in case.” That fattens the abstraction and fights ISP. Protected Variations hides one axis. Extra methods are extra axes sneaking in. Keep the surface small.
A boolean is not a stable abstraction
airportMode, eventDay, and staffOnly on ParkingLot are switches wearing field names. Inject the strategy that already means those words. The lot should not learn site adjectives. Protected Variations is wiring of implementations, not a growing bag of flags.
Takeaway
Find what will move, hide it behind a stable interface, keep ParkingLot boring. One axis per abstraction. Do not protect ticket ids on day one. Name the variation before you name Strategy. Open-Closed is how you live with the interface once it exists.