SOLID
The SOLID principles is a set of good practices that focus on flexibility and maintainability. They apply especially well on OOP.
In a nutshell
- Single responsibility – Class has one job to do. Each change in requirements can be done by changing just one class.
- Open–closed – Class is open to be used by others, but closed to be changed by others.
- Liskov substitution – Class can be replaced by any of its children. Children classes inherit parent's behaviour.
- Interface segregation – When classes promise each other something, they should separate these promises (interfaces) into many small promises, so it's easier to understand.
- Dependency inversion – When classes talk to each other in a very specific way, they both depend on each other to never change. Instead classes should use promises (interfaces, parents), so classes can change as long as they keep the promise.
Principles
Single Responsibility
There should never be more than one reason for a class to change.
Don't put methods in class that has nothing to do with the class.
# BAD
class User:
init(name, email)
sendEmail(message): …
# GOOD
class User:
init(name, email)
class EmailHandler:
sendEmail(user, message): …
User
class shouldn’t have the responsibility to sendEmail
Open-closed
Software entities should be open for extension, but closed for modification.
- Class should be generic
- Don’t directly modify a class: implement extensions via abstraction / polymorphism.
# BAD
class AreaCalculator:
init(shapes)
sum():
foreach (shape in shapes):
if (shape is "square"): …
elif (shape is "circle"): …
return array_sum(areas)
# GOOD
class Square:
init(length)
area(): …
class Circle:
init(radius)
area(): …
class AreaCalculator:
init(shape, area)
sum():
foreach(shape in shapes):
area[] = shape.area()
return array_sum(areas)
If we were to add a new shape, we would need to modify the AreaCalculator
each time. Instead of calculating the area for each shape in the calculator, let each shape provide its own area.
Liskov Substitution
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
- When inherited class overrides behavior of parent's method, there is probably an LSP violation.
- When this happen, create a more generic parent class or interface
# BAD
class Rectangle:
init(width, height)
setWidth(w): width = w
setHeight(h): height = h
area() = width * height
class Square < Rectangle:
# override
setWidth(w): width = w, height = w
setHeight(h): width = h, height = h
# GOOD
interface Shape:
area()
class Rectangle < Shape:
init(width, height)
area(): return width * height
class Square < Shape:
init(side)
area(): return side * 2
See: Design by contract
Interface Segregation
Clients should not be forced to depend upon interfaces that they do not use.
Chose many specific interfaces rather than one general interface
# BAD
interface Animal:
run()
fly()
class Bird:
move():
animal.fly
# GOOD
interface GroundAnimal:
run()
interface SkyAnimal:
fly()
Bird
class never use run()
, so it should be split into other specifics abstract class / interfaces.
Dependency Inversion
Depend upon abstractions, not concretions.
High-level modules (e.g. business logic) shouldn't depend on low-level modules (e.g. database operations). Both should depend from abstraction (e.g. interfaces).
# BAD
class EventTracker:
init(logger): logger = ConsoleLogger.new
# GOOD
class EventTracker:
init(logger)
You can specify the logger when you instantiate the class, so you don't depend of a specific logger.