SwadhyAI Notes

All Low-Level Design notes

Trees explode. Parts mix.

Composition vs inheritance

Composition means you build behavior from parts you can mix. Inheritance means you specialize a parent type. Example: Car, Bike, EVCar, EVBike, HandicappedEVCar looks tidy until a vehicle is both electric and compact, and the next requirement is “staff vehicle.” The inheritance tree was modeling the wrong axis. Prefer a small type plus parts you can combine.

The exploding taxonomy

Vehicle
 ├─ Car
 │   ├─ CompactCar
 │   │   └─ EVCompactCar
 │   └─ EVCar
 ├─ Bike
 │   └─ EVBike
 └─ HandicappedEVCar  // now what?

Roles as fields

One Vehicle. A Size. A Powertrain. Maybe a staff flag. Combinations stay data. Behavior that varies — allocation, pricing — is a strategy the lot holds. That is for variation.

Is this a true is-a, or a bundle of roles?

is for genuine . Any ParkingSpot can answer isFree() and occupy(). Any PricingStrategy can answer feeFor(ticket). If a subclass has to no-op half the parent methods, you picked the wrong parent. Variation in behavior is almost always composition. How we allocate. How we price. How we notify. The lot holds a strategy. Vehicle holds a VehicleType and a Powertrain. You add staff parking with a flag or a , not another layer in the tree.

You still use inheritance for a shallow contract: the strategy interface and its implementations. That is a stable is-a. It is not a taxonomy of the real world. Interviewers have seen the deep Vehicle hierarchy. They are happier with parts.

Roles as fields, not a six-level tree

java
enum Powertrain { PETROL, ELECTRIC }
enum Size { BIKE, COMPACT, LARGE }

class Vehicle {
  private final String plate;
  private final Size size;
  private final Powertrain powertrain;
  private final boolean staff;
}

class ParkingLot {
  private final SpotAllocationStrategy allocation;
  private final PricingStrategy pricing;
}

// Avoid: EVCompactCar extends CompactCar extends Car extends Vehicle

A short test before you type extends

  • Will the next requirement be a new combination of old features? If yes, compose.
  • Must the new type be usable everywhere the old type is, and honor the same promises? If yes, inherit.
  • Are you inheriting just to reuse two methods? Extract a collaborator. Inheritance is a contract, not a discount on copy-paste.
  • Would a subclass have to throw or no-op? Stop. That is an LSP break waiting to happen.

The formal name for the contract test is Liskov Substitution. You can use it without reciting the paper. “Can I hand this to any code that expects the parent and still be right?”

A taxonomy of the real world

Every extra adjective becomes a subclass. Combinations explode. Model axes as fields or strategies instead. Biology classifies. Software composes.

Inheriting just to reuse two methods

If you only wanted the code, extract a collaborator. A shared formatter is not a parent type. A shared fee table is a policy object. See Strategy.

Shallow inheritance that is actually a contract

HourlyPricing is-a PricingStrategy. That tree is one level deep and the promise is one method. That is the good kind. It is also how you leave a seam for protected variations.

java
interface PricingStrategy {
  Money feeFor(StayDuration parked, Vehicle vehicle);
}

final class HourlyPricing implements PricingStrategy {
  private final Money perHour;
  public Money feeFor(StayDuration parked, Vehicle vehicle) {
    return perHour.times((int) parked.billableHours());
  }
}

final class AirportPricing implements PricingStrategy {
  public Money feeFor(StayDuration parked, Vehicle vehicle) {
    // different table, same promise
    return airportTable.rate(parked, vehicle.size());
  }
}

Composition here is not the UML diamond

In object relationships, composition means the part dies with the whole: floors with the lot. On this page, composition means “has a collaborator instead of extending a parent.” Both uses of the word are standard. Say which one you mean. “The lot composes a pricing strategy” is variation. “The lot composes floors” is lifetime. You want both. You do not want Vehicle extends Floor.

How to explain this in an interview

“I will not subclass Car. Vehicle already has a powertrain. Allocation can prefer EV spots by reading that field. Pricing can add a surcharge the same way. The type stays one class.” Then, if they ask for a new pricing rule, you swap the strategy. The tree never grew.

When inheritance is the honest answer

A checked exception hierarchy, a small Expression tree in an interpreter, a Widget that must paint — these can be genuine is-a types. The child honors the parent’s promises and adds behavior. The tree stays shallow. You are not encoding adjectives. You are encoding a contract the compiler should enforce.

java
abstract class Expression {
  abstract int eval();
}

final class NumberLit extends Expression {
  private final int value;
  NumberLit(int value) { this.value = value; }
  int eval() { return value; }
}

final class Add extends Expression {
  private final Expression left;
  private final Expression right;
  int eval() { return left.eval() + right.eval(); }
}

Decorators and strategies are composition with manners

A logging allocator that wraps another SpotAllocationStrategy is composition. A weekend pricing wrapper that adds a surcharge is composition. You keep the same role. You stack behavior. You do not create LoggingWeekendAirportHourlyPricing as a subclass of HourlyPricing. The wrapper implements the interface and holds the inner policy. That is how you add “also notify” without growing a tree.

java
final class WeekendSurcharge implements PricingStrategy {
  private final PricingStrategy inner;
  private final Money extra;

  public Money feeFor(Duration parked, Vehicle vehicle) {
    Money base = inner.feeFor(parked, vehicle);
    return isWeekend() ? base.plus(extra) : base;
  }
}

Square versus Rectangle is the same lesson

If Rectangle.setWidth(10) must leave height alone, Square cannot be a Rectangle. Clients that set width and height independently will break. Compose a Size value, or make both shapes implement Shape with area(). Do not inherit for the shared x and y fields. That reuse is a Point collaborator.

Machine-coding vehicles are Square/Rectangle in costume. EV is not a kind of Car in the type system. It is a powertrain on a vehicle. Once you have seen the combination explode, you stop typing extends for adjectives.

Elevator dispatch is not an Elevator subclass

SCAN, LOOK, and nearest-car are policies. The car is still a car. If you create ScanElevator extends Elevator, the next requirement — “this building uses SCAN in peak and nearest at night” — forces you to swap the type of a living object. Hold a DispatchPolicy. Change the field. The car’s state machine stays put. That is the same move as pricing on the lot.

Logging frameworks make the same mistake with Logger subclasses per destination. A Logger holds an Appender. Console and file are parts. The logger’s job — level, format — does not change when the sink does.

Compose when

The next feature is a combination of old axes, or you only wanted two methods reused.

Inherit when

Every caller of the parent must accept the child, and the child keeps every promise.

Default to compose, inherit on a dare

If you cannot say the parent contract in one sentence, do not extend it. If the next feature is an adjective, it is a field or a strategy. If the new type must stand in for the old one in every caller and keep every promise, inherit — once, shallowly, and with tests that use the parent type.

StrategyLiskov SubstitutionObject relationshipsProtected variations