Often we are talking about some principles that can improve our code. These principles are also known as SOLID. During the past few discussions, I heard a lot of different explanations (correct and not) for the same things.

In this article, I just would like to cover each principle in detail and provide a short and easy-to-understand explanation.

This is an important point. Principles will not turn a bad programmer into a good programmer. Principles have to be applied with judgment. If they are applied by rote it is just as bad as if they are not applied at all. (Uncle Bob)

the history

At the end of 1980 Robert C. Martin started his discussion in USENET related to best principles of programming. During the decade these principles have a lot of changes and finally at the beginning of 2000 SOLID was created. Name SOLID was proposed by Michael Feathers in 2004. This is how SOLID was created.

  • SRP - Single Responsibility Principle
  • OCP - Open-Closed Principle
  • LSP - Liskov Substitution Principle
  • ISP - Interface Segregation Principle
  • DIP - Dependency Inversion Principle

It’s good to understand, what is a principle. A good explanation available on CleanCode and written by Uncle Bob:

The SOLID principles are not rules. They are not laws. They are not perfect truths. They are statements on the order of “An apple a day keeps the doctor away.” This is a good principle, it is good advice, but it’s not pure truth, nor is it a rule.

What do I mean by “Principle” by Uncle Bob

SRP - Single Responsibility Principle

Many developers think, that according to the name, this principle means that every module should be responsible only for something single. And indeed, such a principle correct and exists, but it’s hasn’t any relation to SOLID principles and so does not describe the SRP principle.

The real means of the SRP -

The module should be responsible only for one and just one actor.

where module - is a set of connected functions and data structures. And actor - is a group (from one or a few) of people who wants change. This means, that each software module should have one and only one reason to change

Another wording for the Single Responsibility Principle is:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

SRP

example

One of the main indicators of SRP violation is when a few actors require a change of the same module. If u can see a risk that different developers can start code editing related to the same entity at the same time or/and can make a code duplication then it’s probably an SRP violation.

Here is an example.

class Transport {

  func performMaintenance() {
    // maintanance can be done by separate command of specialists
  }

  func drive() {
    // drive can be done within group of drivers
  }
}

Here we can see, that 2 functions require different actors - one is probably a maintenance team and another - a driver.

To solve this, Uncle Bob proposes a lot of variants, and all of them are related to the process of function separation in different classes.

One of the possible solutions can be done using the Facade pattern:

Facade is a structural design pattern that provides a simplified (but limited) interface to a complex system of classes, library, or framework.

struct TransportData {
	// some data about transport
}

class MaintenanceTeam {
  func performMaintenance(_ object: TransportData) {
    // maintanance can be done by command of specialists
  }
}

class Driver {
  func drive(_ transport: TransportData) {
    // drive can be done within group of drivers
  }
}

protocol TransportControllable {

  func drive()
  func maintenance()
}

class Train: TransportControllable {

  private let data: TransportData
  private let driver: Driver
  private let maintenanceTeam: MaintenanceTeam

  init(
	data: TransportData,
	driver: Driver,
	maintenanceTeam: MaintenanceTeam
  ) {
    self.data = data
    self.driver = driver
    self.maintenanceTeam = maintenanceTeam
  }

  func drive() {
    driver.drive(data)
  }

  func maintenance() {
    maintenanceTeam.performMaintenance(data)
  }
}

OCP - Open-Closed Principle

This principle is known as the most important one. It originated from the work of Bertrand Meyer and created in 1988:

We should write our modules so that they can be extended, without requiring them to be modified. In other words, we want to be able to change what the modules do, without changing the source code of the modules.

In other words, u’r code should be:

  • Open for Extension: U must keep u’r code in a state, that requires minimum efforts for any change. I do believe, that if u think about u’r code (some module) and u feel that u don’t want to modify it - this is it - u’r code is dirty and required additional attention and effort.
  • Closed for modification: Extending the class should not change the existing implementation.

