OOP cont.
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();