DEVELOPING SOLID

APPLICATIONS WITH Scala

Created by Juan Manuel Torres 

FOLLOW ALLONG:

2023

Born and raised in Bogotá Colombia

Juan Manuel Torres

Software Engineer

MS Computer Science SDSU, 15+ years of experience 

Learned to Program in C OOP in C++

Rober C. Martin

Barbara Liskov

Gang of Four

Bertrand Meyer

Quick Survey

  • Who is Familiar with OOP?
  • Who uses OOP at work?
  • Who is Familiar with Design Patterns?
  • Who is familiar with SOLID principles?
  • Who makes an effort to use SOLID on a daily basis?

 

What is S.O.L.I.D?

is a mnemonic acronym that stands for five basic principles of object-oriented programming and design. The principles, when applied together, intend to make it more likely that a programmer will create a system that is easy to maintain and extend over time. [3]

According to Wikipedia SOLID:

What is S.O.L.I.D?

S

 

 

 

 

 

 

 

 

O

 

 

 

 

 

 

 

 

L

 

 

 

 

 

 

 

 

I

 

 

 

 

 

 

 

 

D

 

 

 

 

 

 

 

 

Single

Responsibility

High level Architecture

Open Closed

 

Design and extension 

Liskov

Substitution

Correctness

Interface

Segregation

Thin interfaces

Dependency

Inversion

Level of Abstraction

S

Single Responsibility Principle

O

R

P

Just because you can, doesn't mean you should!

 

L

D

High Level Architecture

I

S

O

R

P

A class or module should have one, and only one, reason to change... Classes should have one responsibility—one reason to change. [4]

 

L

D

Single Responsibility Principle

High Level Architecture

I

While SRP is one of the simplest OO design principles, it is one of the most abused.

Once the program works, I'm done!

Use SRP to neatly organize your application and code

________________________________________________________

S

O

R

P

L

D

High Level Architecture

I

WHY? 

Why do we want to use SRP?

Easier to understand and update. Multipurpose classes prevents us from clearly understanding the original intention of the code.

S

O

R

P

L

D

High Level Architecture

I

Why do we want to use SRP?

Better system design. Collaboration with other small classes to achieve system behavior is better than doing it all in one or few classes.

S

O

R

P

L

D

High Level Architecture

I

Example 1

  trait Modem[F[_]] {
    def dial(phone: Phone): F[Unit]
    def hangUp(): F[Unit]
    def send(message: Message): F[Unit]
    def receive(): F[String]
  }

Single or multiple responsibilities?

S

O

R

P

L

D

High Level Architecture

Connection

Communication

I

  class Version {
    def getMajorVersionNumber: String = ???
    def getMinorVersionNumber: String = ???
    def getPatchVersionNumber: String = ???
    def buildNumber: String = ???
  }

Single or multiple responsibilities?

S

O

R

P

L

D

High Level Architecture

I

Example 2

O

Open Close Principle

L

C

P

D

S

Open heart surgery is not required when putting on a coat!

Design & Extension

I

O

Open Close Principle

L

Classes, modules, functions should be open for extension and close for modification.

Open for Extension:

This means that the behavior of the module can be extended. [5]

Closed for Modification:

The source code of such a module is inviolate. No one is allowed to make source code changes to it. [5]

C

P

D

S

Design & Extension

I

  class Pilot {
    def operate(vehicle: Vehicle) = {
      if(vehicle.isInstanceOf[Car]) {
        // Drive Car
      } else if (vehicle.isInstanceOf[Plane]) {
        // Fly Plane
      }
    }
  }
  trait Vehicle

  class Car extends Vehicle {
    def getType(): String = "car"
  }

Does not conform to the open-closed principle because we would have to modify Pilot when new vehicles are introduced, e.g. a Boat or a Rocket.

O

L

C

P

D

S

Design & Extension

  class Plane extends Vehicle {
    def getType: String = "plane"
  }

Violation Example #1 

I


trait Vehicle[F[_]] {
  def getType: String
  def operateVehicle: F[Unit]
}