This can be achieved by using some techniques such as Dynamic and Static polymorphism, for example:

  • Inheritance
  • Abstraction and composition
  • Generics

This principle makes our code more maintainable and extendable. Also, u’r code becomes more stable when a new change is introduced because it’s divided into separate let’s say pats, that protects lower levels of functionality in a hierarchy.

I guess this is why this principle is known as “the most important one”.

example

Here is a quick example of it:

enum DriveMode {

  case basic
}

class Auto {

  func turnEngineOn() {

  }

  func addFuel() {

  }

  func drive(_ mode: DriveMode) {

  }
}

Now, we should somehow add new functionality but do not change the codebase. To do this we can use inheritance or extract interface and conform to it by specifically created objects:

protocol Car {
  func turnEngineOn()
  func addFuel()
  func drive(_ mode: DriveMode)
}

class BasicCar: Car {
  func turnEngineOn() {

  }

  func addFuel() {

  }

  func drive(_ mode: DriveMode) {

  }
}

// inheritance
class SuperCar: BasicCar {
  override func drive(_ mode: DriveMode) {
    switch mode {
      case .basic:
        slowDrive()
      case .superFast:
        speedDrive()
    }
  }

  private func slowDrive() {
    // basic functionality
  }

  private func speedDrive() {
    // additional functionality
  }
}

// abstraction
class SuperCar2: Car {
  func turnEngineOn() {

  }

  func addFuel() {

  }

  func drive(_ mode: DriveMode) {
    switch mode {
      case .basic:
        slowDrive()
      case .superFast:
        speedDrive()
    }
  }

  private func slowDrive() {
    // basic functionality
  }

  private func speedDrive() {
    // additional functionality
  }
}

There are a lot more options (using generics, overloading, etc)

LSP - Liskov Substitution Principle

This principle was described firstly by Barbar Liskov in her work regarding data abstraction :

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

In other words:

Subclasses should be substitutable for their base classes

A subtype doesn’t automatically become a valid substitutable for its supertype. We must be sure, that this type can behave in the same manner as its supertype.

This principle is not just about the bad side of inheritance - too many/deep inherited types can be messy, that the reason why we should use composition over inheritance. The main idea of this principle - “is about keeping abstractions crisp and well-defined.”

example

Consider this example:

protocol Patient {
  func tellSimptoms() -> String
}

class SickPerson: Patient {

  func tellSimptoms() -> String {
    "oh, ah"
  }
}

class Doctor {
  func askAPatient(patient: Patient) {
    let simptoms = patient.tellSimptoms()
    // do other stuff
  }
}

let doctor = Doctor()
doctor.askAPatient(patient: Patient())

Now, imagine that we add one more type of patient:

class VoicelessPatient: Patient {
  func tellSimptoms() -> String {
  	 //code smell, but, here can be some other reason for such behavior
  	 // code also can throw an error/exception
    fatalError("voiceless patient can't talk")
    
  }
}

then:

let doctor = Doctor()
doctor.askAPatient(patient: VoicelessPatient()) // -> fatal here, violation of LSP

This is an example of LSP violations. To solve this, we can do next:

protocol Patient {

}

protocol SimptomProvidablePatient: Patient {
  func tellSimptoms() -> String
}

class SickPerson: SimptomProvidablePatient {
  func tellSimptoms() -> String {
    "oh, ah"
  }
}

class VoicelessPatient: Patient {

}
class Doctor {
  func askAPatient(patient: SimptomProvidablePatient) {
    let simptoms = patient.tellSimptoms()
    // do other stuff
  }
}

let doctor = Doctor()
doctor.askAPatient(patient: SickPerson())

ISP - Interface Segregation Principle

From the official paper: “If you have a class that has several clients, rather than loading the class with all the methods that the clients need, create specific interfaces for each client and multiply inherit them into the class”:

Many client-specific interfaces are better than one general-purpose interface

or Clients should not be forced to depend on methods that they do not use.

In other words: Don’t add stuff to me that is not needed for me.

