Clean Code - JavaScript
Clean Code - JavaScript
Se você está no desenvolvimento de software, esse é um dos livros que você deve ler!
2
Índice
1. Introdução
2. Variáveis
3. Funções
5. Classes
6. SOLID
7. Testes
8. Concorrência
9. Tratamento de Erros
10. Formatação
11. Comentários
12. Créditos
. . .
1. Introdução
3
Mais uma coisa: aprender isto não irá lhe transformar imediatamente
em um desenvolvedor de software melhor e trabalhar com eles por
muitos anos não quer dizer que você não cometerá erros. Toda porção
de código começa com um rascunho, como argila molhada sendo
moldada em sua forma �nal. Finalmente, talhamos as imperfeições
4
. . .
2. Variáveis
Bom:
getUserInfo();
getClientData();
getCustomerRecord();
Bom:
getUser();
5
Ruim:
Bom:
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
Bom:
saveCityZipCode(city, zipCode);
Ruim:
Bom:
Ruim:
const Car = {
carMake: 'Honda',
carModel: 'Accord',
7
carColor: 'Blue'
};
function paintCar(car) {
car.carColor = 'Red';
}
Bom:
const Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};
function paintCar(car) {
car.color = 'Red';
}
Ruim:
function createMicrobrewery(name) {
const breweryName = name || 'Hipster Brew Co.';
// ...
}
Bom:
{
// ...
}
. . .
3.
2.
1.
• 3. Funções
Ruim:
9
Bom:
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
Ruim:
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Bom:
10
function emailActiveClients(clients) {
clients
.filter(isActiveClient)
.forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
Bom:
Ruim:
11
function parseBetterJSAlternative(code) {
const REGEXES = [
// ...
];
ast.forEach((node) => {
// parse...
});
}
Bom:
function tokenize(code) {
const REGEXES = [
// ...
];
return tokens;
}
function lexer(tokens) {
const ast = [];
tokens.forEach((token) => {
ast.push( /* ... */ );
});
return ast;
}
function parseBetterJSAlternative(code) {
12
Ruim:
function showDeveloperList(developers) {
developers.forEach((developer) => {
const expectedSalary =
developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
13
experience,
githubLink
};
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);
});
}
Bom:
function showEmployeeList(employees) {
employees.forEach((employee) => {
const expectedSalary =
employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch(employee.type){
case 'manager':
data.portfolio = employee.getMBAProjects();
break;
case 'developer':
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
Ruim:
const menuConfig = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
};
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);
Bom:
const menuConfig = {
title: 'Order',
// Usuário não incluiu a chave 'body'
buttonText: 'Send',
cancellable: true
};
function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
createMenu(menuConfig);
Ruim:
Bom:
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
Ruim:
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
Bom:
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
Ruim:
Bom:
Ruim:
Bom:
Ruim:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
let totalOutput = 0;
Bom:
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;
Encapsule condicionais
Ruim:
Bom:
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
Bom:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
21
Evite condicionais
Esta parece ser uma tarefa impossível. Da primeira vez que as pessoas
escutam isso, a maioria diz, “como eu supostamente faria alguma
coisa sem usar if ? ” A resposta é que você pode usar polimor�smo
para realizar a mesma tarefa em diversos casos. A segunda questão é
geralmente, “bom, isso é ótimo, mas porque eu deveria fazer isso?” A
resposta é um conceito de código limpo aprendido previamente: uma
função deve fazer apenas uma coisa. Quando você tem classes e
funções que tem declarações if , você esta dizendo para seu usuário
que sua função faz mais de uma coisa. Relembre-se, apenas uma
coisa.
Ruim:
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() -
this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() -
this.getFuelExpenditure();
}
}
}
Bom:
class Airplane {
// ...
}
Ruim:
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new
Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new
Location('texas'));
}
}
Bom:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
Ruim:
Bom:
Ruim:
24
Bom:
Ruim:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
Bom:
function newRequestModule(url) {
// ...
25
Ruim:
function makeBankAccount() {
// ...
return {
balance: 0,
// ...
};
}
Bom:
26
function makeBankAccount() {
// este é privado
let balance = 0;
return {
// ...
getBalance,
setBalance,
};
}
Ruim:
Bom:
function makeEmployee(name) {
return {
getName() {
return name;
},
};
}
5. Classes
Ruim:
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() {};
Bom:
class Animal {
constructor(age) {
this.age = age;
}
move() { /* ... */ }
}
liveBirth() { /* ... */ }
}
speak() { /* ... */ }
29
Ruim:
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
}
setModel(model) {
this.model = model;
}
setColor(color) {
this.color = color;
}
save() {
console.log(this.make, this.model, this.color);
}
}
Bom:
30
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// NOTA: Retorne this para encadear
return this;
}
setModel(model) {
this.model = model;
// NOTA: Retorne this para encadear
return this;
}
setColor(color) {
this.color = color;
// NOTA: Retorne this para encadear
return this;
}
save() {
console.log(this.make, this.model, this.color);
// NOTA: Retorne this para encadear
return this;
}
}
Ruim:
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// ...
}
Bom:
class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class Employee {
constructor(name, email) {
this.name = name;
32
this.email = email;
}
setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}
6. SOLID
Ruim:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Bom:
33
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()) {
// ...
}
}
}
Ruim:
class HttpRequester {
34
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === 'ajaxAdapter') {
return makeAjaxCall(url).then((response) => {
// transforma a resposta e retorna
});
} else if (this.adapter.name === 'httpNodeAdapter') {
return makeHttpCall(url).then((response) => {
// transforma a resposta e retorna
});
}
}
}
function makeAjaxCall(url) {
// faz a request e retorna a promessa
}
function makeHttpCall(url) {
// faz a request e retorna a promessa
}
Bom:
request(url) {
// faz a request e retorna a promessa
}
}
request(url) {
// faz a request e retorna a promessa
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
35
fetch(url) {
return this.adapter.request(url).then((response) => {
// transforma a resposta e retorna
});
}
}
A melhor explicação para este conceito é se você tiver uma classe pai
e uma classe �lha, então a classe base e a classe �lha pode ser usadas
indistintamente sem ter resultados incorretos. Isso ainda pode ser
confuso, então vamos dar uma olhada no exemplo clássico do
Quadrado-Retângulo (Square-Rectangle). Matematicamente, um
quadrado é um retângulo, mas se você modelá-lo usando uma relação
“isto-é” através de herança, você rapidamente terá problemas.
Ruim:
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
36
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(); // RUIM: Retorna 25
para o Quadrado. Deveria ser 20.
rectangle.render(area);
});
}
Bom:
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);
});
}
Ruim:
38
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}
traverse() {
// ...
}
}
Bom:
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() {
// ...
}
}
animationModule() {}
}
});
Ruim:
class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
40
constructor(items) {
this.items = items;
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
Bom:
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) {
41
// ...
}
}
7. Testes
Testes são mais importantes que entregas. Se você não possui testes
ou um quantidade inadequada, então toda vez que você entregar seu
código você não terá certeza se você não quebrou alguma coisa.
Decidir o que constitui uma quantidade adequada é responsabilidade
do seu time, mas ter 100% de cobertura (todas as sentenças e
branches) é a maneira que se alcança uma alta con�ança e uma paz
de espirito em desenvolvimento. Isso quer dizer que além de ter um
ótimo framework de testes, você também precisa usar uma boa
ferramenta de cobertura.
describe('MakeMomentJSGreatAgain', () => {
it('handles date boundaries', () => {
let date;
42
Bom:
describe('MakeMomentJSGreatAgain', () => {
it('handles 30-day months', () => {
const date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
assert.equal('1/31/2015', date);
});
8. Concorrência
Ruim:
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin',
(requestErr, response) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile('article.html', response.body, (writeErr) => {
if (writeErr) {
console.error(writeErr);
} else {
console.log('File written');
}
});
}
});
Bom:
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);
});
Ruim:
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);
});
Bom:
9. Tratamento de Erros
throw error é uma coisa boa! Eles signi�cam que o programa
identi�cou com sucesso quando algo deu errado e está permitindo
que você saiba parando a execução da função no processo atual,
fechando o processo (em Node), e noti�cando você no console com a
pilha de processos.
Ruim:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
Bom:
try {
functionThatMightThrow();
} catch (error) {
// Uma opção (mais chamativa que console.log):
console.error(error);
// Outra opção:
notifyUserOfError(error);
// Outra opção:
reportErrorToService(error);
// OU as três!
}
Ruim:
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
46
console.log(error);
});
Bom:
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
});
10. Formatação
Formatação é subjetiva. Como muitas regras aqui, não há nenhuma
regra �xa e rápida que você precisa seguir. O ponto principal é NÃO
DISCUTA sobre formatação. Existem muitas ferramentas para
automatizar isso. Utilize uma! É um desperdicio de tempo e dinheiro
para engenheiros discutirem sobre formatação.
Ruim:
const DAYS_IN_WEEK = 7;
47
function eraseDatabase() {}
function restore_database() {}
class animal {}
class Alpaca {}
Bom:
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
function eraseDatabase() {}
function restoreDatabase() {}
class Animal {}
class Alpaca {}
Ruim:
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
lookupPeers() {
return db.lookup(this.employee, 'peers');
48
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() {
// ...
}
}
Bom:
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');
}
49
getManagerReview() {
const manager = this.lookupManager();
}
lookupManager() {
return db.lookup(this.employee, 'manager');
}
getSelfReview() {
// ...
}
}
11. Comentários
Ruim:
function hashIt(data) {
// A hash
let hash = 0;
// Tamanho da string
const length = data.length;
Bom:
50
function hashIt(data) {
let hash = 0;
const length = data.length;
Ruim:
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
Bom:
doStuff();
Ruim:
/**
51
Bom:
function combine(a, b) {
return a + b;
}
Ruim:
/////////////////////////////////////////////////////////////
///////////////////
// Instanciação do Scope Model
/////////////////////////////////////////////////////////////
///////////////////
$scope.model = {
menu: 'foo',
nav: 'bar'
};
/////////////////////////////////////////////////////////////
///////////////////
// Configuração da Action
/////////////////////////////////////////////////////////////
///////////////////
const actions = function() {
// ...
};
Bom:
52
$scope.model = {
menu: 'foo',
nav: 'bar'
};
12. Créditos
clean-code-javascript, traduzido originalmente por Felipe
Augusto
53