SOLID Principles

https://github.com/samzilverberg/solid-principles-workshop

SOLID what?

Acronym for 5 principles intended to make software designs more understandable, flexible and maintainable.

Origins

  • introduced in 2000 by Robert C. Martin, aka Uncle Bob, in his paper "Design Principles and Design Patterns".
  • The SOLID acronym was coined later by Michael Feathers

Some reasons to learn this

  • You've probably been practicing some (if not all) of these principles instinctively or unconsciously.
    It's good to "formalize" this and be conscious about it.
  • Gives extra points to think about in code review (or refactor)
  • Can be applied to all levels of software, from code to architecture.
  • Job interviews: some universities teach this and some workplaces expect you to know this (my uni didn't btw)

!important: Principles, not Rules

As with any other principle in life, every SOLID principle can be misused or overused to the extent of being counterproductive

The 5 Principles

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

Single Responsibility Principle

A class should have one, and only one, reason to change
  • Keep classes focused on one area of functionality
  • If a new area is needed, put it in another class

In other words, SRP encourages you to divide and group similar functionality into different classes

Benefits of SRP

  • Readability - Classes that do many things are hard to follow.
  • Testability – Breaking down your code into small modules makes them easier to test.
  • Maintainability – change to an area of responsibility will likely only occur in the class responsible for it.
example of class evolving and violating SRP
Requirements:
Canvas with width, height and can draw a char on it:
					
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
}
					
				
New requirement of saving canvas to file might result in this implementation:
					
class Canvas {
	... 
	def saveToFile(filepath) = { serialize somehow to file }
}
					
				

SRP violation: Canvas now has 2 unrelated reasons to change

  1. Canvas model changes: if drawLine should also support vertical
  2. Save to file changes: if file should be saved in a different way

Solve the violation by separating saving to file to its own class

					
class CanvasFileSaver {
	def saveToFile(canvas) = ...
} 
					
				

Caution!

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

Open Closed Principle

originaly stated as:

Software entities ... should be open for extension, but closed for modification

But 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.

Benefits of OCP

  • Reliability – Existing code is closed to modification. adding new features will not require changing old code. So existing code stays tested, unchanged and thus reliable.
  • Agility – Code is open to adding new features via extension. making the development process fast and agile as less (or no) refactoring of old code is required to add new features.

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

  • Open for extension by adding new types of discount in new classes.
  • Closed for modification because discount changes are not done in the original PriceCalculator code.

Disclaimer for next 2 principles

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)

Liskov Substitution Principle

Derived classes must be substitutable for their base classes

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:

  • When extending base classes, you don't need to worry about overriding (or forgetting to) a fn of a base class to be a no-op (or throw exception)
  • When using a collection of the base class, you don't need to check instanceOf and can assume the base methods are safe

Rule of thumb

If you find yourself writing instanceOf or overriding your own base class functions to do nothing, you should do something about it.

Caution

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.

Interface Segregation Principle

no client should be forced to depend on methods it does not use
also sometimes stated as

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 specific

Separate 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)

Dependency Inversion Principle

Depend on abstractions, not on concretions

The Dependency-Inversion Principle consists of two rules:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend upon details. Details should depend upon abstractions.

What are high and low levels?

  1. Low-level - concrete implementation including the small details such as connection protocl, file I/O, storage etc
  2. High-level - business rules and the bigger picture

Example of dependency inversion

Benefits OR "Why avoid high-level modules depending on low-level?"

  • When we make a change in a low-level module, we want to prevent that we also need to make changes a high-level module.
    We want to minimize the "surface of change".
  • High-level modules (biz rules) are easier to reuse when they aren't tightly coupled to low-level modules.

Dependency Inversion Principle (DIP) is often confused with:

  • Inversion of Control (IoC)
  • Dependency Injection (DI)

IoC is just inversing the control, the opposite of IoC is control!

Strategy Pattern is a nice example because code usign it hands over control to the strategy.

Dependency Injection is just a combination IoC and DIP to manage dependencies


IoC: Instead of classes instantiating their dependencies, they give control to a DI framework.

DIP: classes depend on an abstraction, the DI framework knows the concrete (low-level) instance needed.
Thanks, questions? ...

but wait, theres some more

fun with SOLID kata

https://github.com/emilybache/Racing-Car-Katas