Skip to content

Instantly share code, notes, and snippets.

@paulwongx
Created October 18, 2023 18:02
Show Gist options
  • Save paulwongx/3a6c69d9d54b92836ec41c2d6a073b3e to your computer and use it in GitHub Desktop.
Save paulwongx/3a6c69d9d54b92836ec41c2d6a073b3e to your computer and use it in GitHub Desktop.
Object Oriented Programming (OOP) Notes

Object Oriented Programming

SOLID Principles

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

1. Single Responsibility Principle (SRP)

A class should only have one responsibility. Do exactly one thing.

  • In React, break out react components into single responsibility reusable components
    • Usually items you map over
    • When components have a useState and useEffect, refactor it into a custom hook

Example:

// Without SRP
class UserManagement {
	registerUser(user) {
		/* ... */
	}
	loginUser(user) {
		/* ... */
	}
	generateReports(user) {
		/* ... */
	}
}

// With SRP
class UserManager {
	registerUser(user) {
		/* ... */
	}
	loginUser(user) {
		/* ... */
	}
}

class ReportGenerator {
	generateReports(user) {
		/* ... */
	}
}

2. Open/Closed Principle (OCP)

Software entities (classes, modules, functions) should be open for extension but closed for modification. You should be able to extend the behaviour of a class without changing it's source code

  • In React, props should accept more generic values and not specific implementations
    • Accept an icon prop as opposed to a role prop (back, forward, main) that determines an icon

Example:

// Without OCP
class Circle {
	radius: number;

	constructor(radius: number) {
		this.radius = radius;
	}

	area() {
		return Math.PI * this.radius * this.radius;
	}
}

// Extending the Circle class
class ColoredCircle extends Circle {
	color: string;

	constructor(radius: number, color: string) {
		super(radius);
		this.color = color;
	}

	// Violates OCP because we had to modify the source code
	area() {
		return super.area() + ` of color ${this.color}`;
	}
}

// With OCP
interface Shape {
	area(): number;
}

class Circle implements Shape {
	radius: number;

	constructor(radius: number) {
		this.radius = radius;
	}

	area() {
		return Math.PI * this.radius * this.radius;
	}
}

class ColoredCircle implements Shape {
	circle: Circle;
	color: string;

	constructor(circle: Circle, color: string) {
		this.circle = circle;
		this.color = color;
	}

	area() {
		return this.circle.area() + ` of color ${this.color}`;
	}
}

3. Liskov Substitution Principle (LSP)

Subtype objects should be substitutable for supertype objects. In TypeScript, subclasses should not violate the expected behavior of the base class.

  • In React, pass in props to the underlying parent component
    • className should be passed through into the underlying component
    • ...props should be passed into the underlying component
    • forwardRef should be passed into the underlying component

Example:

// Violating LSP
class Bird {
	fly() {
		/* ... */
	}
}

class Ostrich extends Bird {
	fly() {
		throw new Error("Ostriches can't fly");
	}
}

// With LSP
class Bird {
	feathers: number;
	beakColor: string;
}
// All flying birds
class Neornithes extends Bird {
	fly() {}
}
// Non-flying birds
class Ratite extends Bird {}

class Ostrich extends Ratite {}

4. Interface Segregation Principle (ISP)

Clients should not depend upon interfaces that they don't use. It promotes creating small, specific interfaces rather than large monolithic ones.

  • In React, components shouldn't depend on props it doesn't use
    • Don't pass a whole object when you only need some parameters for the component

Example:

// Violating ISP
interface Worker {
	work(): void;
	eat(): void;
}

class Engineer implements Worker {
	work() {
		/* ... */
	}
	eat() {
		/* ... */
	}
}

class Manager implements Worker {
	work() {
		/* ... */
	}
	eat() {
		/* ... */
	}
}

// With ISP
interface Workable {
	work(): void;
}

interface Feedable {
	eat(): void;
}

class Engineer implements Workable, Feedable {
	work() {
		/* ... */
	}
	eat() {
		/* ... */
	}
}

class Manager implements Workable {
	work() {
		/* ... */
	}
}

5. Dependency Inversion Principle (DIP)

An entity should depend upon abstractions, not concretions. In TypeScript, this often involves using interfaces and dependency injection.

  • In React, make a react component a standalone component and able to extend
    • Passing in handleSubmit into a component, as opposed to writing it in the component. Calling it ConnectedForm that has the handleSubmit and the Form component

Example:

// Without DIP
class LightBulb {
	turnOn() {
		/* ... */
	}
	turnOff() {
		/* ... */
	}
}

class Switch {
	bulb: LightBulb;

	constructor(bulb: LightBulb) {
		this.bulb = bulb;
	}

