---
title: Inheritance
source: 'https://academia.sh/en/courses/paradigms/inheritance'
course: 'Programming Paradigms'
language: en
updated: '2026-08-23T07:01:20+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
// 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.

```js
// 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.

```js
// 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.

```sh
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.

```js
// 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.

```js
// 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.

```js
// 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`);
}
```

```sh
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.
