LearnLLDOOPInheritance

Inheritance

SavingsAccount was 140 lines. CurrentAccount was 140 lines. 120 of them were the same, because someone copied one file to make the other. In March a rounding bug in deposit was fixed in the savings file. The current account file still has it.

Copy and paste means every fix has to be made twice, and the second time gets forgotten. Inheritance is the first tool people reach for to stop that. It is also the tool most often used where it does not belong, so this page spends as long on when not to use it.

Your answer

A bank has savings accounts and current accounts. A customer can hold several accounts. Which of these should inherit from which, and which should not inherit at all? Say why.

The idea

The picture to keep
A family recipe

You make your mother’s biryani. You did not write the recipe out again. You took all of it, you use a little less oil, and you add fried onions at the end. When she improves the marinade, yours improves too, because you are still following her recipe for that step.

In one line: A child class gets everything the parent has, and then adds to it or changes one step.

Writing it once

class Account {
  protected balance = 0
 
  constructor(readonly owner: string) {}
 
  deposit(amount: number): void {
    if (amount <= 0) throw new Error('Deposit must be positive')
    this.balance += amount
  }
 
  withdraw(amount: number): void {
    if (amount > this.balance) throw new Error('Insufficient balance')
    this.balance -= amount
  }
 
  getBalance(): number {
    return this.balance
  }
}
 
class SavingsAccount extends Account {
  addInterest(ratePercent: number): void {
    this.balance += (this.balance * ratePercent) / 100
  }
}
 
const savings = new SavingsAccount('Asha')
savings.deposit(1000)
savings.addInterest(4)
 
console.log(savings.getBalance()) // 1040
console.log(savings.owner) // Asha

SavingsAccount extends Account gives the savings account everything Account has: the fields, the constructor, deposit, withdraw, getBalance. The only code written for it is the part that is new.

balance is protected here, not private. Private would hide it from SavingsAccount too, and addInterest needs it.

ModifierThe class itselfSubclassesEveryone else
public (the default)yesyesyes
protectedyesyesno
privateyesnono

Changing one step

A current account is allowed to go below zero, up to a limit. Same method name, different rule. That is overriding.

class CurrentAccount extends Account {
  constructor(owner: string, private readonly overdraftLimit: number) {
    super(owner)
  }
 
  override withdraw(amount: number): void {
    const limit = this.balance + this.overdraftLimit
    if (amount > limit) throw new Error('Overdraft limit exceeded')
    this.balance -= amount
  }
}
 
const current = new CurrentAccount('Chai Point', 5000)
current.deposit(1000)
current.withdraw(4000)
 
console.log(current.getBalance()) // -3000

Two new things. super(owner) calls the parent’s constructor, and TypeScript insists it comes before you touch this. And override tells the compiler that you mean to replace a parent method.

You can also keep the parent’s step and add to it, with super.method().

class SalaryAccount extends Account {
  override deposit(amount: number): void {
    super.deposit(amount)
    console.log(`SMS to ${this.owner}: ${amount} credited`)
  }
}
 
const salary = new SalaryAccount('Ravi')
salary.deposit(52000)
// > SMS to Ravi: 52000 credited

The validation in Account.deposit still runs. The subclass only added the SMS after it.

Why the override keyword earns its place

class Account {
  withdraw(amount: number): void {}
}
 
class KidsAccount extends Account {
  override withdrawl(amount: number): void {}
  // Error: This member cannot have an 'override' modifier because it
  //   is not declared in the base class 'Account'.
}

Without override, that typo compiles. You would have a brand new method called withdrawl that nothing calls, and the parent’s withdraw running with no limits on a child’s account. With noImplicitOverride on, the keyword is mandatory, and the compiler catches both the typo and the day someone renames the parent method.

Figure 1. Three kinds of account share one parent. A customer is not an account, so it holds accounts instead of extending one.

When not to inherit

Inheritance is the tightest link you can make between two classes. The child depends on every method, every field and every future change of the parent. So before you write extends, say this sentence out loud:

A savings account is an account.

If it sounds right, and it will stay right for every method the parent has, inherit. If the honest sentence is “has a”, do not.

class Account {
  constructor(private balance: number) {}
 
  getBalance(): number {
    return this.balance
  }
}
 
class Customer {
  private readonly accounts: Account[] = []
 
  open(account: Account): void {
    this.accounts.push(account)
  }
 
  netWorth(): number {
    return this.accounts.reduce((sum, a) => sum + a.getBalance(), 0)
  }
}
 
const asha = new Customer()
asha.open(new Account(1040))
asha.open(new Account(-3000))
 
console.log(asha.netWorth()) // -1960

A customer has accounts. This is composition: one object holds another and uses it through its public methods. Customer knows nothing about how a balance is stored, and a change inside Account cannot break it.

Inheriting to save typing

class Customer extends Account would work, for a week. Then a customer opens a second account and the model has no place for it, and customer.withdraw() reads like nonsense. If you find yourself extending a class only because it has a method you want, hold an instance of it instead and call the method.

The tree that keeps growing

Bike, then ElectricBike, then RentedBike, then RentedElectricBike. Two independent choices (fuel and ownership) forced into one tree gives you a class for every combination. Past two levels deep, stop and split the choices into separate objects that the class holds. The Strategy and Decorator pages are both ways of doing that.

Composition over inheritance

You will be asked what this phrase means. A good answer: “Inheritance fixes the relationship at compile time and exposes the parent’s internals to the child. Composition lets me swap the part at runtime and only depends on its public methods. I inherit when there is a true is-a relationship and the child can stand in for the parent everywhere. Otherwise I compose.” That last clause is the Liskov substitution principle, and naming it earns the follow up you want.

Is a, or has a?

Decide before you reveal
CreditCard and Card
Order and OrderItem
Rider and Bike
AdminUser and User
Stack and Array
TypeScript you just picked up
class SavingsAccount extends Account
Inherit every field and method of Account. One parent only.
super(owner)
Call the parent constructor. Required before using this in a subclass constructor.
super.deposit(amount)
Call the parent version of a method you have overridden.
override withdraw(...)
Replace a parent method. The compiler checks the parent really has it.
protected balance
Reachable from subclasses, hidden from outside code.
constructor(owner: string, private readonly limit: number)
A plain parameter passed up to super, and a parameter property kept on the subclass.
accounts.reduce((sum, a) => sum + a.getBalance(), 0)
Fold an array into one value. The 0 is the starting sum, and sets its type.

Checkpoint

Checkpoint

1. A parent field is marked private. Can a subclass method read it?

2. With noImplicitOverride on, you misspell an overriding method as withdrawl and mark it override. What happens?

3. You need Report to be able to send emails, and EmailSender already has a send() method. What should you do?

Say this in 60 seconds

Inheritance lets a child class take everything a parent has and then add to it or override a step, so shared behaviour is written once. In TypeScript that is extends, with super to reach the parent and the override keyword so the compiler checks I am really replacing something. Protected is what lets a subclass see a field that outsiders cannot. But inheritance is the tightest coupling there is, so I only use it for a true is-a relationship where the child can stand in for the parent everywhere. When the honest sentence is has-a, or when I only want to reuse a method, I use composition: hold the other object and call it.

IndGeek provides solutions in the software field, and is a hub for ultimate Tech Knowledge.