En mis primeros años como desarrollador front-end, formaba parte de un equipo de desarrolladores en el que los Tech Leads estaban muy preocupados por "limpiar" el código.
En retrospectiva, entiendo lo que me pedían: que mi código tenía que ser muy descriptivo para que cualquiera pudiera entenderlo. Si alguien revisaba mi código, debía darse cuenta rápida y fácilmente de cómo resolvíamos las historias de usuario del cliente. El resultado final sería un código legible y mantenible.
¿Y cómo podríamos conseguirlo? Bueno, no es tan complicado y los Tech Leads tenían razón, necesitábamos código limpio. Desde entonces he tenido mentores que me han ayudado mucho, y me han ayudado a destapar un deseo de aprendizaje continuo que continúa hasta el día de hoy utilizando ciertos cursos y libros. Y, el concepto de escribir código limpio se me ha quedado grabado.
En este post, me centro en algunos puntos que debemos tener en cuenta a la hora de escribir código limpio, centrándome en TypeScript.
Recuerde, el "mal código" o código espagueti no significa que el código no es utilizable. Pero como programadores, creo que debemos producir software reutilizable, legible y refactorizable.
No añadas contexto innecesario
Si el nombre de tu clase/tipo/objeto
te dice algo, no lo repitas en el nombre de tu variable.
Deberías evitarlo:
type Car = {
carMake: string;
carModel: string;
carColor: string;
}
function print(car: Car): void {
console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}
Haz esto en su lugar:
type Car = {
make: string;
model: string;
color: string;
}
function print(car: Car): void {
console.log(`${car.make} ${car.model} (${car.color})`);
}
Utilizar enum
Los enums pueden ayudarte a documentar la intención del código. Por ejemplo cuando nos preocupa que los valores sean diferentes en lugar del valor exacto de los mismos.
Debe evitarse:
const GENRE = {
ROMANTIC: 'romantic',
DRAMA: 'drama',
COMEDY: 'comedy',
DOCUMENTARY: 'documentary',
}
projector.configureFilm(GENRE.COMEDY);
class Projector {
// declaration of Projector
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// some logic to be executed
}
}
}
Haz esto en su lugar:
enum GENRE {
ROMANTIC,
DRAMA,
COMEDY,
DOCUMENTARY,
}
projector.configureFilm(GENRE.COMEDY);
class Projector {
// declaration of Projector
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// some logic to be executed
}
}
Los nombres de las funciones deben decir lo que hacen
Deben evitarse:
function addToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);
Podrías hacer esto
function addMonthToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
addMonthToDate(date, 1);
Preferir la programación funcional a la imperativa
Debería evitar:
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < contributions.length; i++) {
totalOutput += contributions[i].linesOfCode;
}
Una mejor práctica podría ser
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = contributions
.reduce((totalLines, output) => totalLines + output.linesOfCode, 0);
Evitar los condicionales negativos
Se deben evitar:
function isEmailNotUsed(email: string): boolean {
// ...
}
if (isEmailNotUsed(email)) {
// ...
}
Se podría sustituir por
function isEmailUsed(email: string): boolean {
// ...
}
if (!isEmailUsed(email)) {
// ...
}
Prefiero la inmutabilidad
Debería evitar:
interface Config {
host: string;
port: string;
db: string;
}
Se puede modificar a
interface Config {
readonly host: string;
readonly port: string;
readonly db: string;
}
tipo vs. interfaz (Type vs Interfaces)
Utilices un tipo cuando necesite una unión o intersección. Utiliza una interfaz cuando quieras extends
o implements
. No hay una regla estricta, sin embargo, utiliza la que más te convenga.
Debe evitarse:
interface EmailConfig {
// ...
}
interface DbConfig {
// ...
}
interface Config {
// ...
}
//...
type Shape = {
// ...
}
Haz esto en su lugar:
type EmailConfig = {
// ...
}
type DbConfig = {
// ...
}
type Config = EmailConfig | DbConfig;
// ...
interface Shape {
// ...
}
class Circle implements Shape {
// ...
}
class Square implements Shape {
// ...
}
Un único concepto por prueba
Las pruebas también deben seguir el principio de responsabilidad única. Hacer una sola afirmación por prueba unitaria.
Debe evitarse:
import { assert } from 'chai';
describe('AwesomeDate', () => {
it('handles date boundaries', () => {
let date: AwesomeDate;
date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));
date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));
date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});
Una mejor práctica podría ser
import { assert } from 'chai';
describe('AwesomeDate', () => {
it('handles 30-day months', () => {
const date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));
});
it('handles leap year', () => {
const date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));
});
it('handles non-leap year', () => {
const date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});
Conclusión
Hay toneladas de cosas que decir sobre el código limpio, un artículo no es suficiente, sin embargo; es de suma importancia ir cada día mejorando.