๐ŸŽจ

SOLID OOP

โ€ฃ
๐Ÿ”ช What is the Single Responsibility Principle (SRP)?
โ€ฃ
Every software component should have one and only one responsibility. A software component can be a class method or module
image
  • SRP Example: Swiss Army Knife vs. Knife
  • image

What is cohesion?

โ€ฃ
โ™ป๏ธ Cohesion
Cohesion is the degree to which the various parts of a software component are related of relation
image
  • Top: Unsegregated trash. It is low cohesion
  • Bottom: Notice the yellow bin. The bottles are not alike. However, the contents of the yellow bin have a common relation - they are all made of plastic. It is high cohesion
โ€ฃ
How would you describe the methods of this class Square in the context of the SRP
export class Square {
  constructor(public side: number) {}

  public calculateArea(): number {
    return this.side * this.side;
  }

  public calculatePerimeter(): number {
    return this.side * 4;
  }

  public draw(): void {
    if (this.highResolutionMonitor) {
      //render a high res image of a square
    } else {
      //render a low res image of a square
    }
  }
  public rotate(degree: number): void {
    // rotate image clockwise
  }
}
  1. calculateArea() and calculatePerimeter() methods have high degree of cohesion / relation
  2. draw() and rotate() methods also have high degree of cohesion / relation
  • However, #1 and #2 do not.
โ€ฃ
How would you refactor class Square to increase cohesion?
export class Square {
  constructor(public side: number) {}

  public calculateArea(): number {
    return this.side * this.side;
  }

  public calculatePerimeter(): number {
    return this.side * 4;
  }

  public draw(): void {
    if (this.highResolutionMonitor) {
      //render a high res image of a square
    } else {
      //render a low res image of a square
    }
  }
  public rotate(degree: number): void {
    // rotate image clockwise
  }
}
โ€ฃ
class Square refactor to class Square and class SquareUI
๐Ÿ’ก
class Square Responsibility: Measurements of squares
export class Square {
  constructor(public side: number) {}

  public calculateArea(): number {
    return this.side * this.side;
  }

  public calculatePerimeter(): number {
    return this.side * 4;
  }
}
๐Ÿ’ก
class SquareUI Responsibility: Rendering of squares
class SquareUI {
  public draw(): void {
    if (this.highResolutionMonitor) {
      //render a high res image of a square
    } else {
      //render a low res image of a square
    }
  }
  public rotate(degree: number): void {
    // rotate image clockwise
  }
}
โ€ฃ
What is the the (single) responsibility of class Square and class SquareUI?
๐Ÿ’ก
class Square Responsibility: Measurements of squares
export class Square {
  constructor(public side: number) {}

  public calculateArea(): number {
    return this.side * this.side;
  }

  public calculatePerimeter(): number {
    return this.side * 4;
  }
}
๐Ÿ’ก
class SquareUI Responsibility: Rendering of squares
class SquareUI {
  public draw(): void {
    if (this.highResolutionMonitor) {
      //render a high res image of a square
    } else {
      //render a low res image of a square
    }
  }
  public rotate(degree: number): void {
    // rotate image clockwise
  }
}
๐Ÿ”‘
High Cohesion helps attain better adherence to the SRP

Coupling

โ€ฃ
๐Ÿš‚ Train Example: Coupling
Coupling is defined as the level of interdependency between various software components
image
  • Standard Gauge Rail (1.4 m wide) vs Broad Gauge Rail (1.6 m wide)
  • The trains are tightly coupled to their track
โ€ฃ
How would you describe the methods of this class Student in the context of the SRP?
class Student {
  private studentId: string;
  private dob: Date;
  private address: string;

  public save(): void {
    const query = JSON.stringify(this);
    let connection: Connection = null;
    let statement: Statement = null;
    try {
      connection = db.connect("mysql://localhost:3306");
      statement = connection.createStatement();
      connection.execute(`INSERT INTO STUDENT VALUES ${query}`);
    } catch {
      console.error();
    }
  }
  public getStudentId(): string {
    return this.studentId;
  }
  public setStudentId(studentId: string): void {
    this.studentId = studentId;
  }
	...
	...
	...
}
  • Tightly coupled with the database layer
  • Student class should not be cognizant of the low level details related to dealing with the database
