Solid principles
The Single Responsibility Principle
A module should be responsible to one, and only one, actor, Uncle Bob says.
Suppose we use a class in two different features or screens etc. and the implementation of a method has to be different in these features. A quick fix that violates this principle and may cause big troubles in time is adding a flag parameter in the method and using it in two different actors.
Suppose we have a class called Logger.
classDiagram
class Logger
Logger: +log(message)
To see what these diagrams tell click here.
Let's say if the program is in debug mode we want to print the message, or if the program is in release mode we don't want to print the message but we want to send the message to a service. If we put a flag parameter called isDebug in log method for example, what will happen if we also want different behavior for different variants like testDebug, testRelease, productionDebug etc. The right way to prevent the complexity is:
classDiagram
class Logger
<<interface>> Logger
Logger: +log(message)
class Foo
Foo: -Logger logger
Foo --> Logger
class DebugLogger
DebugLogger --|> Logger
class ReleaseLogger
ReleaseLogger --|> Logger
Class Foo uses Logger interface. This way, we created different implementations(DebugLogger, ReleaseLogger) for different actors(debug mode, release mode).
The Open-Closed Principle
"A software artifact should be open for extension but closed for modification."
classDiagram
note for Domain "Highest level code/business rules"
class Domain
note for Presentation "Views, view logics etc."
class Presentation
note for Data "Database, network etc"
class Data
Data --> Domain
Presentation --> Domain
or as in the clean architecture book
classDiagram
note for Interactor "Highest level code/business rules"
class Interactor
note for Presenter "View logics etc."
class Presenter
class View
note for Data "Database, network etc."
class Data
Data --> Interactor
Presenter --> Interactor
View --> Presenter
One of the most important rules is dependency direction must be in one direction only. If class A uses class B, class B cannot know anything about class A.
The purpose of a program, business rules, they are the highest level code should be protected from changes but open for extension so that the changes that are being made in low level code like ui, database etc. won't affect these business rules. As you see in the graphs, other modules depend on domain(or interactor) module. This means that changes in other modules won't affect them.
The Liskov Substitution Principle
classDiagram
class Car
<<abstract>> Car
Car: +putGasoline(amount)
Car: +Int gasoline
class ElectricCar
ElectricCar: -Int chargeAmount
ElectricCar: +charge(amount)
ElectricCar --|> Car
The structure above forces the developer to type-check the object to determine if he or she can use the putGasoline method or not. Because if the Car object is an ElectricCar object, using putGasoline method may cause something wrong.
An implementation of a super type must not limit the usage of super type functionalities and must not require type checking so that the the developer can decide to use those functionalities or not.
The Interface Segregation Principle
classDiagram
class ImageEditor
ImageEditor: +compress(qualityPercentage) Bitmap
ImageEditor: +crop() Bitmap
ImageEditor --> ImageLibrary
class FeatureA
class FeatureB
FeatureA --> ImageEditor
FeatureB --> ImageEditor
Suppose we use an image processing library and created an ImageEditor class using the library. We use this class in more than one place in our program. In new versions of this library, changes in the internals will cause recompiling of the modules that depend on our class. To fix it:
classDiagram
class ImageCompressor
<<interface>> ImageCompressor
ImageCompressor: +compress(qualityPercentage)
class ImageCropper
<<interface>> ImageCropper
ImageCropper: +crop() Bitmap
class ImageEditor
ImageEditor --|> ImageCompressor
ImageEditor --|> ImageCropper
ImageEditor --> ImageLibrary
class FeatureA
class FeatureB
FeatureA --> ImageCompressor
FeatureB --> ImageCropper
Now, features depend on interfaces only and new changes in the internals of the library won't affect our features and won't cause recompilation.
The Dependency Inversion Principle
Dependency inversion is a technique that allows us to change dependency direction and actually we used it in previous principles. This principle promotes concrete classes use/depend on interfaces/abstract classes so that changes in implementation details won't affect the ones using them. Suppose we have 2 classes.
classDiagram
class A
class B
A --> B
class A uses class B. To apply dependency inversion:
classDiagram
class A
class B
class C
<<interface>> C
A --> C
B --|> C
This way, B now implements C interface. A uses C and doesn't depend on implementation details(B) anymore.