A module, class, or a function MUST have only one reason to change.
"A responsability is a family of functions that serves a specific actor in the system" - Robert C. Martin. aka Uncle Bob
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
//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);
});
};Classes, modules or functions should be open for extension, but closed to modification
The functionality of a module could be extended without modifying the source code of the module.
'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();
1. Try to anticipate everything and create abstractions for every single thing.
2. Don't solve a problem you still don't have.
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.
Clients should not be forced to depend upon interfaces that they don't use.
// 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);
}
});What is it for?
//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