โ€ฃ
How would you refactor class Student to decrease coupling? What would this refactor allow?
class Student {
  private studentId: string;
  private dob: Date;
  private address: string;

  public save(): void {
    const query = JSON.stringify(this);
    let connection: Connection = null;
    let statement: Statement = null;
    try {
      connection = db.connect("mysql://localhost:3306");
      statement = connection.createStatement();
      connection.execute(`INSERT INTO STUDENT VALUES ${query}`);
    } catch (Error) {
      console.error(Error.message);
    }
  }
  public getStudentId(): string {
    return this.studentId;
  }
  public setStudentId(studentId: string): void {
    this.studentId = studentId;
  }
	...
	...
	...
}
  • Tightly coupled with the database layer
  • Student class should not be cognizant of the low level details related to dealing with the database
โ€ฃ
Refactor
class Student {
  private studentId: string;
  private dob: Date;
  private address: string;

  public save(): void {
    (new StudentRepository).save(this)
  }
  public getStudentId(): string {
    return this.studentId;
  }
  public setStudentId(studentId: string): void {
    this.studentId = studentId;
  }
	...
	...
	...
}

class StudentRepository {
	public save(student: Student): void {
    const query = JSON.stringify(this);
    let connection: Connection = null;
    let statement: Statement = null;
    try {
      connection = db.connect("mysql://localhost:3306");
      statement = connection.createStatement();
      connection.execute(`INSERT INTO STUDENT VALUES ${query}`);
    } catch (Error) {
      console.error(Error.message);
    }
}
  • This would allow us the flexibility to change our database operations details without changing our student class, or switch out our underlying database if we wanted to
โ€ฃ
What is the (single) responsibility of class Student and class StudentRepository?
๐Ÿ’ก
The class Student has the single responsibility of handling core student related data
class Student {
  private studentId: string;
  private dob: Date;
  private address: string;

  public save(): void {
    (new StudentRepository).save(this)
  }
  public getStudentId(): string {
    return this.studentId;
  }
  public setStudentId(studentId: string): void {
    this.studentId = studentId;
  }
	...
	...
	...
}
๐Ÿ’ก
The class StudentRepository has the single responsibility of handling database operations

class StudentRepository {
	public save(student: Student): void {
    const query = JSON.stringify(this);
    let connection: Connection = null;
    let statement: Statement = null;
    try {
      connection = db.connect("mysql://localhost:3306");
      statement = connection.createStatement();
      connection.execute(`INSERT INTO STUDENT VALUES ${query}`);
    } catch (Error) {
      console.error(Error.message);
    }
}

An alternative interpretation

โ€ฃ
What is another way of describing the SRP?
Every software component should have one and only one reason to change.
โ€ฃ
How would you test for low cohesion and high coupling? Alternatively, how do you identify violations of the SRP?
  • Ask yourself, how many reasons a piece of code has to change, and make refactors based on that
โ€ฃ
How would you fix code that violates the SRP?
  • Refactor, and aim for high cohesion, and low coupling.
โ€ฃ
What are the reasons this code would need to change?
class Student {
  private studentId: string;
  private dob: Date;
  private address: string;

