Clean Code JavaScript
Neste artigo, vou apresentar algumas boas práticas de programação em JavaScript. No entanto, essas práticas podem ser aplicadas a qualquer linguagem de programação, pois representam um conjunto de diretrizes recomendadas.
O conceito central abordado aqui é o "Clean Code," que se refere a um código que é fácil de ler e entender, bem como fácil de manter. O objetivo é criar um código claro e conciso.
Antes de prosseguir, gostaria de começar com uma citação de Bjarne Stroustrup, o criador da linguagem C++:
"Eu gosto que meu código seja elegante e eficiente. A lógica deve ser direta para dificultar a ocultação de bugs, as dependências devem ser mínimas para facilitar a manutenção, o tratamento de erros deve ser completo de acordo com uma estratégia articulada, e o desempenho deve ser próximo do ideal, de forma a não tentar as pessoas a tornar o código confuso com otimizações sem princípios. O código limpo faz uma coisa bem."
Variáveis
Nomes
- Utilize nomes que descrevam claramente o propósito da variável.
- Evite nomes genéricos, como
a
,b
,c
,x
,y
ez
.
// Ruim
const start = new Date();
const dateE = new Date();
const j = 'Tautorn';
// Bom
const startDate = new Date();
const endDate = new Date();
const userName = 'Tautorn';
Em alguns casos, usar nomes mais longos para variáveis pode ser uma boa opção, desde que o nome descreva com precisão o que a variável representa.
componentDidMount
é um exemplo de nome de função que descreve bem o que ela faz.
startLifeCycleAfterRenderer
é um exemplo de nome de função que descreve bem o que ela faz, mas é um nome muito grande.
O mesmo princípio se aplica a nomes de funções:
// ruim
const getId = () => {
// ...
}
// bom
const getUserId = () => {
// ...
}
Convenção
- Utilize
camelCase
para nomear variáveis e funções. - Utilize
PascalCase
para nomear classes. - Utilize
UPPER_CASE
para nomear constantes.
// Ruim
const user_name = 'Tautorn'
class userList {}
const Range_limitForLoan = 100
// Bom
const userName = 'Tautorn'
class UserList {}
const RANGE_LIMIT_FOR_LOAN = 100
Evite variáveis globais
Variáveis globais devem ser evitadas, pois podem ser acessadas e modificadas de qualquer lugar no código, o que pode levar a problemas difíceis de identificar.
Não utilize números mágicos
Números mágicos são valores numéricos usados diretamente no código sem explicação. Evite isso atribuindo esses valores a constantes significativas.
// Ruim
const getDiscount = (value) => {
if (value > 100) {
return value * 0.1
} else {
return value * 0.05
}
}
// Bom
const DISCOUNT_VALUE = 0.1
const DISCOUNT_VALUE_DEFAULT = 0.05
const DISCOUNT_LIMIT = 100
const getDiscount = (value) => {
if (value > DISCOUNT_LIMIT) {
return value * DISCOUNT_VALUE
} else {
return value * DISCOUNT_VALUE_DEFAULT
}
}
Funções:
Utilize arrow functions para funções curtas e callbacks:
// Ruim
function sanitizeName(name) {
return name.trim().toLowerCase()
}
// Bom
const sanitizeName = (name) => name.trim().toLowerCase()
Funções com responsabilidade única:
- Funções devem fazer apenas uma coisas. Esse é o princípio de responsabilidade única.
Ruim:
const getUserNameAndSalary = (id) => {
const response = await fetch(`api.com/${id}`)
const userData = await response.json()
const responseSalary = await fetch(`api.com/salary/${id}`)
const userSalary = await responseSalary.json()
return {
name: userData.name,
salary: userSalary.salary
}
}
Além de misturar duas requests a manutenção fica mais difícil, tests e evolução.
Bom:
const getUserName = () => {
const response = await fetch(`api.com/${id}`)
const userData = await response.json()
return {
name: userData.name
}
}
const getUserSalary = () => {
const responseSalary = await fetch(`api.com/salary/${id}`)
const userSalary = await responseSalary.json()
return {
salary: userSalary.salary
}
}
Dessa forma, cada função possui uma única responsabilidade, facilitando a manutenção e a compreensão do código.
É claro que poderíamos ter uma função única chamada getUser
que traria todos os dados do usuário. Mas eu só quis trazer um exemplo de um método realizando duas coisas que deveríam estar isoladas.
Funções devem ter no máximo 3 parâmetros
Ruim:
const getCarContext = (id, color, weight, model, engine) => {
// ...
}
Isso dificulta e muito a evolução da função, aumenta a complexidade e quem chama a função também pode ter dificuldade com os parâmetros (tipagem ajuda mas não estou tratando isso neste artigo).
Bom:
const getCarContext = (id) => {
// ...
}
Mas como estamos utilizando JavaScript é possível esperar um objeto como parâmetro, evitando assim o problema de muitos parâmetros.
const getLocation = ({ city, state, country }) => {
// ...
}
O formato acima, com objeto, pra mim é o melhor cenário porque quem está chamando a função não precisa saber cada parâmetro que a função espera baseado em sua posição, ex:
// ruim
getLocation('São Paulo', 'SP', 'Brasil')
// bom
getLocation({
city: 'São Paulo',
state: 'SP',
country: 'Brasil'
})
Dessa maneira fica mais fácil de entender o que a função espera e evita erros bobos.
Também podemos realizar um destructuring no parâmetro da função para facilitar a utilização:
const getLocation = (props) => {
const { city, state, country } = props
// ...
}
Essa é uma boa maneira de evitar muitos parâmetros em uma função.
Funções puras
Funções puras são funções que não alteram o estado de nada, ou seja, não alteram o valor de uma variável, não alteram o valor de um objeto, não alteram o valor de um array.
// ruim
let userName = 'value'
const getUserName = (name) => {
userName = name
// ...
}
// bom
const getUserName = (name) => {
const userName = name
// ...
}
Evitar a alteração do valor de uma variável ou objeto em locais diferentes ajuda a prevenir efeitos colaterais e torna os erros mais fáceis de identificar.
Condicionais:
Evite utilizar If Else
Usar muitos blocos if else no meio do código pode torná-lo complexo e difícil de entender. Em vez disso, utilize operadores ternários ou estruturas de dados para simplificar o código.
// Ruim
const getProduct = (id) => {
const response = await fetch(`api.com/${id}`)
const product = await response.json()
if (product.value > 100) {
return {
...product,
discount: 10
}
} else {
return {
...product,
discount: 5
}
}
}
// Bom
const getProduct = (id) => {
const response = await fetch(`api.com/${id}`)
const product = await response.json()
const discount = product.value > 100 ? 10 : 5
return {
...product,
discount
}
}
O exemplo Bom
é muito mais legível e fácil de manter do que o exemplo Ruim
. Um ternário tornou as coisas mais simples.
Existem números mágicos na função, mas falarei mais pra frente.
Note que também deixe apenas um return
na função, este é o melhor cenário. Funções com vários returns
são bastante problemáticas.
// Ruim
const getAuthenticateSession = (token) => {
let isActive = false
const limitTime = 1000 * 60 * 60 * 24 * 30 // 30 dias
if (!token) {
return false
}
const tokenDate = new Date(token.date)
const currentDate = new Date()
const isValid = currentDate - tokenDate > limitTime
if (isValid) {
const tokenDate = new Date(token.date)
isActive = true
}
if (isActive) {
return {
status: 'active',
message: 'Sessão ativa'
date: new Date()
}
} else {
return {
status: 'inactive',
message: 'Sessão inativa'
date: new Date()
}
}
}
// Bom
const tokenIsValid = (token) => {
const limitTime = 1000 * 60 * 60 * 24 * 30 // 30 dias
if (!token) {
return false
}
const tokenDate = new Date(token.date)
const currentDate = new Date()
return currentDate - tokenDate > limitTime
}
const getAuthenticateSession = (token) => {
const isActive = tokenIsValid(token)
return{
status: isActive ? 'active' : 'inactive',
message: isActive ? 'Sessão ativa' : 'Sessão inativa',
date: new Date()
}
}
Utilize objetos literais ao invés de if else e switch/case
// Ruim
function getCategory(category) {
if (category === 'food') {
return 'food'
} else if (category === 'drink') {
return 'drink'
} else if (category === 'dessert') {
return 'dessert'
} else {
return 'other'
}
}
// Ruim
function getCategory(category) {
switch (category) {
case 'food':
return 'food'
case 'drink':
return 'drink'
case 'dessert':
return 'dessert'
default:
return 'other'
}
// Bom
function getCategory(category) {
const categories = {
food: 'food',
drink: 'drink',
dessert: 'dessert',
other: 'other'
}
return categories[category] || 'other'
}
Essa abordagem vale para vários casos, até mesmo para retorno de callbacks, exemplo:
function getFinanciersCallBack(type) {
const financiers = {
bank: () => {
// ...
},
creditCard: () => {
// ...
},
loan: () => {
// ...
}
}
return financiers[type] || () => {
// ...
}
}
Objetos:
Utilize getter
e setter
para acessar propriedades de um objeto
Os métodos getter e setter oferecem controle sobre a manipulação das propriedades de um objeto, permitindo validações antes de acessar ou modificar valores.
const person = {
_name: 'John',
get name() {
return this._name;
},
set name(newName) {
if (typeof newName === 'string') {
this._name = newName;
} else {
console.error('O nome deve ser uma string.');
}
}
};
console.log(person.name); // Obtém o nome
person.name = 'Alice'; // Define o nome
person.name = 42; // Erro de validação
Use propriedades descritivas
Ao definir propriedades de objetos, utilize nomes que descrevam claramente o que a propriedade representa.
// Ruim
const location = {
c: 'Brasil',
s: 'SP',
ct: 'São Paulo'
}
// Bom
const location = {
country: 'Brasil',
state: 'SP',
city: 'São Paulo'
}
Clonar objetos
Utilize spread
ou Object.assign
para clonar objetos. em vez de atribuições diretas. Dessa maneira evita-se efeitos colaterais ao modificar o objeto original.
// Ruim
const clonedObject = originalObject
// Bom
const clonedObject = { ...originalObject }
ou
const clonedObject = Object.assign({}, originalObject)
Arrays:
Utilize map, filter, reduce ao invés de for
// Ruim
const users = [
{ name: 'Tautorn', age: 30 },
{ name: 'John', age: 20 },
{ name: 'Mary', age: 25 }
]
const usersWithAge = []
for (let i = 0; i < users.length; i++) {
usersWithAge.push({
...users[i],
age: users[i].age + 1
})
}
// Bom
const users = [
{ name: 'Tautorn', age: 30 },
{ name: 'John', age: 20 },
{ name: 'Mary', age: 25 }
]
const usersWithAge = users.map((user) => ({
...user,
age: user.age + 1
}))
Utilize spread
para copiar arrays
// Ruim
const users = [
{ name: 'Tautorn', age: 30 },
{ name: 'John', age: 20 },
{ name: 'Mary', age: 25 }
]
const usersCopy = []
for (let i = 0; i < users.length; i += 1) {
itemsCopy[i] = items[i];
}
// Bom
const users = [
{ name: 'Tautorn', age: 30 },
{ name: 'John', age: 20 },
{ name: 'Mary', age: 25 }
]
const usersCopy = [...users]
Concorrência:
Callback hell
Callback hell é quando temos muitos callbacks aninhados, isso torna o código muito difícil de ler e dar manutenção.
// Ruim
getProfile(function(user)) {
getArticles(function(articles) {
getComments(function(comments) {
getLikes(function(likes) {
// ...
})
})
})
}
// Bom
getProfile(user)
.then(getArticles)
.then(getComments)
.then(getLikes)
.then((likes) => {
// ...
})
Tratamento de erros:
Não ignore erros capturados e saiba como organizar os erros. É muito importante saber o que fazer com os erros, em muitos casos são ignorados e isso pode gerar problemas muito difíceis de serem identificados. Identificar os problemas quando o código já está em produção pode ser uma tarefa difícil quando o tratamento de erros não foi bem pensado/executado.
// Ruim
try {
functionCausingError()
} catch (error) {
console.log(error)
}
// Bom
try {
functionCausingError()
} catch (error) {
// Console error é um alerta melhor do que utilizar console.log
console.error(`Error description ${error}`)
generateLog(error)
notifyUser(error)
}
Try/catch
O mesmo vale utilizando try/catch, aqui um exemplo com async/await.
// Ruim
const getProfile = async (id) => {
try {
const response = await fetch(`api.com/${id}`)
const profile = await response.json()
return profile
} catch (error) {
console.log(error)
}
}
// Bom
const getProfile = async (id) => {
try {
const response = await fetch(`api.com/${id}`)
const profile = await response.json()
return profile
} catch (error) {
// Console error é um alerta melhor do que utilizar console.log
console.error(`Error description ${error}`)
generateLog(error)
notifyUser(error)
}
}
A lógica foi a mesma, mas agora estamos tratando o erro de uma maneira melhor.
Conclusão
Neste artigo vimos algumas boas práticas de programação utilizando JavaScript. Mas isso pode ser aplicado em qualquer linguagem de programação, isso é uma boa prática.
A regra de escoteiro:
"Sempre deixe o acampamento mais limpo do que você o encontrou."
Utilizando a mesma ideia mas trazendo para a programação:
"Sempre deixe o código mais limpo do que você o encontrou."
Seguindo as práticas de nomenclatura descritiva, funções com responsabilidade única, tratamento de erros eficaz, evitando condicionais excessivos e outras orientações apresentadas neste artigo, você pode criar código mais legível, de fácil manutenção e menos propenso a erros.
Ao adotar essas boas práticas, você não apenas melhora a qualidade de seu código, mas também torna o desenvolvimento e a manutenção de software uma experiência mais eficiente e satisfatória. Lembre-se de que o código limpo é um investimento no presente e no futuro de seu projeto.
No entanto, não há uma abordagem única e universal para a escrita de código. Às vezes, pode haver situações em que as práticas mencionadas precisam ser ajustadas às necessidades específicas do projeto. Essas diretrizes fornecem um alicerce sólido para aprimorar a qualidade do seu código e, ao mesmo tempo, tornar sua vida como desenvolvedor mais tranquila.
Referências
- Livro: Clean Code - Uncle Bob
- Clean code javascript
- Airbnb JavaScript Style Guide
- 3rs-of-software-architecture
- Felipe Augusto