This principle describes a problem with the fat interface - when too many functions/methods/variables are described in the interface, it’s become unmaintainable and so problematic: too many elements, too many responsibilities, too many everything.

example

Imagine, we have an idea to describe a bird:

protocol Bird {
  func speak()
  func fly()
  func jump()
}

class Sparrow: Bird {
  func jump() {
    // ok, sparrow can do this
  }
  func speak() {
    // ok, sparrow can do this too
  }
  func fly() {
    // ok, sparrow can do this also, it's a bird
  }
}

Looks like everything is fine, but what happens, if we would like to describe another Bird - Penguin:

class Penguin: Bird {
  func jump() {
    // ok, penguin good at this - can jump up to 3 m!
  }
  func speak() {
    // ok, penguin can do this
  }
  func fly() {
    // oops, not possible to fly :(
  }
}

To solve this, we can create small protocols instead:

protocol Speakable {
  func speak()
}

protocol Flyable {
  func fly()
}

protocol Jumpable {
  func jump()
}

class Sparrow: Speakable, Flyable, Jumpable {
  func jump() {
    // ok, sparrow can do this
  }
  func speak() {
    // ok, sparrow can do this too
  }
  func fly() {
    // ok, sparrow can do this also, it's a bird
  }
}

class Penguin: Speakable, Jumpable {
  func jump() {
    // ok, penguin good at this - can jump up to 3 m!
  }
  func speak() {
    // ok, penguin can do this
  }
}

DIP - Dependency Inversion Principle

The implication of this principle is quite simple. Every dependency in the design should target an interface or an abstract class. No dependency should target a concrete class.

Depend upon Abstractions. Do not depend upon concretions

The last in list, but not least principle is the essence of development when u have a deal with reusable components - u should decouple dependencies using abstraction. This will improve code reusability.

“Every dependency in the design should target an interface or an abstract class. No dependency should target a concrete class.”

Following this principle not only improve reusability but also bring light to testability.

example

Imagine, that u have the next example:

struct File {

  var path: String
  var name: String
  var type: String
}

class FileHandle {

  func open(_ file: File) {

  }

  func close(_ file: File) {

  }
}

Now, in case if u have a complex logic inside FileHandle - how would u test it? Even if u able to create an instance, the logic inside may be time-consuming, which is not ok within FIRST principles for testing… Also, think about reuse - if something will depend on FileHandle, we can’t easily extend and reuse it. To solve this - here is a DIP:

protocol FileRepresentable {
  var path: String { get }
  var name: String { get }
  var type: String { get }
}

protocol FileProcessable {
  func open(_ file: FileRepresentable)
  func close(_ file: FileRepresentable)
}

struct AnotherFile: FileRepresentable {

  var path: String
  var name: String
  var type: String
}

class AnotherFileHandle: FileProcessable {

  func open(_ file: FileRepresentable) {

  }

  func close(_ file: FileRepresentable) {

  }
}

what’s wrong with SOLID?

During the past few years, there is more and more discussion about efficiency and actuality of some or all principles from SOLID.

If u also think that these principles are irrelevant and old for our time, there is a fresh and complete answer about relevance of SOLID principles from uncle Bob.

I do agree with SOLID, and I do believe that thanks to these principles, my code can become better.

To be honest, I do not’s always follow these principles due to time pressure or some other imaginary reasons. But, I do always try to improve my code, and these principles (within another one like DRY, KISS, etc) are very helpful.

Conclusion

Knowing this principle, as was sad by uncle Bob, will not turn u into a good programmer. But, if u understand them, and use them (at least in the most obvious and critical cases), this improves u’r coder’s life.

Using this principle, u can solve few problems related to software development:

  • Fragility: When any change in the code may affect another part of the code that u don’t expect.
  • Immobility: Big coupling makes the reuse of components impossible.
  • Rigidity: Any change requires too many efforts because it affects a lot part of u’r code.


download source code

Resources