LearnLLDOOPEncapsulation

Encapsulation

A customer’s wallet balance showed minus 4,000 rupees. The withdraw function could not have done that, it checks the balance first. It took a day to find a refund script in another folder that did wallet.balance -= amount directly and never went near the check.

Nobody wrote a bug in withdraw. The bug was that withdraw was optional.

Your answer

A Wallet class has a public balance field and a withdraw() method that refuses to overdraw. List the ways code outside the class could still leave the wallet in a wrong state.

The idea

The picture to keep
An ATM

The cash is in a vault you cannot reach. You get a few buttons, and the machine checks every request against your balance before a single note moves. Nobody thinks an ATM is restrictive. It is the reason the bank can trust the number on your statement.

In one line: Keep the data private, and make every change go through a method that checks it.

Encapsulation is two things done together: the data and the code that guards it live in one class, and the data is closed to everyone else. The rule that a balance never goes negative is called an invariant. Encapsulation is how you make an invariant impossible to skip, instead of something everyone has to remember.

The leak

class Wallet {
  balance = 0
}
 
const wallet = new Wallet()
wallet.balance = 500
wallet.balance -= 4500
 
console.log(wallet.balance) // -4000

Every field in a TypeScript class is public unless you say otherwise. So this Wallet is a variable with a class around it. Any file that can see the object can set any number.

Closing it

class Wallet {
  private balance = 0
 
  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
  }
}
 
const wallet = new Wallet()
wallet.deposit(500)
wallet.withdraw(200)
console.log(wallet.getBalance()) // 300
 
wallet.balance = 9999
// Error: Property 'balance' is private and only accessible within
//   class 'Wallet'.

One word, private, and the refund script from the top of this page no longer compiles. Its author is sent to withdraw, which is where the check lives.

try {
  wallet.withdraw(4500)
} catch (error) {
  if (error instanceof Error) console.log(error.message)
  // > Insufficient balance
}
 
console.log(wallet.getBalance()) // 300

The bad request was refused and the balance did not move. In catch, TypeScript treats error as unknown, because JavaScript lets you throw anything. The instanceof check narrows it to an Error so that .message is allowed.

Figure 1. Two callers, one door. Checkout and the refund job both go through the public methods, so both get the same checks.

Read only, and the constructor shortcut

Some data should be readable by anyone and writable by nobody. TypeScript has three tools for that, and the next snippet uses all of them.

class Wallet {
  private balance = 0
 
  constructor(readonly owner: string) {}
 
  get available(): number {
    return this.balance
  }
 
  deposit(amount: number): void {
    if (amount <= 0) throw new Error('Deposit must be positive')
    this.balance += amount
  }
}
 
const wallet = new Wallet('Asha')
wallet.deposit(500)
 
console.log(wallet.owner) // Asha
console.log(wallet.available) // 500
 
wallet.owner = 'Ravi'
// Error: Cannot assign to 'owner' because it is a read-only property.
wallet.available = 0
// Error: Cannot assign to 'available' because it is a read-only property.

constructor(readonly owner: string) {} is a parameter property. Putting readonly, private or public in front of a constructor parameter declares the field and assigns it in one go. It replaces three lines, and you will see it on every page from here on.

get available() is a getter with no matching setter, so from outside it behaves like a read only property.

private is a promise, # is a lock

This one catches people. private is checked by the compiler and then erased, like every other type. At runtime the field is an ordinary property.

class Wallet {
  private balance = 500
  #pin = '4321'
 
  checkPin(pin: string): boolean {
    return pin === this.#pin
  }
}
 
const wallet = new Wallet()
const sneaky = wallet as any
sneaky.balance = -4000
 
console.log(sneaky.balance) // -4000
console.log(Object.keys(wallet)) // [ 'balance' ]
console.log(wallet.checkPin('4321')) // true

as any switches the checker off for that value, and private goes with it. The #pin field is different. The # is JavaScript’s own private syntax, enforced by the engine, and it does not even show up in Object.keys.

My rule: private for application code, because it reads well and works with parameter properties. # when something really must not be reachable, like a secret, or when you are writing a library for strangers.

Two ways to get it wrong

Handing out the array

A private field that you return by reference is not private. The caller holds the same array you do and can push into it. Return it as readonly, or return a copy.

class Cart {
  private items: string[] = []
 
  add(item: string): void {
    this.items.push(item)
  }
 
  getItems(): readonly string[] {
    return this.items
  }
}
 
const cart = new Cart()
cart.add('Masala dosa')
 
cart.getItems().push('Free biryani')
// Error: Property 'push' does not exist on type 'readonly string[]'.
console.log(cart.getItems().length) // 1
A setter for every field

getBalance() plus setBalance(n) is a public field with extra typing. Nothing is guarded. Encapsulation is not about hiding fields behind accessors, it is about exposing actions that keep the object valid. withdraw(200) can say no. setBalance(-4000) cannot.

Encapsulation or abstraction?

The follow up is almost always the difference between the two. Encapsulation is about who can change the state, and it protects the object from its callers. Abstraction is about what the caller has to know, and it protects the callers from the details. A private balance is encapsulation. Calling pay() without knowing it is UPI underneath is abstraction.

TypeScript you just picked up
private balance = 0
Only code inside this class may read or write it. Checked at compile time.
#pin = "4321"
JavaScript private field. Enforced at runtime as well.
readonly owner: string
Can be set in the constructor and never again.
constructor(readonly owner: string) {}
Parameter property. Declares the field and assigns it in one line.
get available(): number
A getter. Read as wallet.available, with no brackets.
readonly string[]
An array you can read and loop over but not push to or reorder.
error instanceof Error
Narrows the unknown value in a catch block so .message is allowed.
wallet as any
A type assertion that turns checking off. Almost always a mistake.

Checkpoint

Checkpoint

1. A class has private balance and a public setBalance(amount) that assigns it directly. Is the balance encapsulated?

2. At runtime, what stops code from changing a field marked private in TypeScript?

3. Cart has private items: string[] and getItems() returns this.items typed as string[]. What is the problem?

Say this in 60 seconds

Encapsulation means the data and the rules that protect it live in one class, and everything outside has to go through those rules. I make fields private and expose actions, like deposit and withdraw, that can refuse a bad request, so an invariant such as a balance never going negative cannot be skipped. I do not add a setter for every field, because that is a public field with more typing. In TypeScript, private is a compile time check that is erased at runtime, and a hash field is enforced by the engine. And I never return a private array by reference, I return it as readonly or as a copy.

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