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
- No Concrete Classes
- No class should derive from concrete classes
- No method overwriting
Conclusion
- Use SRP to keep your code clean and organized
- Use OCP to enable you and others to extend your code
- Use LSP to ensure your classes can be reused without side-effects
- Use ISP to prevent forcing subtypes to implement methods they don't use
- Use the DIP to create more modular code
The Road to S.O.L.I.D.
- S.O.L.I.D. Requires repetition
- Be disciplined about repetition
- Use design patterns to solve problems
- Use libraries, components and frameworks to help you out
- Do not be discouraged
- Do not over do it
- You can always find ways to abstract your code
- Do enough to have a good design
- Do enough to cover your use cases
- Refactoring is a part of life APPLY S.O.L.I.D. WHILE REFACTORING
Questions?
THE END
BY Juan Manuel Torres / @onema
REFERENCES
- Object-oriented programming
- Object Oriented PHP for Beginners
- SOLID
- Clean Code
- The Open-Closed Principle
- Object Oriented Programming Principles (Abstraction)
- Object Oriented Programming Principles (Encapsulation)
- Liskov Substitution & Interface Segregation Principles
- Data abstraction and Hierarchy
- Understanding and Applying Polymorphism in PHP
- Interface Segregation Principle
- The I in SOLID
- From STUPID to SOLID code
- The Single Responsibility Principle
- The Open-Closed Principle
- Liskov Substitution Principle
- The Interface Segregation Principle
-
The Dependency Inversion Principle
Developing SOLID applications with Scala
By Juan Manuel Torres
Developing SOLID applications with Scala
- 115