Principles of OOP in more detail

Encapsulation and Abstraction

Data hiding is an essential component of object oriented programming. At its most basic, it means hiding away the internal functions of a class and providing an interface to interact with, without knowing all the details within. Prevents unauthorized access or modification of the original contents.

A class is by its nature an encapsulation. Good practice is to declare all variables within a class private, then write getters and setters for access, as well as other custom methods.

class ParentClass {
	#var;
	#anotherVar;

	constructor(v = "", a = 0) {
		this.#var = v;
		this.#anotherVar = a;
	}

	get_var() {
		return this.#var;
	}

	set_var(v) {
		// skipping check before assignment s
		this.#var = v;
	}

	print_all_vars() {
		console.info(`var was: `, this.#var);
		console.info(`anotherVar was: `, this.#anotherVar);
	}
}

Abstraction is hiding away the details of something. Here in a circle class, all that the other code needs to know is a radius, after which both perimeter and area can be calculated. Area and perimeter functions are abstractions that hide away the calculation.

class Circle {
  radius;
  pi;
  //define data attributes within the constructor
  constructor (radius = 0) {
    this.radius = radius;
    this.pi = 3.142
  }
  //define methods
  getArea() {
    return this.pi * Math.pow(this.radius, 2);
  }
  getPerimeter() {
    return 2 * this.pi * this.radius;
  }
};

const circle = new Circle(5);

Inheritance

  • inheritance, copy with attrs and methods from a parent class as a starting point, helps with reusability
    • inheritance can be single or multi level
    • items can inherit from multiple parent classes
    • a single parent class can have many children classes inherit from it

Design Tool - UML

Unified Modeling Language is a standardized catalog of ways to describe a system.

Three types of main building blocks

  • things
  • relationships
  • diagrams

Things

  • Structural, physical thing - class, drawn as box with list of properties and methods
    • object, pretty much same as class
    • interface, box around two items to show they are connected
    • use case, box around a circle
    • actor, person icon
    • component, oblong shape with its name
    • node, cube with its name
  • Behavioral
  • showing activities
  • showing flow of information
  • Grouping
  • Annotation

SOLID principles for software

Created by Robert C. Martin, these design principles are considered best practice for object oriented design. Classes are considered the implementation of this as far as I can tell.

S - Single Responsibility Principle O - Open/Closed Principle L - Liskov substitution I - Interface Segregation D - Dependency Inversion

Single Responsibility

A class should do one thing and one thing only, keeping things apart makes things easier to test, maintain, and refactor.

So instead of writing all the utilities in one class like so BAD:

class Report {
  generate() { /* ... */ }
  print() { /* ... */ }  // printing is a separate concern
}

Write two classes, one that handles objects of the other. GOOD:

class Report {
  generate() { /* ... */ }
}

class ReportPrinter {
  print(report) { /* ... */ }
}

Open/Closed

Classes are open for extension but closed for modification. Build things in such a way that they can be extended, such as a Shape parent class that then can have Square and Circle sub-classes.

class Shape {
  draw() {}
}

class Circle extends Shape {
  draw() { /* draw circle */ }
}

class Square extends Shape {
  draw() { /* draw square */ }
}

function drawShape(shape) {
  shape.draw();
}

Liskov Substitution

Substituting parent objects into a subclass shouldn’t break functionality. In other words, subclasses shouldn’t break their parent’s methods.

class Bird {}
class FlyingBird extends Bird {
  fly() {}
}

class Sparrow extends FlyingBird {}
class Ostrich extends Bird {}

Interface Segregation

Rather than have one class throw errors trying to satisfy an interface it cannot, break up the interfaces.

interface Printer {
  print(): void;
}

interface Scanner {
  scan(): void;
}

class BasicPrinter implements Printer {
  print(): void {
    console.log("Printing...");
  }
}

class AllInOnePrinter implements Printer, Scanner {
  print(): void {
    console.log("Printing...");
  }
  scan(): void {
    console.log("Scanning...");
  }
}

Dependency Inversion

High level modules should not have low level modules as dependencies. Both should rely on abstractions to create instances and pass them.

BAD:

class MySQLDatabase {
  connect(): void {
    console.log("Connected to MySQL");
  }
}

class UserService {
  private db = new MySQLDatabase();

  getUsers(): void {
    this.db.connect();
    console.log("Getting users");
  }
}

GOOD:

interface Database {
  connect(): void;
}

class MySQLDatabase implements Database {
  connect(): void {
    console.log("Connected to MySQL");
  }
}

class UserService {
  constructor(private db: Database) {}

  getUsers(): void {
    this.db.connect();
    console.log("Getting users");
  }
}

const service = new UserService(new MySQLDatabase());
service.getUsers();