class Plane[F[_]] extends Vehicle[F] {
  def getType: String = "plane"
  def operateVehicle: F[Unit] = ??? // Implement Drive Vehicle
}
class Car[F[_]] extends Vehicle[F] {

  def getType(): String = "car"
  def operateVehicle: F[Unit] = ??? // Implement Drive Vehicle
}

Use the Strategy Design Pattern and other design patterns to extend code without internal modifications!

AKA Polymorphism

O

C

P

S

Design & Extension

L

I

D

val pilot = new Pilot[IO]()
val car = new Car[IO]()
pilot.operate(car)
  
val plane = new Plane[IO]()
pilot.operate(plane)

Pilot can conduct any type of vehicle without having to modify the pilot code.

O

C

P

S

Design & Extension


class Pilot[F[_]] {
  def operate(vehicle: Vehicle[F]): F[Unit] = {
    vehicle.operateVehicle
  }
}

L

I

D

O

C

P

S

Design & Extension

L

I

D

What Functional Design Pattern

we can use to achieve Polymorphism?

L

S

D

S

P

O

If it looks like a duck

Quacks like a duck

But needs batteries

You probably have the wrong abstraction

Liskov Substitution Principle

Correctness

I

L

Liskov Substitution Principle

S

If for each object o1  of type S there is an object o2  of type T such that for all programs P   defined in terms of T, the behavior of P is unchanged when o1  is substituted for o2,  then S is a subtype of T. [9]

S

P

O

D

Correctness

I

L

S

D

S

P

O

o2 (T)

o1 (S)

T

S

P

If for each object o1  

there is an object o2 

such that for all programs P   defined in terms of T, 

the behavior of P is unchanged when o1  is substituted for o2,  

then S is a subtype of T.

of type S 

of type T 

Subtypes must be substitutable for their base types.

Child classes should never break the parent class' type definitions [8]

Correctness

I

L

S

S

P

O

D

Violation Example #1 


trait Bird[F[_]] {
  def birdSound: F[Unit]
  def fly(): F[Unit]
}
class Duck extends Bird[IO] {
  override def birdSound: IO[Unit] = {
    IO.println("Quack!")
  }
  override def fly(): IO[Unit] = {
    IO.println("The duck is flying!")
  }
}
class Ostrich extends Bird[IO] {
  override def birdSound: IO[Unit] = {
    IO.println("Ostrich noises!")
  }
  override def fly(): IO[Unit] = {
    IO.println("D'oh!")
  }
}

Introduces new behavior,

NON-FLYING BIRD!!!

Correctness

I

L

S

S

P

O

D

Violation Example #2

class Ostrich extends Bird[IO] {
  override def birdSound: IO[Unit] = {
    IO.println("Ostrich noises!")
  }
  override def fly(jetPack: JetPack[IO]): IO[Unit] = {
    IO.println("Attach JetPack and FLY!!!")
  }
}

class BirdSimulator(bird: Bird[IO]) {
  def main: IO[Unit] =  {
    bird.birdSound >> bird.fly()
  }
}
class Ostrich needs to be abstract. Missing implementation for:
  def fly(): 

What error are we going to get here?

Correctness

I

val birdSim1 = new BirdSimulator(new Duck)
birdSim1.main

val birdSim2 = new BirdSimulator(new Ostrich)
birdSim2.main

L

S

S

P

O

D

So what is the point?

The Liskov Substitution Principle (A.K.A Design by Contract) is an important feature of all programs that conform to the Open-Closed principle.

It is only when derived types are completely substitutable for their base types that functions which use those base types can be reused with impunity, and the derived types can be changed with impunity. [16] 

Correctness

I

I

S

L

S

P

O

D

Interface Segregation Principle

Thin interfaces

You want me to plug this in where?

I

S

L

S

P

O

D

The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use. [11]

Interface Segregation Principle

Classes that implement interfaces should not be forced to implement methods they do not use. [12]

Thin interfaces

I

S

L

S

P

O

D

This principle deals with disadvantages of fat interfaces


