Skip to content
academia.sh

Lesson 04 / 14

Inheritance

Separating inheritance's two distinct promises: implementation sharing is measured by line count, the subtype relationship by a three-clause fee contract; a subclass that complies with the contract and one that does not are run through the same client, and the number of wrong results the noncompliant version produces is counted.

Contents

The previous lesson hid the tariff module’s representation and brought the detail known by clients down to zero. That module hid a single tariff form. In a real fee-calculation library, there is more than one tariff type: an insured-shipment tariff, a hand-delivery tariff, a contracted-customer tariff. One way to meet the same names with different implementations is to derive one type from another.

Inheritance is a class inheriting another’s fields and methods and, when needed, replacing part of them with its own implementation. This is the definition introduced in the Programming Fundamentals course. The question asked in this lesson is this: does deriving always produce a subtype, what shows when it does not, and what does it cost.

Inheritance’s Two Distinct Promises

Inheritance is spoken of as if it were a single tool, but it promises two separate things, and the two do not require each other.

The first is implementation sharing: the subclass does not rewrite the superclass’s code. This promise is countable and is kept almost always.

The second is the subtype relationship: a subtype can be put anywhere a supertype is expected and the program keeps working correctly. This promise concerns behavior, not shape, and the language does not check it. Writing extends guarantees the first with certainty; it may not guarantee the second at all.

Separating the two requires writing down what the supertype promises. A supertype’s contract is the guarantees it gives about its methods’ inputs and results. A subtype cannot weaken these guarantees: it cannot narrow the input set, break the promise made about the result, or violate the supertype’s invariant. This rule is known by the name Liskov substitution principle.

The Supertype and the Fee Contract

The tiered tariff turns into a class. The tiers, factors, and thresholds are private; fee, zones, and minimum are exposed outward. The base method is open for subclasses to use.

// tariff.mjs — supertype: tiered tariff; this class defines the fee contract
export class Tariff {
  #tiers; #ratePerKg; #zoneFactor; #minimum;

  constructor({ tiers, ratePerKg, zoneFactor, minimum }) {
    this.#tiers = tiers;
    this.#ratePerKg = ratePerKg;
    this.#zoneFactor = zoneFactor;
    this.#minimum = minimum;
  }

