Acronym for 5 principles intended to make software designs more understandable, flexible and maintainable.
As with any other principle in life, every SOLID principle can be misused or overused to the extent of being counterproductive
S | Single Responsibility Principle | A class should have one, and only one, reason to change |
O | Open Closed Principle | You should be able to extend a classes behavior, without modifying it |
L | Liskov Substitution Principle | Derived classes must be substitutable for their base classes |
I | Interface Segregation Principle | Make fine grained interfaces that are client specific |
D | Dependency Inversion Principle | Depend on abstractions, not on concretions |
Lets examine them one by one
In other words, SRP encourages you to divide and group similar functionality into different classes
class Canvas {
val contents[][]
val height, width
private def inBounds(x,y)
def drawPointAt(x,y, char) = {
if (inBounds) contents[x][y] = char
}
def drawLine(x1,y1, x2, y2) = { only supports horizontal line }
def toString = contents.map(_.mkString).mkString
}
class Canvas {
...
def saveToFile(filepath) = { serialize somehow to file }
}
SRP violation: Canvas now has 2 unrelated reasons to change
Solve the violation by separating saving to file to its own class
class CanvasFileSaver {
def saveToFile(canvas) = ...
}
Careful of interpreting this as "class should have one job only"
You might end up with a simple functionality divided between 5 classes each with a 4 liner function
"one reason to change" should be a business logic related reason or an important technical one
originaly stated as:
Software entities ... should be open for extension, but closed for modificationBut later phrased in a better way by Uncle Bob as:
You should be able to extend a classes behavior, without modifying it
The idea behind the Open Closed Principle is to ensure that the classes have scope for extension in the future
and that this extension will not break existing code.
New features are added by extending or writing new code without modifying old code.
Old code remains unchanged, tested and this reliable.
Example
We have a TicketPrice class, which holds a price for a ticket.
The price can be discounted according to the customer type.
A PriceCalculator class performs this discount check.
Implementation might initially look like this:
case class TicketPrice(price)
class PriceCalculator {
def getDiscountedPrice(ticketPrice) = {
if(customer == 'vip') {
return ticketPrice.price * 0.25;
}
else if(customer == 'family') {
return ticketPrice.price * 0.5;
}
else return ticketPrice.price;
}
}
Adding customer types or changing the behavior of an existing type would break the existing tests for this code
This is where the Open Closed Principle can help
Lets refactor according to OCP.
class PriceCalculator {
def getDiscountedPrice(ticketPrice) = {
return ticketPrice.getDiscountedPrice();
}
}
Discount calculation according to customer types go in their own classes:
class TicketPrice {
def getDiscountedPrice() = price
}
class VIPDiscount extends TicketPrice {
def getDiscountedPrice() = {
return price * 0.25;
}
}
class FamilyDiscount extends TicketPrice {
def getDiscountedPrice() = {
return price * 0.5;
}
}
In this way, we can continue adding more discount types without breaking the core PriceCalculator behavior
In other words, PriceCalculator is
The examples for the Liskov Substitution and Interface Segregation principles might look like these principles
were made only for staticaly typed languages.
But actually the principles can also be applied to dynamic langs and also on higher levels (architecture, system etc)
Example
class Bird{
def fly() = { fly }
}
class Eagle extends Bird {
}
class Ostrich extends Bird {
override def fly() = { throw new Exception("cant fly") }
}
Ostrich is a bird, but it cant fly!
So if you had to iterate over birds and make them fly:
for (bird: birds) {
if (bird instanceOf Ostrich) {
//noop - to avoid an error being thrown
} else bird.fly()
}
this invalidates the Liskov Substitution Principle because an Ostrich cant be a (safe) substitute for bird - it throws exception on fly
refactor according to LSP to
class Bird {
def makeSound() // only fns that all birds have!
}
class FlyingBirds extends Bird {
def fly() = { fly... }
}
class Eagle extends FlyingBirds {}
class Ostrich extends Bird {}
for (bird: birds) bird.makeSound()
for (bird: flyingBirds) bird.fly() //safe now
Benefits of LSP are Maintainability & Reliability:
If you find yourself writing instanceOf or overriding your own base class functions to do nothing, you should do something about it.
Don't try to forsee all future extensions of a base class.
It's ok to start with something and later refactor it
YAGNI approach.
Make fine grained interfaces that are client specific
Interface Segregation Principle is about avoiding "Fat" interfaces
This usually happens gradually. Starting from a small interface with a simple implementation that slowly grows
Lets look at an example
We start with a simple Worker interface that can work and eat:
interface Worker {
def work()
def eat()
}
class NormalWorker implements Worker {
def work() = { ... }
def eat() = { ... }
}
class SlowWorker implements Worker { same but slower }
A new requirement for a Robot worker arrives.
A robot doesn't eat
class RobotWorker implements Worker {
def work() = { /* robot work */ }
def eat() = { /* do nothing */ }
}
This violates ISP
Our common interface Worker has now become "Fat" and contains methods that some of its implementations don't need
The solution is simple:
Make fine grained interfaces that are client specificSeparate the big interface to smaller ones:
interface Workable { def work() }
interface Feedable { def eat() }
class NormalWorker implements Workable, Feedable {
...
}
class Robot implements Workable {
...
}
A nice saying is that Interface Segregation and Liskov Substitution are Two sides of the same coin
ISP cares about "client" side - keeping clients from implementing your interfaces in an unsafe way (no-op or throw exceptions).
LSP is about the "server" side - keeping you from from writing potentially dangerous code (runtime errs)
The Dependency-Inversion Principle consists of two rules:
What are high and low levels?
Benefits OR "Why avoid high-level modules depending on low-level?"
Dependency Inversion Principle (DIP) is often confused with:
IoC is just inversing the control, the opposite of IoC is control!
Dependency Injection is just a combination IoC and DIP to manage dependencies
but wait, theres some more