SOLID Design Principles#
Single-Responsibility Principle#
There should never be more than one reason for a class to change.
Reduces complexity of a single class
Reduces refactoring
Pragmatics#
Break down classes once they’re too big to handle
A thing doing an action can have its action being done to the thing.
@startuml class Employee { + name + print_department() + print_tax_info() } @enduml
@startuml class Employee { + name + print_department() } class TaxReport { ... print(employee: Employee) } Employee <- TaxReport @enduml
Open/Close Principle#
Classes should be open for extension but closed for modification.
Reduces risk of breaking a well-tested class when adding new features
Pragmatics#
Add a new feature to a class by subclassing the common feature
@startuml class Food { foods: array[tuple[string, float]] get_calories() } note left of Food::get_calories for food_name, weight in foods: if (food_name == "apple") {return weight * 30} else if (food_name == "burger") {return weight * 300 } else ... end note @enduml
Adding more types to the above would require modifying the
Workout
class. Imagine even more methods that requires a check toWorkout::type
, we would also need to modify those.@startuml Meal o-right-- Food Food <|-- Apple Food <|-- Burger class Meal { foods: array[Food] get_calories() } note left of Meal::get_calories return sum(food.calories() for food in self.foods) end note abstract Food { {static} calories_per_weight weight get_calories() } class Apple { } class Burger { } @enduml
Liskov Substitution Principle#
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it
Basically, any subclass must be compatible with all methods that takes in its superclass. Specifically this principle has the following rules:
Overridden method types should match or more abstract
Overridden return types should match or be a subtype
Overridden methods exceptions should not throw newer ones
Overridden methods pre-conditions shouldn’t be stronger
Overridden methods post-conditions shouldn’t be weaker
Invariants must be preserved.
Don’t override private fields
Take this motivating example where we the following class design:
@startuml
Rectangle <|-- Square
class Rectangle {
width
height
set_width(val)
set_height(val)
}
class Square {
set_width(val)
set_height(val)
}
note right of Square::set_width
self.width = self.height = val
end note
note right of Square::set_height
set_width(val)
end note
@enduml
Say we have a function change_aspect_ratio(rectangle, width, height)
that changes the aspect ratio by keep the area of a Rectangle
. The issue is this method is not compatible with a Square. This examples violates (6) that mutating the width and height does not affect each other.
Interface Segregation Principle#
Many client-specific interfaces are better than one general-purpose interface
A general-purpose interface can have a lot more methods to be implemented than the concrete class needs causing a lot of unimplemented functions.
Pragmatics#
Break down interfaces into many interfaces
Break down routes and endpoints
This is a double edge sword since more interfaces means more complexity
Dependency Inverstion Principle#
Depend upon abstractions, [not] concretions
Pragmatics#
Design and/or implement high-level classes first before low-level classes.
Aligns with the direction of test-driven development.