	toggle() {
		if (this.bulb.isOn) {
			this.bulb.turnOff();
		} else {
			this.bulb.turnOn();
		}
	}
}

// With DIP
interface Switchable {
	turnOn(): void;
	turnOff(): void;
}

class LightBulb implements Switchable {
	turnOn() {
		/* ... */
	}
	turnOff() {
		/* ... */
	}
}

class Switch {
	device: Switchable;

	constructor(device: Switchable) {
		this.device = device;
	}

	toggle() {
		if (this.device.isOn) {
			this.device.turnOff();
		} else {
			this.device.turnOn();
		}
	}
}

Object Oriented Typescript

Source: https://www.youtube.com/watch?v=HsWKyERYGKQ

  • Writing functions requires changing the function each time it isn't compatible with another function
  • Writing objects require much more thoughtfulness upfront but allows for extending functionality and ideally only writing it once (or a few times only)

Encapsulation and private variables

  • Protect your variables and methods appropriately using the private or protected names
    • private - Can't be changed by external client, can't be accessed by a subclass
    • protected - Can't be changed by external client, can be accessed by a subclass
// Not private
class Player {
  health: number
  speed: number
}

const mario = new Player()
mario.health = -8
mario.speed = 1

// Private variables
class Player {
  private health: number

  setHealth(health: number) {
    if (health < 0) {
      console.log("You can't set the health below 0");
      return;
    }
    this.health = health
  }

  getHealth() {
    return this.health
  }
}

const mario = new Player();
mario.setHealth(10)

Inheritenace

  • Avoids code duplication
  • Making sure class hierarchies make sense
    • IS-A - Used to determine if a class should extend another. Ex. Dog is an Animal
    • HAS-A - Used to determine if a class should have a variable. Ex. Animals have coordinates, Dog has a owner
class Animal() {
  protected coordX: number
  protected coordY: number
}

// Dog IS-A Animal
class Dog extends Animal {
  owner: string; // Dog HAS-A owner

  returnToOwner() {
    console.log(`I'm at (${this.coordX}, ${this.coordY})`)
  }
}

Multiple level inheritance

  • You can override the super class method or extend it by called super.method()
class Animal() {
  protected coordX: number
  protected coordY: number

  makeNoise() {
    console.log("Make noise");
  }

  move() {
    console.log(`I'm moving from coord (${this.coordX}, ${this.coordY})`)
  }

}
class Canine extends Animal {}
class Dog extends Canine {

  // Overriding the super class method
  makeNoise() {
    console.log("bark bark bark");
  }

  // Calling the super class method
  move() {
    console.log("getting on all four paws...")
    super.move(); // I'm moving from coord(_,_)
  }
}
class Wolf extends Canine {}

// dog can access methods in Animal and Canine
const dog = new Dog();

Polymorphism

Inheritance

  1. Removes code duplication
  2. Providing a common protocol for a group of subclasses (ie., polymorphism)
  • A subclass is the type of the super class in typescript (ie., an archer is a Hero. archer:Hero = new Archer())
    • Poly = many
    • Morph = forms
    • So a hero can be many forms (archer, mage, knight)
class Hero {
  hunger: number;
  health: number

  attack() {
    console.log("I'm attacking")
  }
  move() {
    console.log("I'm moving")
  }
  eat() {
    console.log("I'm eating")
  }
}

class Archer extends Hero {
  arrows: number

  attack() {
    super.attack()
    console.log("Firing an arrow")
    this.arrows -= 1;
  }
}

class Mage extends Hero {
  mana: number;

  attack() {
    super.attack();
    console.log("Throwing a potion")
    this.mana -= 1;
  }
}

class Knight extends Hero {
  sword: number

  attack() {
    super.attack();
    console.log("I'm swinging with a sword")
  }
}

// Since an archer extends a hero, an archer IS-A hero
const archer: Hero = new Archer();
const mage: Hero = new Mage();
const knight: Hero = new Knight();

archer.attack();
mage.attack();
knight.attack();

class Tribe {
  private heros: Hero[]

  setHeros(heros: Hero[]) {
    this.heros = heros
  }

  // This method can depend on the fact that the hero is of type Hero and has an attack method
  attack(): void {
    for (let hero of this.heros) {
      hero.attack();
    }
  }
}

const heros: Hero[] = [arhcer, mage, knight]
const tribe = new Tribe()
tribe.setHeros(heros)
tribe.attack()
  • Weeks later, you can create a new Thief hero and add it to the tribe as a new set of heros and it'll still work by extending the functionality as opposed to modifying it