trait Vehicle[F[_]] {
  def accelerate(): F[Unit]
  def brake(): F[Unit]
  def changeGear(gear: GearBox): F[Unit]
  def ejectCD(): F[Unit]
  def getType(): String
  def lightsOn(): F[Unit]
  def operateVehicle(): F[Unit]
  def signalLeft(): F[Unit]
  def signalRight(): F[Unit]
  def startEngine(): F[Unit]
  def stopRadio(): F[Unit]
} 

Thin interfaces

If you put too many things in your vehicle you are going to leave your donkey up in the air!

I

S

L

S

P

O

D

Each interface should be grouped into methods that serve a different set of clients


trait CDRadio[F[_]] {
  def ejectCD(): F[Unit]
  def stopRadio(): F[Unit]
}

trait SpeedControl[F[_]] {
  def accelerate(): F[Unit]
  def brake(): F[Unit]
  def changeGear(gear: GearBox): F[Unit]
  def startEngine(): F[Unit]
}

class Car extends Vehicle[IO] {
  override def operateVehicle(): IO[Unit] = {
    val speedControl = new CarSpeedControl[IO]()
    speedControl.startEngine()
    speedControl.accelerate()
  }
}

Thin interfaces

D

S

L

I

P

O

I

Dependency Inversion Principle

Level of Abstraction

Would you solder a lamp directly to the electrical wiring in a wall? 

D

S

L

I

P

O

I

Dependency Inversion Principle

A. High level modules should not depend upon low level modules. Both should depend upon abstractions. [18]

 

B. Abstractions should not depend upon details. Details should depend upon abstractions. [18]

Level of Abstraction

D

S

L

I

P

O

I

Level of Abstraction

Dependency Inversion Principle VS Dependency Injection

Dependency Inversion: is about programming to interfaces NOT implementations. 

Dependency Injection: Is the means by which an object acquires a dependency. [13]

Dependency Inversion can be achieved WITH or WITHOUT Dependency Injection.

D

S

L

I

P

O

I

Level of Abstraction


class Car extends Vehicle[IO] {
  override def operateVehicle(): IO[Unit] = {
    val speedControl = new CarSpeedControl[IO]()
    speedControl.startEngine()
    speedControl.accelerate()
    // ...
  }
}

SRP

ISP

 OCP 

Broke Vehicle interface into single purpose interfaces

The class depends on a specific implementation of SpeedControl

D

S

L

I

P

O

I

Level of Abstraction

CarSpeedControl

Depends on

Car

SpeedControl Interface

Depends on an interface

Car

CarSpeedControl

TurboSpeedControl

Classes "communicate" through an abstraction. Classes don't need to know about each other!

TestSpeedControl

LSP

D

S

L

I

P

O

I

Level of Abstraction




class Car(speedControl: SpeedControl[IO]) extends Vehicle[IO] {
  override def operateVehicle(): IO[Unit] = {
    speedControl.startEngine()
    speedControl.accelerate()
  }
}
val myCar = new Car(new CarSpeedControl())
val turboCar = new Car(new TurboSpeedControl())

D

S

L

I

P

O

I

Level of Abstraction

Guidelines

  1. No Concrete Classes
  2. No class should derive from concrete classes
  3. No method overwriting

Conclusion 

  1. Use SRP to keep your code clean and organized
  2. Use OCP to enable you and others to extend your code
  3. Use LSP to ensure your classes can be reused without side-effects
  4. Use ISP to prevent forcing subtypes to implement methods they don't use
  5. Use the DIP to create more modular code

The Road to S.O.L.I.D.

  1. S.O.L.I.D. Requires repetition
  2. Be disciplined about repetition
  3. Use design patterns to solve problems
  4. Use libraries, components and frameworks to help you out
  5. Do not be discouraged
  6. Do not over do it
    1. You can always find ways to abstract your code
    2. Do enough to have a good design
    3. Do enough to cover your use cases
  7. Refactoring is a part of life APPLY S.O.L.I.D. WHILE REFACTORING

Questions?

THE END

BY Juan Manuel Torres / @onema

REFERENCES

Developing SOLID applications with Scala

By Juan Manuel Torres

Developing SOLID applications with Scala

  • 104