Clean Code Javascript
Clean Code Javascript
clean-code-javascript
Contenido
1. Introducción
2. Variables
3. Funciones
4. Objetos y estructuras de data
5. Clases
6. SOLID
7. Pruebas
8. Concurrencia
9. Resolver los errores
10. Formatear
11. Comentarios
Introducción
Los principios de la ingeniería de software, del libro de Robert C. Martin Clean Code, adaptado para
JavaScript. Esta no es una guía de estilo, en cambio, es una guía para crear software que sea
reutilizable, comprensible y que se pueda mejorar con el tiempo.
No hay que seguir tan estrictamente todos los principios en este libro, y vale la pena mencionar
que hacia muchos de ellos habrá controversia en cuanto al consentimiento. Estas son reflexiones
hechas después de muchos años de experiencia colectiva de los autores de Clean Code.
Nuestra obra de ingeniería de software lleva poco más que 50 años como negocio, y aún estamos
aprendiendo. Cuando la arquitectura de software llegue a ser tan vieja como la arquitectura en sí
misma, quizás tengamos reglas más estrictas para seguir. Hasta entonces, dejemos que estas
guías sirvan como ejemplo para medir la calidad del código en JavaScript que tú y tu equipo
producen.
Una cosa más: saber esto no te hará un mejor ingeniero inmediatamente, y tampoco trabajar con
estas herramientas durante muchos años garantiza que nunca te equivocarás. Cualquier código
empieza primero como un borrador, como arcilla mojada moldeándose en su forma final. Por
último, arreglamos las imperfecciones cuando lo repasamos con nuestros compañeros de trabajo.
No seas tan duro contigo mismo por los borradores iniciales que aún necesitan mejorar. ¡Trabaja
más duro para mejorar el programa!
Variables
Utiliza nombres significativos y pronunciables para las variables
Mal hecho:
const yyyymmdstr = moment().format('YYYY/MM/DD');
Bien hecho:
const fechaActual = moment().format('YYYY/MM/DD');
Bien hecho:
// Decláralos como variables globales de 'const'.
const MILISEGUNDOS_EN_UN_DIA = 8640000;
setTimeout(hastaLaInfinidadYMasAlla, MILISEGUNDOS_EN_UN_DIA);
Bien hecho:
const direccion = 'One Infinite Loop, Cupertino 95014';
const codigoPostalRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [, ciudad, codigoPostal] = direccion.match(codigoPostalRegex) || [];
guardarcodigoPostal(ciudad, codigoPostal);
⬆ vuelve hasta arriba
Evitar el mapeo mental
El explícito es mejor que el implícito.
Mal hecho:
const ubicaciones = ['Austin', 'New York', 'San Francisco'];
ubicaciones.forEach((u) => {
hazUnaCosa();
hasMasCosas()
// ...
// ...
// ...
// Espera, para qué existe la 'u'?
ejecuta(u);
});
Bien hecho:
const ubicaciones = ['Austin', 'New York', 'San Francisco'];
ubicaciones.forEach((ubicacion) => {
hazUnaCosa();
hazMasCosas()
// ...
// ...
// ...
ejecuta(ubicacion);
});
function pintarCoche(coche) {
coche.cocheColor = 'Red';
}
Bien hecho:
const Coche = {
marca: 'Honda',
modelo: 'Accord',
color: 'Blue'
};
function pintarCoche(coche) {
coche.color = 'Red';
}
Bien hecho:
function crearMicroCerveceria(nombreDelMicroCerveceria = 'Hipster Brew Co.') {
// ...
}
Bien hecho:
function crearMenu({ titulo, contexto, textoDelBoton, cancelable }) {
// ...
}
crearMenu({
titulo: 'Foo',
contexto: 'Bar',
textoDelBoton: 'Baz',
cancelable: true
});
Bien hecho:
function escribirClientes(clientes) {
clientes
.filter(esActivoElCliente)
.forEach(email);
}
function esActivoElCliente(cliente) {
const recordDelCliente = database.busca(cliente);
return recordDelCliente.esActivo();
}
Bien hecho:
function agregarMesAlDia(mes, fecha) {
// ...
}
ast.forEach((node) => {
// parse...
});
}
Bien hecho:
function tokenize(code) {
const REGEXES = [
// ...
];
return tokens;
}
function lexer(tokens) {
const ast = [];
tokens.forEach((token) => {
ast.push( /* ... */ );
});
return ast;
}
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
ast.forEach((node) => {
// parse...
});
}
render(data);
});
}
function showManagerList(managers) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Bien hecho:
function showEmployeeList(employees) {
employees.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
function createMenu(config) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable === undefined ? config.cancellable : true;
}
createMenu(menuConfig);
Bien hecho:
const menuConfig = {
title: 'Order',
// El usuario no tenía la clave 'body'
buttonText: 'Send',
cancellable: true
};
function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// el variable 'config' ahora iguala: {title: "Order", body: "Bar", buttonText: "Sen
// ...
}
createMenu(menuConfig);
⬆ vuelve hasta arriba
No utilices 'marcadores' como parámetros de las funciones
Los marcadores existen para decirle a tu usuario que esta función hace más que una sola cosa.
Como se ha mencionado antes las funciones deben hacer una sola cosa. Divide tus funciones en
varias funciones más pequeñas si se adhieren a distintos métodos basados en un booleano.
Mal hecho:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Bien hecho:
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
Bien hecho:
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
Bien hecho:
const addItemToCart = (cart, item) => {
return [...cart, { item, date : Date.now() }];
};
Bien hecho:
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
let totalOutput = 0;
Bien hecho:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const INITIAL_VALUE = 0;
Bien hecho:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
Bien hecho:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
Bien hecho:
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
Bien hecho:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
Bien hecho:
function combine(val1, val2) {
return val1 + val2;
}
function newRequestModule(url) {
// ...
}
Bien hecho:
function newRequestModule(url) {
// ...
}
return {
balance: 0,
// ...
};
}
Bien hecho:
function makeBankAccount() {
// this one is private
let balance = 0;
return {
// ...
getBalance,
setBalance,
};
}
const account = makeBankAccount();
account.setBalance(100);
Bien hecho:
function makeEmployee(name) {
return {
getName() {
return name;
},
};
}
this.age = age;
};
Animal.call(this, age);
this.furColor = furColor;
};
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};
Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};
Bien hecho:
class Animal {
constructor(age) {
this.age = age;
}
move() { /* ... */ }
}
liveBirth() { /* ... */ }
}
speak() { /* ... */ }
}
setMake(make) {
this.make = make;
}
setModel(model) {
this.model = model;
}
setColor(color) {
this.color = color;
}
save() {
console.log(this.make, this.model, this.color);
}
}
Bien hecho:
class Car {
constructor() {
this.make = 'Honda';
this.model = 'Accord';
this.color = 'white';
}
setMake(make) {
this.make = make;
// NOTE: Returning this for chaining
return this;
}
setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}
setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}
save() {
console.log(this.make, this.model, this.color);
// NOTE: Returning this for chaining
return this;
}
}
// ...
}
// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}
Bien hecho:
class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Bien hecho:
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === 'ajaxAdapter') {
return makeAjaxCall(url).then((response) => {
// transform response and return
});
} else if (this.adapter.name === 'httpNodeAdapter') {
return makeHttpCall(url).then((response) => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
Bien hecho:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = 'ajaxAdapter';
}
request(url) {
// request and return promise
}
}
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then((response) => {
// transform response and return
});
}
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach((rectangle) => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
Bien hecho:
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
getArea() {
return this.width * this.height;
}
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach((shape) => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}
traverse() {
// ...
}
}
Bien hecho:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
Mal hecho:
class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
Bien hecho:
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ['WS'];
}
requestItem(item) {
// ...
}
}
describe('MakeMomentJSGreatAgain', () => {
it('handles date boundaries', () => {
let date;
Bien hecho:
import assert from 'assert';
describe('MakeMomentJSGreatAgain', () => {
it('handles 30-day months', () => {
const date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
assert.equal('1/31/2015', date);
});
Bien hecho:
import { get } from 'request';
import { writeFile } from 'fs';
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then((response) => {
return writeFile('article.html', response);
})
.then(() => {
console.log('File written');
})
.catch((err) => {
console.error(err);
});
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then((response) => {
return writeFile('article.html', response);
})
.then(() => {
console.log('File written');
})
.catch((err) => {
console.error(err);
});
Bien hecho:
import { get } from 'request-promise';
import { writeFile } from 'fs-promise';
Bien hecho:
try {
functionThatMightThrow();
} catch (error) {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
}
Bien hecho:
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
// Una opción (más ruidoso que console.log):
console.error(error);
// Otra opción:
notifyUserOfError(error);
// Otra opción
reportErrorToService(error);
// O haz las tres!
});
function eraseDatabase() {}
function restore_database() {}
class animal {}
class Alpaca {}
Bien hecho:
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
function eraseDatabase() {}
function restoreDatabase() {}
class Animal {}
class Alpaca {}
lookupPeers() {
return db.lookup(this.employee, 'peers');
}
lookupManager() {
return db.lookup(this.employee, 'manager');
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getManagerReview() {
const manager = this.lookupManager();
}
getSelfReview() {
// ...
}
}
Bien hecho:
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
lookupPeers() {
return db.lookup(this.employee, 'peers');
}
getManagerReview() {
const manager = this.lookupManager();
}
lookupManager() {
return db.lookup(this.employee, 'manager');
}
getSelfReview() {
// ...
}
}
// Length of string
const length = data.length;
// Iterar cada caracter en la data
for (let i = 0; i < length; i++) {
// Conseguir el código del caracter
const char = data.charCodeAt(i);
// Crear el hash
hash = ((hash << 5) - hash) + char;
// Conviertelo hasta 32-bit
hash &= hash;
}
}
Bien hecho:
function hashIt(data) {
let hash = 0;
const length = data.length;
hazAlgo()
// hazMasCosas();
// hazAunMasCosas();
// hazTantasOtrasCosas();
Bien hecho:
doStuff();
Bien hecho:
function combinar(a, b) {
return a + b;
}
////////////////////////////////////////////////////////////////////////////////
// Iniciar de acciones
////////////////////////////////////////////////////////////////////////////////
const acciones = function() {
// ...
};
Bien hecho:
$scope.modelo = {
menu: 'foo',
nav: 'bar'
};