  public save(): void {
    const query = JSON.stringify(this);
    let connection: Connection = null;
    let statement: Statement = null;
    try {
      connection = db.connect("mysql://localhost:3306");
      statement = connection.createStatement();
      connection.execute(`INSERT INTO STUDENT VALUES ${query}`);
    } catch (Error) {
      console.error(Error.message);
    }
  }
  public getStudentId(): string {
    return this.studentId;
  }
  public setStudentId(studentId: string): void {
    this.studentId = studentId;
  }
	...
	...
	...
}
  1. Change to student id format
  2. Change in the student name format
  3. A change to the database
๐Ÿ’ก
#1 and #2 can be combined because they're closely related. So you can say there are two reasons: 1. Changes to student profile 2. Change to backend
โ€ฃ
๐ŸŽฎ What is the Open/Closed Principle (OCP)?
Software components should be closed for modification but open for extension
image
๐Ÿ’ก
The Wii was designed in such a way that it is closed for modification, but open for extension
โ€ฃ
What does closed for modification mean?
  • New features getting added to the software component, should NOT have to modify existing code.
โ€ฃ
What does open for extension mean?
  • A software component should be extendable to add a new feature or to add a new behavior to it.
โ€ฃ
๐Ÿ’ธ Example: Suppose you have a system that calculates insurance premium discounts for health insurance customers implemented like so
class InsurancePremiumDiscountCalculator {
	public caculateDiscount(customer: HealthInsuranceCustomerProfile): number {
		if(customer.isLoyalCustomer()) {
			return 20
		}
		return 0
	}
}

class HealthInsuranceCustomerProfile {
	public isLoyalCustomer(): boolean {
		return true; //or false
	}
}
  • Now, suppose you would like to change this system to be able to caclulate discounts for another kind of customer like below:
class VehicleInsuranceCustomerProfile {
	public isLoyalCustomer(): boolean {
		return true;//or false
	}
}
โ€ฃ
How could you refactor the above? Which one should you choose and why?
  1. You could implement another method that will make the same calculation, but for the class VehicleInsuranceCustomerProfile
  2. You could define an interface interface CustomerProfile that both our customer profile classes could implement, and change the method signature to reflect the newly defined interface.
  3. interface CustomerProfile {
    	public isLoyalCustomer(): boolean;
    }

You should choose option 2.

๐Ÿ‘Ž Option 1 repeats code, and would force you to modify the discount calculator class for any new kind of customer profile.

๐Ÿ‘Option 2 would allow you to extend the functionality of the discount calculator for any customer profile that implements CustomerProfile

โ€ฃ
๐Ÿ‘ What are the key benefits of OCP?
  • Ease of adding new features.
  • Leads to minimal cost of developing and testing software.
  • Open Closed Principle often requires decoupling, which, in turn, automatically follows the Single Responsibility Principle.
โ€ฃ
What should you be wary of when following SRP+OCP?
  • You could end up with too many classes and overly complicated design.
โ€ฃ
How do you identify violations of the OCP?
  • When you need to modify and or repeat existing code to accommodate additional features that have similar functionality.
โ€ฃ
How do you fix violations of the OCP?
  • Interfaces! Usually helps improve reusability of code for different kinds/types of input.
โ€ฃ
๐Ÿ›’ What is Liskov Substitution Principle (LSP)?
Objects should be replaceable with their subtypes without affecting the correctness of the program
โ€ฃ
Which of the following violate the LSP? Why?
image

The ostrich "is-a" bird, but ostriches can't fly. So any function that takes an argument of type Bird (or a subtype of bird) that calls the fly method will throw an error for instances of the Ostrich class.

image
๐Ÿ’ก
Unimplemented methods in child classes are indicative of a design flaw according to LSP, and SRP
โ€ฃ
The following statement is often associated with LSP:
If it looks like a duck and quacks like a duck but it needs batteries, you probably have the wrong abstraction!

As such, the LSP requires a test standard that is more strict than the "is-a" test.

image
๐Ÿ’ก
Racing cars do not have cabins
โ€ฃ
How & why does the following code violate the Liskov Substitution Principle? And how would you fix it?
image
  • It breaks because getCabinWidth() is not implemented on Racing Cars (because racing cars don't have cabins). And, since class RacingCar is a subtype of Car, but breaks when an a Car instance breaks under, it fails under LSP.
  • Break the hierarchy - You implement a parent superclass that both classes will extend:
image
โ€ฃ
How do you fix code that violates the Liskov Substitution Principle?
  1. Break the hierarchy - Specifically, you could implement a parent superclass that the offending class and the original parent class inherit from
  2. Tell-don't-ask - Rather than asking an object for data and acting on that data, we should instead tell an object what to do. (e.g. if (product instanceof InHouseProduct) { product.applyExtraDiscount()} versus implementing the check and the "extra discount" in the discount() method on the class InhouseProduct)

https://martinfowler.com/bliki/TellDontAsk.html

โ€ฃ
How do you identify violations of the Liskov Substitution Principle?

You can try to replace an object class with an instance of it's subtype.

โ€ฃ
How & why does the following code violate the Liskov Substitution Principle? And how would you fix it?
image
  • This code fails to obey LSP because the behavior of the code changes depending on the (sub)-type of the product.
  • You could fix it by implementing the Ask-dont-tell principle via inverting class ProductUtils dependency on the product (sub)type by overriding the getDiscount() method on the class InHouseProduct and applying the extra discount there.
image
โ€ฃ
๐Ÿ“ฐ What is Interface Segregation Principle (ISP)?
No client should be forced to depend on methods it does not use
โ€ฃ
Suppose you're designing a system that will allow developers to interact with a collection of printers and scanners. And to do so, you define interface IMultiFunction to the system that will be implemented by class XeroxWorkCenter, class HPPrinterScanner, and class CanonPrinter like so:
image
image

What is wrong with this code? Why is this a problem?

  • This code violates the Interface Segregation Principle, because both the HP PrinterScanner class and the Canon Printer class' implementations depend on the IMultiFunction interface with methods they do not use.
  • It's a problem because if a developer (user for our system) comes and uses our system to fax, he or she could cause the system to break down by calling the fax() method on the HP PrinterScanner class or the Canon Printer class
โ€ฃ
Suppose you have designed an API that allows developers to interact with a collection of printers and scanners. And to do so, you define class XeroxWorkCenter, class HPPrinterScanner, and class CanonPrinter that implement the interface IMultiFunction like below.

Which design principle is this implementation violating? How would you fix it?

image
image
  • The ISP
  • You could fix it by splitting the big interface into smaller + higher cohesion interfaces, and the interfaces the printer/scanner classes implement.
  • Additionally, you could define a parent interface (e.g. interface Exportable) that can be extended with additional methods in interface IScan, interface IPrint , and interface IFax
image
image
โ€ฃ
How do you identify violations of the Interface Segregation Principle?
  1. Fat Interfaces
  2. Interfaces With Low Cohesion
  3. Empty Method Implementations
โ€ฃ
How do you fix violations of the Interface Segragation Principle?
  • Split the fat interface into leaner and higher cohesion interfaces. You could also define a parent interface that these leaner interfaces will extend.
โ€ฃ
What is Dependency Inversion Principle (DIP)?
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend upon details. Details (concrete implementations) should depend upon abstractions.

A Ecommerce web app

๐Ÿ’ก
High level modules and low level modules are relative
image
โ€ฃ
The following implements the ProductCatalog class and a SQLProductRepository for an eCommerce Web Application. What is wrong with the code? And how would you fix it?
image
  • The class ProductCatalog (the high level module) depends on class SQLProductRepository (the low level module) and thus violates the DIP
  • You can fix it by inverting the dependency, and refactoring the implementation of class ProductCatalog to depend on class ProductFactory , an abstraction
    • In this instance, you would want to define an interface ProductRepository that the class SQLProductRepository will implement.
    • ๐Ÿ’ก
      This means that both class ProductCatalog and class SQLProductRepository will now depend on the interface ProductRepository
    • Additionally, we would not want our class ProductCatalog to directly instantiate the SQLProductRepository isntance. Instead, we would want define a class ProductFactory which will return an instance of the SQLProductRepository class
    • ๐Ÿ’ก
      This means that the implementation of class ProductCatalog now depends on the abstraction class ProductFactory
      image
โ€ฃ
How do you identify violations of DIP? (High-level modules should not depend on low-level modules. Both should depend on abstractions.)
  • If a high level module is directly aware of the details of a low level class.
โ€ฃ
How do you fix violations of DIP?

Defining interfaces that both the high level and low level will depend on

Additionally defining classes that abstract implementation details of lower level modules and provide them to the higher level module via dependency injection.

โ€ฃ
The following implements a ProductCatalog class, a ProductRepository interface, and a SQLProductRepository that implements the Product repository interface for an eCommerce Web Application. What is wrong with the code? And how do we fix it?
image
  • As we can see, we use the factory method in order to provide an instance of the SQLProductRepository object.
๐Ÿ’ก
Which means that the concrete implementation details of class ProductCatalog depends on the abstraction class ProductFactory
โ—
Ideally, however, we don't want the ProductCatalog class to have to worry about when and how instances of classes that implement the ProductRepository interface should be created.
  • Dependency Injection: So, instead we can provide the instance of the class implementing the interface ProductRepository to the class ProductCatalog when it is being instantiated. As such we implement another class Main that will inject that dependency, and take care of instantiation logic.
image
โ€ฃ
What is dependency injection?

It is a method of applying the dependency inversion principle to classes by providing instances of dependencies.