- SRP Example: Swiss Army Knife vs. Knife
What is cohesion?
Cohesion is the degreeto which the various parts of a software component are relatedof relation
- 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
class Square in the context of the SRPcalculateArea()andcalculatePerimeter()methods have high degree of cohesion / relationdraw()androtate()methods also have high degree of cohesion / relation
- However, #1 and #2 do not.
class Square to increase cohesion? class Square refactor to class Square and class SquareUIclass Square Responsibility: Measurements of squaresexport 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 squaresclass 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
}
}class Square and class SquareUI?class Square Responsibility: Measurements of squaresexport 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 squaresclass 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
}
}Coupling
Coupling is defined as the level of interdependency between various software components
- Standard Gauge Rail (
1.4 m wide) vs Broad Gauge Rail (1.6 m wide) - The trains are tightly coupled to their track
class Student in the context of the SRP?- Tightly coupled with the database layer
- Student class should not be cognizant of the low level details related to dealing with the database
class Student to decrease coupling? What would this refactor allow?- Tightly coupled with the database layer
- Student class should not be cognizant of the low level details related to dealing with the database
- 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
class Student and class StudentRepository?class Student has the single responsibility of handling core student related dataclass 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 has the single responsibility of handling database operationsAn alternative interpretation
Every software component should have one and only one reason to change.
- Ask yourself, how many reasons a piece of code has to change, and make refactors based on that
- Refactor, and aim for high cohesion, and low coupling.
- Change to student id format
- Change in the student name format
- A change to the database
Software components should be closed for modification but open for extension
- New features getting added to the software component, should NOT have to modify existing code.
- A software component should be extendable to add a new feature or to add a new behavior to it.
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
}
}- You could implement another method that will make the same calculation, but for the
class VehicleInsuranceCustomerProfile - You could define an interface
interface CustomerProfilethat both our customer profile classes could implement, and change the method signature to reflect the newly defined interface.
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
- 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.
- You could end up with too many classes and overly complicated design.
- When you need to modify and or repeat existing code to accommodate additional features that have similar functionality.
- Interfaces! Usually helps improve reusability of code for different kinds/types of input.
Objects should be replaceable with their subtypes without affecting the correctness of the program
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.
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.
- It breaks because
getCabinWidth()is not implemented on Racing Cars (because racing cars don't have cabins). And, sinceclass RacingCaris a subtype ofCar, but breaks when an aCarinstance breaks under, it fails under LSP. - Break the hierarchy - You implement a parent superclass that both classes will extend:
- Break the hierarchy - Specifically, you could implement a parent superclass that the offending class and the original parent class inherit from
- 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 thediscount()method on theclass InhouseProduct)
You can try to replace an object class with an instance of it's subtype.
- 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-tellprinciple via invertingclass ProductUtils dependency on the product (sub)type by overriding thegetDiscount()method on theclass InHouseProductand applying the extra discount there.
No client should be forced to depend on methods it does not use
interface IMultiFunction to the system that will be implemented by class XeroxWorkCenter, class HPPrinterScanner, and class CanonPrinter like so: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
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?
- 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 ininterface IScan,interface IPrint, andinterface IFax
- Fat Interfaces
- Interfaces With Low Cohesion
- Empty Method Implementations
- Split the fat interface into leaner and higher cohesion interfaces. You could also define a parent interface that these leaner interfaces will extend.
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
- The
class ProductCatalog(the high level module) depends onclass SQLProductRepository(the low level module) and thus violates the DIP - You can fix it by inverting the dependency, and refactoring the implementation of
class ProductCatalogto depend onclass ProductFactory, an abstraction - In this instance, you would want to define an
interface ProductRepositorythat theclass SQLProductRepositorywill implement. - Additionally, we would not want our
class ProductCatalogto directly instantiate theSQLProductRepositoryisntance. Instead, we would want define aclass ProductFactorywhich will return an instance of the SQLProductRepository class
class ProductCatalog and class SQLProductRepository will now depend on the interface ProductRepositoryclass ProductCatalog now depends on the abstraction class ProductFactory- If a high level module is directly aware of the details of a low level class.
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.
- As we can see, we use the factory method in order to provide an instance of the SQLProductRepository object.
class ProductCatalog depends on the abstraction class ProductFactory- Dependency Injection: So, instead we can provide the instance of the class implementing the
interface ProductRepositoryto theclass ProductCatalogwhen it is being instantiated. As such we implement another classMainthat will inject that dependency, and take care of instantiation logic.
It is a method of applying the dependency inversion principle to classes by providing instances of dependencies.