  get minimum() { return this.#minimum; }

  zones() { return Object.keys(this.#zoneFactor).map(Number); }

  base(weight) {
    const tier = this.#tiers.find(([max]) => weight <= max);
    if (tier !== undefined) return tier[1];
    const [lastMax, lastFee] = this.#tiers.at(-1);
    return lastFee + Math.ceil((weight - lastMax) / 1000) * this.#ratePerKg;
  }

  fee(weight, zone) {
    return Math.max(Math.round(this.base(weight) * this.#zoneFactor[zone]), this.#minimum);
  }
}

export const SETTINGS = {
  tiers: [[1000, 4500], [5000, 7000], [10000, 11000]],
  ratePerKg: 1800,
  zoneFactor: { 1: 1, 2: 1.25, 3: 1.6 },
  minimum: 5000,
};

The fee method’s contract has three clauses:

  1. C1 — Defined for every positive weight and every zone in the zones() list; returns a finite number.
  2. C2 — The result does not fall below the minimum fee.
  3. C3 — The result does not decrease with weight: if a1 <= a2, then fee(a1) <= fee(a2).

The third clause cannot be read from the code; it follows from the tiers being increasing and the factor being positive. Unless it is written down, whoever writes a subclass has no reason to know it. Writing the contract is the only way to make inheritance’s second promise checkable.

Two Subclasses, One Compliant, One Not

The first subclass is the insured-shipment tariff: it adds an insurance charge that increases with weight to the superclass’s result.

// insured.mjs — subclass complying with the contract: adds a weight-increasing insurance charge
import { Tariff } from "./tariff.mjs";

export class InsuredTariff extends Tariff {
  #ratePerGram;
  constructor(settings, ratePerGram) { super(settings); this.#ratePerGram = ratePerGram; }
  fee(weight, zone) {
    return super.fee(weight, zone) + Math.round(weight * this.#ratePerGram);
  }
}

The second is the hand-delivery tariff. The requirement is real: shipments between 6 and 9 kilograms cannot be carried by one person, so they are surcharged. The implementation looks direct enough too.

// hand-delivery.mjs — subclass violating the contract: puts a surcharge on one weight band
import { Tariff } from "./tariff.mjs";

export class HandDeliveryTariff extends Tariff {
  #bandSurcharge;
  constructor(settings, bandSurcharge) { super(settings); this.#bandSurcharge = bandSurcharge; }
  fee(weight, zone) {
    const base = super.fee(weight, zone);
    return weight >= 6000 && weight <= 9000 ? base + this.#bandSurcharge : base;
  }
}

Both classes are written in the same shape: the same superclass, the same method signature, the same return type. The difference between them is at the band’s exit: at 9000 grams the fee is high with the surcharge, at 9001 grams the surcharge drops off and the fee falls. Clause C3 is violated.

The implementation-sharing promise was kept in both. Its measure is line count.

for f in tariff.mjs insured.mjs hand-delivery.mjs; do echo "$f  $(grep -c . $f) lines"; done
tariff.mjs  27 lines
insured.mjs  9 lines
hand-delivery.mjs  10 lines

The 27 lines of tier, factor, and minimum-fee logic were not repeated in either subclass; both made do with nine to ten lines. There is no difference between the two subclasses regarding inheritance’s first promise. The difference is only in the second promise.

The Contract Checker

The three clauses are tried on a weight grid. The checker depends on the supertype; it does not know which subclass was given.

// contract.mjs — tries the fee contract's three clauses on a weight grid
export function violations(tariff) {
  const broken = new Set();
  for (const zone of tariff.zones()) {
    let previous = -Infinity;
    for (let w = 100; w <= 20000; w += 100) {
      let fee;
      try { fee = tariff.fee(w, zone); } catch { broken.add("C1 defined for every valid input"); continue; }
      if (!Number.isFinite(fee)) broken.add("C1 defined for every valid input");
      if (fee < tariff.minimum) broken.add("C2 does not fall below the minimum fee");
      if (fee < previous) broken.add("C3 does not decrease with weight");
      previous = fee;
    }
  }
  return [...broken].sort();
}

A Client Working Through the Supertype

The client finds the heaviest shipment that can be sent within a given budget. It uses binary search, and this choice rests directly on clause C3: if the fee at one weight exceeds the budget, no heavier shipment fits the budget either. The same file also holds a slow, one-by-one scanning version as the correctness reference.

// budget.mjs — client: finds the heaviest shipment that fits within a given budget
const MAX = 16000;

export function binarySearch(tariff, zone, budget) {
  let low = 0, high = MAX;
  while (low < high) {
    const mid = Math.ceil((low + high) / 2);
    if (tariff.fee(mid, zone) <= budget) low = mid; else high = mid - 1;
  }
  return low;
}

export function scan(tariff, zone, budget) {
  let best = 0;
  for (let w = 0; w <= MAX; w += 1) if (tariff.fee(w, zone) <= budget) best = w;
  return best;
}

The run passes the two subclasses first through the contract check, then through the client across three zones and sixty budgets.

// run.mjs — runs both subclasses through the same contract check and the same client
import { SETTINGS } from "./tariff.mjs";
import { InsuredTariff } from "./insured.mjs";
import { HandDeliveryTariff } from "./hand-delivery.mjs";
import { violations } from "./contract.mjs";
import { binarySearch, scan } from "./budget.mjs";

const BUDGETS = Array.from({ length: 60 }, (_, i) => 5000 + i * 500);

for (const [name, tariff] of [
  ["InsuredTariff", new InsuredTariff(SETTINGS, 0.2)],
  ["HandDeliveryTariff", new HandDeliveryTariff(SETTINGS, 8000)],
]) {
  const broken = violations(tariff);
  let wrong = 0;
  let maxGap = 0;
  for (const zone of tariff.zones()) {
    for (const budget of BUDGETS) {
      const found = binarySearch(tariff, zone, budget);
      const correct = scan(tariff, zone, budget);
      if (found !== correct) { wrong += 1; maxGap = Math.max(maxGap, correct - found); }
    }
  }
  console.log(`${name}`);
  console.log(`  contract violation = ${broken.length}  ${broken.join("; ")}`);
  console.log(`  wrong result       = ${wrong}/${BUDGETS.length * 3}`);
  console.log(`  largest gap        = ${maxGap} grams`);
}
node run.mjs
InsuredTariff
  contract violation = 0
  wrong result       = 0/180
  largest gap        = 0 grams
HandDeliveryTariff
  contract violation = 1  C3 does not decrease with weight
  wrong result       = 48/180
  largest gap        = 8001 grams

With the hand-delivery tariff, 48 of the 180 queries were answered wrong. At the largest gap, the client suggested a lighter shipment in a case where the budget actually covered a shipment 8001 grams heavier. There is no bug in the client’s code; binary search is correct whenever clause C3 holds. The error appeared because a contract-violating subclass was substituted for the supertype.

That the number is not the full 180 also carries information: the violation becomes visible only when one of the search’s probes lands inside the surcharge band, and other queries answer correctly. Contract violations do not detonate on every call but in specific input regions, which is why they slip past testing done with sample inputs.

Where the Contract Is Written

The hand-delivery tariff does not fail to pass any type check; on the contrary, it passes every one. The method signature is the same, the return type is the same, the extends bond is valid. Static typing catches part of C1 — the parameter and return types; it does not catch C2 and C3, because these are a relationship between values, not types.

The measured 48 wrong results are the cost of the contract not being written down. Putting the three clauses into text and applying them to every subclass with a checker like contract.mjs moves this cost from the client’s runtime back to the moment the subclass is written. The same checker showed the violation in a single line.

The hand-delivery need itself was not wrong; modeling it as a subclass was. The band surcharge is not a subtype of the tariff but a separate service line added to it; carried as a separate field alongside the fee, C3 is not violated.

Summary

  • Inheritance promises two separate things: implementation sharing and the subtype relationship; extends guarantees the first with certainty and may not guarantee the second at all.
  • Implementation sharing was kept in both subclasses: the 27 lines of superclass logic were not repeated in the nine- and ten-line subclasses.
  • The subtype relationship is checkable only if the supertype’s contract is written down; the contract here had three clauses, and the third could not be read from the code.
  • The compliant subclass produced 0 violations and 0 wrong results across 180 queries; the noncompliant one produced 1 violation and 48 wrong results, with a largest gap of 8001 grams.
  • A violation is visible not on every call but in specific input regions; testing with sample inputs answered 132 of the 180 queries correctly.
  • Static typing checks only signature compliance; clauses about the result’s value require a written contract and a checker that applies it.

Next Step

In this lesson, both subclasses behaved differently under the same name — fee — and the client did not know which one it was working with. This is a single name corresponding to more than one behavior, and the subtype relationship is only one way to achieve it. The next lesson builds all three ways: subtype, parametric, and ad hoc polymorphism. The measurement changes too — the same problem is implemented three ways, then a new tariff type and a new operation are added to the system, and the number of files touched in each way is counted.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close