SOLID PRINCIPLES

Alberto Romero

 

The best architecture is a plugin architecture

 

SOLID

  1. Single Responsibility Principle
  2. Open Close Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

Single Responsibility Principle

A module, class, or a function MUST have only one reason to change.

What is a responsibility?

"A responsability is a family of functions that serves a specific actor in the system" - Robert C. Martin. aka Uncle Bob

Examples of actors

CEO

DBA

Marketing

Finances

Product Designers

 

//routes.js

const appRouter = {
  'GET /currency_conversion/:from/:to/:amount': UtilController.convertMoney
};

//controllers.js
const API_URL = 'www.google.com/conversor';
const UtilController = {
  convertMoney(req, res) {
    const currencyFrom = req.params.from;
    const currencyTo = req.params.to;
    const moneyAmount = req.params.amount;

    const requestURI = `${API_URL}?a=${moneyAmount}&from=${currencyFrom}&to=${currencyTo}`;
    // The controller do the request to the API
    request(requestURI, (error, result) => {
      if (error) return callback(error);
      // And also does the type of conversion!
      const conversionResult = moneyAmount * result;
      callback(null, conversionResult);
    });
  }
};

SRP Violation?

const getMoneyConversion = require('./money-conversor');

const UtilController = {
  convertMoney(req, res) {
    const currencyFrom = req.params.from;
    const currencyTo = req.params.to;
    const moneyAmount = req.params.amount;

    getMoneyConversion(moneyAmount, currencyFrom, currencyTo, (error, result) => {
      res.send(error, result);
    });
};

With a little bit of abstraction and separating responsibilities

what is a controller good for?

//money-conversor.js
const request = require('request');

const API_URL = 'www.google.com/conversor?q=${currencyFrom},${currencyTo},${moneyAmount}';

const getMoneyConversion = function(moneyAmount, currencyFrom, currencyTo, callback) {
  // Example: currencyFrom: USD. currencyTo: MXN.
  request(API_URL, (error, response) => {
    if (error) return callback(error);
    let body = JSON.parse(response.body);
    const exchangeRate = body.rate;
    const conversionResult = moneyAmount * exchangeRate;
    callback(null, conversionResult);
  });
};

"Responsibilities":

Fetch the exchange rate to the cloud

Calculate the conversion

What did happen?

  • API Request Isolated from the controller
  • Money conversion isolated
//exchange-rate-fetcher.js
const request = require('request');

const UNIT = 1;
const API_URL = 'https://www.google.com/finance/converter'

const buildRequestURI = function(moneyAmount, currencyFrom, currencyTo) {
  const requestURI = `${API_URL}?a=${moneyAmount}&from=${currencyFrom}&to=${currencyTo}`;

  return requestURI;
};

const exchangeRateFetcher = function(moneyAmount, currencyFrom, currencyTo, callback) {
  const requestURI = buildRequestURI(moneyAmount, currencyFrom, currencyTo);
  request(requestURI, (error, response) => {
    if (error) return callback(error);
    let responseBody = JSON.parse(response.body);
    const exchangeRate = responseBody.exchangeRate;
    callback(null, exchangeRate;
  });
};

//money-conversor.js
const exchangeRateFetcher = require('./exchange-rate-fetcher');
const getMoneyConversion(moneyAmount, currencyFrom, currencyTo, callback) {
  exchangeRateFetcher(moneyAmount, currencyFrom, currencyTo, (error, exchangeRate) => {
    const conversion = moneyAmount * exchangeRate;
    callback(null, exchangeRate);
  });
};

Open Close Principle

Classes, modules or functions should be open for extension, but closed to modification

In other words

The functionality of a module could be extended without modifying the source code of the module.

Open close principle violation

'use strict'

class CraftBeer {
  constructor(beer) {
    this.beer = beer;
  }

  execute() {
    switch(this.beer) {
      case 'modelo especial':
        console.log('easy beer crafting right here');
        break;
      case 'vicky':
        console.log('umm some beer crafting right here');
        break;
      case 'minerva stout':
        console.log('Really complex beer crafting right here');
        break;
      default:
        throw new Error('beer is not supported');
    }

    console.log('The beer was successfully crafted');
  }
}

const usecase = new CraftBeer('modelo especial');
usecase.execute();

"When building software, we have to listen to three main actors in the system: Don Objeto, Don Cliente and Don Diseño" - Carlos Ordoñez

Do we have to treat our business objects as strings or as objects?

Text

'use strict';

class CraftBeer {
  constructor(beer) {
    this.beer = beer;
  }

  execute() {
    this.beer.craft();
  }
}

class VictoriaBeer {
  craft() {
    console.log('easy beer crafting right here');
    return 14;
  }
}

class MinervaStoutBeer {
  craft() {
    console.log('easy beer crafting right here');
    return 21.50;
  }
}

class ModeloEspecialBeer {
  craft() {
    console.log('easy beer crafting right here');
    return 4;
  }
}

const usecase = new CraftBeer(new ModeloEspecialBeer());
usecase.execute();

This principle only protects us from change, if we can attempt to predict the future

Two approaches:

1. Try to anticipate everything and create abstractions for every single thing.

2. Don't solve a problem you still don't have.

Liskov Substitution Principle

If a module is using a Base class, then the reference to the Base class can be replaced with a derived class without screwing the functionality of the module.

class Rectangle {
  setHeight(height) {
    this.height = height;
  }

  setWidth(width) {
    this.width = width;
  }

  getArea() {
    return height * width;
  }
}

class Square extends Rectangle {
  setHeight(height) {
    this.height = height;
    this.width = height;
  }

  setWidth(width) {
    this.width = width;
    this.height = width;
  }
}

Liskov's substitution principle violation

Liskov's Substitution Principle is about contracts and semantics.


A contract is a set of preconditions and postconditions that must be respected into the system

Solution to the shapes problem.

It depends.

Interface Segregation Principle

Clients should not be forced to depend upon interfaces that they don't use.

Interface Segregation Violation

// Service definition
class BotService {
  createBot() {
    // implementation for create a bot
  }

  updateBot() {
    // implementation for update a bot
  }

  deleteBot() {
    // implementation for delete a bot
  }

  deployBot() {
    // implementation for deploy a bot
  }
}

// Clients using it
app.post('/bot, (req, res) => {
  const requestBody = buildRequestBody(req);
  try {
    BotService().createBot();
    buildSuccessResponse();
  } catch (error) {
    buildErrorResponse(res);
  }
});

Why is it violating the principle?

Dependency Inversion Principle

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

Abstraction Concept

What is it for?

  • Hide complexity or low level details using higher level modules.
  • The high level modules doesn't need to know about how the lower level modules do something.

Bad example

//database.js
const SQL = require('sqldriver');

class Database {
  findUserById(id, callback) {
    const query = `SELECT * FROM USERS WHERE id = ${id}`;
    SQL.exec(query, callback);
  }

  findUserByUsername(username, callback) {
    const query = `SELECT * FROM USERS WHERE username = ${username}`;
    SQL.exec(query, callback);
  }

  saveUser(user, callback) {
    const fields = Object.keys(user);
    const values = fields.map((field) => {
      return user[field];
    });

    const query = `INSERT INTO user (${Object.keys(user)}) values (${values.join(',')}`;
    SQL.exec(query, callback);
  }
}
//database.js

const UserStore = require('./user-store');
const User = require('./entities/user');

class Database {
  constructor(args) {
    this.userStore = new UserStore();
  }

  findUserById(id, callback) {
    this.userStore.findUserById(id, (error, user) => {
      callback(error, new User(user));
    });
  }

  findUserByUsername(id, callback) {
    this.userStore.findUserById(id, (error, user) => {
      callback(error, new User(user));
    });
  }

  saveUser(user, callback) {
    this.userStore.saveUser(user, callback);
  }
}

class UserStore {
  findUserById(id, callback) {
    const query = `SELECT * FROM USERS WHERE id = ${id}`;
    SQL.exec(query, callback);
  }

  ...
}

Dependency inversion principle applied :D

Benefits:

1. Database driver swaping.

2. Business logic remains untouched.

Thanks!

References:


www.cleancoders.com

www.oodesign.com

www.lostechies.com



SOLID PRINCIPLES

By Alberto Romero

SOLID PRINCIPLES

A quick overview of the 5 SOLID principles of software development: Single Responsibility Principle, Open Close Principle, Liskov's Substitution Principle, Interface Segregation Principle and Dependency Inversion Principle

  • 526