Abstract Classes

  • abstract Restrict a class from being instantiated. Allows inheritance when extended
  • abstract methods - Method MUST be implemented in concrete classes. Need to be overridded.
  • concrete classes - Classes that can be instantiated
  • abstract methods must be in abstract classes - since if you extend a class with an abstract method, the method isn't implemented
  • abstract classes can extend other abstract classes without implemented the abstract method
abstract class Hero {
  abstract attack(): void;

  move(): void {
    console.log("I'm moving");
  }
}

class Archer extends Hero {
  attack() {
    console.log("Firing an arrow");
  }
}

const archer: Archer = new Archer();
const knight: Knight = new Knight();

const bob: Hero = new Hero();

// Abstract classes can extend other abstract classes
abstract class Mage extends Hero {
  mana: number;
}

class Wizard extends Mage {
  attack() {
    this.mana -= 1;
    console.log("Wizard attacks")
  }
}
class Witch extends Mage {
  attack() {
    this.mana -= 1;
    console.log("Witch attacks")
  }
}

Multiple Inheritance and Interfaces

  • Classes cannot extend multiple classes - since methods may clash
  • interface - A subclass can extend multiple interface classes
  • implements - Keyword used to implement an interface
  • Using multiple inheritance for polymorphism of multiple classes trades off inheritance from the super class

The example below doesn't work:

class Character {
  hunger: number;
  health: number;
}

class Hero extends Character {
  heroId: number;

  eat() {
    this.hunder += 3;
  }
}

class Enemy extends Character {
  enemyId: number;

  eat() {
    this.hunder += 1;
  }
}
// Does NOT work
class Spy extends Hero, Enemy{}

The example below DOES work:

// DOES work
abstract class Character {
  hunger: number;
  health: number;

  abstract eat(): void;
}

interface Hero extends Character {
  heroId: number;
}

interface Enemy extends Character {
  enemyId: number;
}

class Spy implements Hero, Enemy{
  // Need to implement hunder and health yourself since Character is an abstract class
  hunger: number;
  health: number;
  heroId: number;
  enemyId: number;

  eat() {
    this.hunger -= 1;
  }
}

const hero: Hero = new Spy();
const enemy: Enemy = new Spy();
  1. Basic classes
    • When it doesn't pass any IS-A test or there is no super class
  2. Subclasses
    • When your class IS-A existing class
  3. Abstract classes
    • To create a template for other classes, but don't want that class to be instantiated
  4. Interfaces
    • Need multiple types for Polymorphic reasons
// 1. No other classes. Doesn't pass any IS-A test
class Character {}

// 2. Knight and Archer IS-A character
class Knight extends Character {}
class Archer extends Character {}

// 3. Abstract classes - template for other classes, but don't want Mage to be instantiated
abstract class Mage extends Character {}
class Wizard extends Character {}
class Witch extends Character {}

// 4. Interfaces - Implementing multiple interfaces
interface Hero extends Character {}
interface Enemy extends Character {}
class Spy implements Hero, Enemy {}

Constructors, Static, Parameter, Readonly

  • Typescript parameter properties - Ability to define the constructor variables inline constructor(public hunger: number, public health: number) {}
  • static - variables and methods that can only be called from the Class itself and not an instance of the class
    • static variables are like shared state amongst objects
  • readonly - cannot modify it after it's been instantiated
class Character {
  // Lives on the class level not on the instance level
  // Can call Character.characterCount
  static characterCount = 0;
  private hunger: number;
  private health: number;

  constructor(hunger: number, health: number) {
    Character.characterCount += 1;
    console.log(`I'm the ${Character.characterCount} character created`);
    this.hunger = hunger;
    this.health = health;
  }

  setHunger(hunger: number): void {
    this.hunger = hunger;
  }

  setHealth(health: number): void {
    this.health = health;
  }

  getHunger(): number {
    return this.hunger;
  }

  getHealth(): number {
    return this.health;
  }
}

class Hero extends Character {
  private heroId: number;
  // Cannot mutate this variable outside of the constructor
  private readonly wealth: number;

  constructor(heroId: number, hunger: number, health: number) {
    // Need to call the super class constructor. Similar to calling `new Character()`
    super(hunger, health)
    this.heroId = heroId;
  }

  setHeroId(heroId: number): void {
    this.heroId = heroId;
  }

  getHeroId(): number {
    return this.heroId;
  }
}

const jeff = new Character(100,100);

Typescript parameter properties

// Less typing, but more difficult to understand
class Character {
  constructor(public hunger: number, public health: number) {}
}

// same as
class Character {
  public hunger: number;
  public health: number;

  constructor(hunger: number, health: number) {
    this.hunger = hunger;
    this.health = health;
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment