SOLID Principles Author: Majid Ahmaditabar
do dolphins fly in your code??

SOLID Principles

Majid Ahmaditabar

--

In object-oriented computer programming, SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. The principles are a subset of many principles promoted by American software engineer and instructor Robert C. Martin first introduced in his 2000 paper Design Principles and Design Patterns.

The SOLID concepts

S : Single-responsibility principle

O : Open–closed principle

L : Liskov Substitution Principle

I : Interface Segregation Principle

D : Dependency Inversion Principle

Single-responsibility principle (SRP)

Try to make every class responsible for a single part of the functionality provided by the software, and make that responsibility entirely encapsulated by the class.

The main goal of this principle is reducing complexity. You don’t need to invent a sophisticated design for a program that only has about 200 lines of code. Make a dozen methods pretty, and you’ll be fine.

The real problems emerge when your program constantly grows and changes. At some point classes become so big that you can no longer remember their details.

There’s more: if a class does too many things, you have to change it every time one of these things changes. While doing that, you’re risking breaking other parts of the class which you didn’t even intend to change.

class User{
display_user(){}
update_user(){}
send_email(){}
display_orders(){}
}

What is the purpose and responsibility of “User” class? what is the exact responsibility of User Class? Probably store or display user information.The User class should not be responsible for sending emails or handling user orders.In this case, our class is not confined to its inherent functions. That is, the User class is mixed with a series of unrelated functions. we change it to :

class User{
display_user(){}
update_user(){}
}
class Email{
send_email(){}
}
class Order{
display_orders(){}
}

As you can see, our User Class is more clean and simple. It is also easier to develop and implement it.

Open–closed principle (OCP)

The main idea of this principle is to keep existing code from breaking when you implement new features.

When I first learned about this principle, I was confused because the words open & closed sound mutually exclusive. But in terms of this principle, a class can be both open (for extension) and closed (for modification) at the same time.

This principle isn’t meant to be applied for all changes to a class. If you know that there’s a bug in the class, just go on and fix it; don’t create a subclass for it. A child class shouldn’t be responsible for the parent’s issues.

class Order{

display_orders(){}

get_shipping_cost(shipping_type){
if(shipping_type == "ground"){
return "ground cost"
}
if(shipping_type == "air"){
return "air cost"
}
}
}order = new order()
display -> order.get_shipping_cost("air")

BEFORE: you have to change the Order class whenever you add a new shipping method to the app.The Order class is not closed to changes and is always subject to manipulation from the outside.

You can solve the problem by applying the Strategy pattern.

class ground_shipping {
get_cost() {
return "ground cost"
}
}
class air_shipping {
get_cost() {
return "air cost"
}
}
// new shipping method
class train_shipping{
get_cost() {
return "train cost"
}
}
class Order {
display_orders() {}
get_shipping_cost(shipping_type) {
return shipping_type.get_cost()
}
}
order = new Order()
display -> order.get_shipping_cost(new train_shipping())

As you can see, The Order Class is more clean and simple. It is also easier to develop and implement it.and when we add new shipping method it doesn’t need to change Order Class we just define new shipping Class.

Liskov Substitution Principle (LSP)

This principle is named by Barbara Liskov, who defined it in 1987 in her work Data abstraction and hierarchy.

LSP means that the subclass should remain compatible with the behavior of the superclass. When overriding a method, extend the base behavior rather than replacing it with some- thing else entirely.

  • Parameter types in a method of a subclass should match or be more abstract than parameter types in the method of the super- class.
  • The return type in a method of a subclass should match or be a subtype of the return type in the method of the superclass
  • A method in a subclass shouldn’t throw types of exceptions which the base method isn’t expected to throw
  • A subclass shouldn’t strengthen pre-conditions.
  • A subclass shouldn’t weaken post-conditions.
  • Invariants of a superclass must be preserved.
  • A subclass shouldn’t change values of private fields of the superclass.
class Note{
public constructor(id) {
// ...
}
display_note(){}
update_note(text){
// save note
}
}
note_1 = new Note(10)
note_1.display_note()
note_2 = new Note(20)
note_2.display_note()
note_2.update_note("Update note") // save text

if we add new feature called ReadonlyNote that never change their information.

class Note{
public constructor(id) {
// ...
}
display_note(){}
update_note(text){
// save note
}
}
class ReadonlyNote extends Note{
update_note(text){
throw new Error("Can't update readonly Note!");
}
}
note_1 = new Note(10) ---> ReadonlyNote(10) //Here we do the replacement!note_1.display_note()note_2 = new Note(20) ---> ReadonlyNote(20) //Here we do the replacement!note_2.display_note()note_2.update_note("Update note") //then return Exception

Here the LSP principle was violated! solution is :

class Note{
public constructor(id) {

}
display_note(){}
}
class WritableNote extends Note {
constructor(id) {
super(id);
}

update_note(text){
// save note
}
}

class ReadonlyNote extends Note {
constructor(id) {
super(id)
}

update_note(text){
throw new Error("Can't update readonly Note!");
}
}
note_1 = new WritableNote(10)
note_2 = new WritableNote(20)
note_2.update_note("Update note")

Interface Segregation Principle (ISP)

Try to make your interfaces narrow enough that client classes don’t have to implement behaviors they don’t need.

According to the interface segregation principle, you should break down “fat” interfaces into more granular and specific ones. Clients should implement only those methods that they really need. Otherwise, a change to a “fat” interface would break even clients that don’t use the changed methods.

Classes should not have to implement methods they do not need

Do Dolphins fly? let see it!

interface Animal {
fly();
run();
eat();
}
class Bird implements Animal {

fly() {
// Fly
}

run() {
// Run
}

eat() {
// Eat
}
}
class Dolphin implements Animal {
fly() {
return false
}

run() {
// Run
}

eat() {
// Eat
}
}
dolphin = new Dolphin()
dolphin.fly() //seriously?

Here the ISP principle was violated! solution is :

interface Animal {
run()
eat()
}

interface FlyableAnimal {
fly()
}
class Bird implements Animal , FlyableAnimal {

fly() {
// Fly
}

run() {
// Run
}

eat() {
// Eat
}
}

class Dolphin implements Animal {
run() {
// Run
}

eat() {
// Eat
}
}

Dependency Inversion Principle (DIP)

Usually when designing software, you can make a distinction between two levels of classes.

  • Low-level classes implement basic operations such as working with a disk, transferring data over a network, connecting to a database, etc.
  • High-level classes contain complex business logic that directs low-level classes to do something.

The dependency inversion principle suggests changing the direction of this dependency.

  1. For starters, you need to describe interfaces for low-level operations that high-level classes rely on, preferably in business terms. For instance, business logic should call a method
  2. openReport(file) rather than a series of methods
  3. openFile(x) , readBytes(n) , closeFile(x) . These interfaces count as high-level ones.
  4. Now you can make high-level classes dependent on those interfaces, instead of on concrete low-level classes. This dependency will be much softer than the original one.
  5. Once low-level classes implement these interfaces, they become dependent on the business logic level, reversing the direction of the original dependency.
  6. The dependency inversion principle often goes along with the open/closed principle: you can extend low-level classes to use with different business logic classes without breaking existing classes.
// low level class
class MySQL {
insert() {}
update() {}
delete() {}
}
// high level class
class BankAccount {
private database;

constructor() {
this.database = new MySQL;
}
open () {}
save () {}
}

BEFORE: a high-level class depends on a low-level class.

You can fix this problem by creating a high-level interface that describes read/write operations and making the reporting class use that interface instead of the low-level class. Then you can change or extend the original low-level class to implement the new read/write interface declared by the business logic.

interface Database {
insert()
update()
delete()
}
class MySQL implements Database {
insert() {}
update() {}
delete() {}
}

class Oracle implements Database {
insert() {}
update() {}
delete() {}
}

class MongoDB implements Database {
insert() {}
update() {}
delete() {}
}
class BankAccount{
private db: Database

setDatabase(db: Database) {
this.db = db
}

open () {
this.db.insert()
}

save () {
this.db.update()
}
}bank_account = new BankAccountbank_account.setDatabase(new MongoDB);
// your code and process
bank_account.setDatabase(new Oracle);
// your code and process
bank_account.setDatabase(new MySQL);
bank_account.update();

Like all other SOLID principles, this principle seeks to reduce the interdependence between components so that we can write more maintainable, cleaner, and more extensible code.

References

--

--