1052 lines
30 KiB
Markdown
1052 lines
30 KiB
Markdown
# Prácticas de código limpio (Clean Code)
|
|
|
|
> Parte de esta guía se basa en conceptos del resumen del curso **"Clean Code"** de [Academind GmbH / Maximilian Schwarzmüller](https://academind.com), © 2020.
|
|
|
|
Este documento desarrolla las prácticas de código limpio definidas en el `SKILL.md` principal. Sus recomendaciones deben aplicarse con criterio profesional, priorizando la corrección, claridad, simplicidad y mantenibilidad del software.
|
|
|
|
Las métricas y reglas estructurales descritas a continuación son orientaciones de diseño, salvo que las convenciones específicas del proyecto establezcan requisitos más estrictos.
|
|
|
|
## 1. ¿Qué es el código limpio?
|
|
|
|
Es código que resulta:
|
|
|
|
- **Fácil de comprender:** expresa claramente su intención.
|
|
- **Fácil de modificar:** permite realizar cambios localizados y controlados.
|
|
- **Fácil de probar:** facilita verificar su comportamiento y controlar sus dependencias.
|
|
- **Sencillo:** evita complejidad, abstracciones y mecanismos innecesarios.
|
|
- **Mantenible:** facilita corregir errores y evolucionar funcionalidades sin introducir riesgos desproporcionados.
|
|
|
|
El código limpio no es necesariamente el más corto ni el que utiliza más patrones de diseño.
|
|
|
|
**El objetivo es minimizar la complejidad global del sistema, no optimizar métricas aisladas.**
|
|
|
|
## 2. Enfoque centrado en las personas
|
|
|
|
El software tiene tres grupos principales de destinatarios:
|
|
|
|
1. **Usuarios:** necesitan que el producto satisfaga sus necesidades.
|
|
2. **Clientes:** esperan obtener valor económico, operativo o estratégico.
|
|
3. **Desarrolladores:** deben comprender, mantener y evolucionar el software.
|
|
|
|
El código suele leerse muchas más veces de las que se escribe.
|
|
|
|
Por tanto, debe favorecer la comprensión y el mantenimiento sin perjudicar las necesidades funcionales del producto.
|
|
|
|
La calidad debe evaluarse considerando tanto el comportamiento observable como el coste de evolución del sistema.
|
|
|
|
---
|
|
|
|
## 3. Principios de nomenclatura
|
|
|
|
### 3.1. Consistencia y unicidad
|
|
|
|
**Prioridad máxima: un mismo concepto debe tener un nombre coherente dentro de su contexto.**
|
|
|
|
Evita utilizar términos diferentes para representar exactamente la misma operación o responsabilidad.
|
|
|
|
```typescript
|
|
// MAL: Nombres inconsistentes para una misma operación.
|
|
getUserById(id);
|
|
fetchCustomerById(id);
|
|
retrieveClientById(id);
|
|
|
|
// BIEN: Convención coherente para operaciones equivalentes.
|
|
getUser(id);
|
|
getOrder(id);
|
|
getProduct(id);
|
|
```
|
|
|
|
No obstante, distingue correctamente los conceptos del dominio. Por ejemplo, `User`, `Customer` y `Client` no deben tratarse como sinónimos si representan entidades diferentes.
|
|
|
|
Mantén las convenciones existentes de un módulo cuando sean coherentes y no contradigan requisitos superiores.
|
|
|
|
### 3.2. Comprensibilidad
|
|
|
|
Utiliza nombres que expresen el significado del dominio.
|
|
|
|
```typescript
|
|
// MAL: Nombre genérico.
|
|
const arr = users.filter((user) => user.isActive);
|
|
|
|
// BIEN: Expresa claramente el resultado.
|
|
const activeUsers = users.filter((user) => user.isActive);
|
|
```
|
|
|
|
No utilices terminología de negocio incorrecta únicamente para evitar términos técnicos.
|
|
|
|
### 3.3. Especificidad
|
|
|
|
Evita nombres ambiguos como `data`, `info`, `manager`, `handler`, `processor` o `utils` cuando no permitan identificar la responsabilidad real.
|
|
|
|
```typescript
|
|
// MAL: Responsabilidades imprecisas.
|
|
class DataManager {}
|
|
|
|
function processInfo(data: unknown): void {}
|
|
|
|
// BIEN: Responsabilidades identificables.
|
|
class OrderRepository {}
|
|
|
|
function validatePayment(payment: Payment): void {}
|
|
```
|
|
|
|
Estos términos no están prohibidos. Pueden resultar adecuados cuando describan correctamente el concepto, como `EventHandler` o `ImageProcessor`.
|
|
|
|
### 3.4. Brevedad sin perder claridad
|
|
|
|
Los nombres deben ser concisos, pero suficientemente descriptivos.
|
|
|
|
```typescript
|
|
// MAL: Abreviaturas crípticas.
|
|
const usrLst = getUsrs();
|
|
|
|
// MAL: Nombre innecesariamente largo.
|
|
const listOfAllActiveUsersInTheSystem = getActiveUsers();
|
|
|
|
// BIEN: Breve y claro.
|
|
const activeUsers = getActiveUsers();
|
|
```
|
|
|
|
### 3.5. Facilidad de búsqueda
|
|
|
|
Utiliza nombres suficientemente específicos para facilitar su localización.
|
|
|
|
```typescript
|
|
// MAL: Nombre demasiado genérico.
|
|
const data = fetch();
|
|
|
|
// BIEN: Identifica el contenido.
|
|
const orderSummary = fetchOrderSummary();
|
|
```
|
|
|
|
### 3.6. Facilidad de pronunciación
|
|
|
|
Los identificadores deben poder comunicarse con claridad entre desarrolladores.
|
|
|
|
```typescript
|
|
// MAL: Abreviatura difícil de interpretar.
|
|
const genymdhms = generateTimestamp();
|
|
|
|
// BIEN: Concepto comprensible.
|
|
const timestamp = generateTimestamp();
|
|
```
|
|
|
|
### 3.7. Evitar términos redundantes
|
|
|
|
No añadas palabras que repitan información evidente.
|
|
|
|
```typescript
|
|
// MAL: Redundancia innecesaria.
|
|
const userData = user;
|
|
class UserClass {}
|
|
|
|
// BIEN: Nombres directos.
|
|
const currentUser = user;
|
|
class User {}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Funciones y métodos
|
|
|
|
Las funciones deben ser comprensibles, cohesionadas y expresar una responsabilidad concreta.
|
|
|
|
### 4.1. Minimizar los parámetros
|
|
|
|
Un número reducido de parámetros suele facilitar la lectura y el uso de una función.
|
|
|
|
Sin embargo, **no existe un número máximo universalmente correcto**.
|
|
|
|
```typescript
|
|
// Operación sin parámetros.
|
|
createSession();
|
|
|
|
// Operación con un parámetro.
|
|
isValidEmail(email);
|
|
|
|
// Operación con dos parámetros.
|
|
calculateTotal(price, quantity);
|
|
|
|
// Operación con varios parámetros relacionados.
|
|
createUser({
|
|
firstName: 'Max',
|
|
lastName: 'Smith',
|
|
email: 'test@example.com',
|
|
role: 'editor',
|
|
age: 31,
|
|
});
|
|
```
|
|
|
|
Agrupa parámetros cuando formen una unidad conceptual o mejoren la legibilidad.
|
|
|
|
```typescript
|
|
interface Rectangle {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
function createRectangle(rectangle: Rectangle): void {
|
|
// Crear el rectángulo.
|
|
}
|
|
|
|
createRectangle({
|
|
x: 10,
|
|
y: 9,
|
|
width: 30,
|
|
height: 12,
|
|
});
|
|
```
|
|
|
|
No crees objetos de parámetros innecesarios para funciones sencillas con dos o tres argumentos claros.
|
|
|
|
### 4.2. Mantener las funciones pequeñas y cohesionadas
|
|
|
|
Una función debe tener una responsabilidad identificable.
|
|
|
|
Evita mezclar validaciones, acceso a datos, decisiones de negocio y efectos externos cuando exista una separación clara y útil.
|
|
|
|
```typescript
|
|
// MAL: Mezcla responsabilidades y compara contraseñas directamente.
|
|
async function login(email: string, password: string): Promise<void> {
|
|
if (!email.includes('@')) {
|
|
throw new Error('Correo electrónico no válido');
|
|
}
|
|
|
|
const user = await database.findUserByEmail(email);
|
|
|
|
if (!user || user.password === password) {
|
|
throw new Error('Credenciales no válidas');
|
|
}
|
|
|
|
await sessionService.create(user.id);
|
|
}
|
|
```
|
|
|
|
Además de mezclar responsabilidades, el ejemplo anterior contiene un error lógico en la comprobación de las credenciales y presupone una comparación directa de contraseñas.
|
|
|
|
Una implementación más clara delega las operaciones específicas:
|
|
|
|
```typescript
|
|
async function login(
|
|
email: string,
|
|
password: string,
|
|
): Promise<void> {
|
|
validateLoginInput(email, password);
|
|
|
|
const user = await findUserByEmail(email);
|
|
|
|
if (!user) {
|
|
throw new Error('Credenciales no válidas');
|
|
}
|
|
|
|
await verifyCredentials(user, password);
|
|
await createUserSession(user.id);
|
|
}
|
|
```
|
|
|
|
Este ejemplo presupone que `findUserByEmail` devuelve `Promise<User | null>` y que `verifyCredentials` rechaza la promesa cuando la contraseña no es válida. Solo se crea la sesión después de verificar las credenciales. Adapta el error genérico a la política del proyecto sin filtrar si una cuenta existe.
|
|
|
|
La verificación de credenciales debe utilizar el mecanismo seguro establecido por el proyecto.
|
|
|
|
**No extraigas funciones únicamente para reducir líneas. Cada extracción debe aportar significado, reutilización o una separación útil de responsabilidades.**
|
|
|
|
### 4.3. Un nivel de abstracción coherente
|
|
|
|
Las operaciones dentro de una función deben mantener un nivel de abstracción razonablemente uniforme.
|
|
|
|
```typescript
|
|
// MAL: Mezcla configuración técnica y operación de negocio.
|
|
async function printDocument(documentPath: string): Promise<void> {
|
|
const options = { encoding: 'utf8' as const };
|
|
const content = await fileSystem.readFile(documentPath, options);
|
|
|
|
const printer = new Printer('pdf');
|
|
|
|
await printer.print(content);
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// BIEN: Las operaciones expresan la intención.
|
|
async function printDocument(documentPath: string): Promise<void> {
|
|
const document = await readDocument(documentPath);
|
|
|
|
await printPdfDocument(document);
|
|
}
|
|
```
|
|
|
|
No es necesario crear una función auxiliar para cada instrucción. El objetivo es evitar que los detalles técnicos oculten el flujo principal.
|
|
|
|
### 4.4. Dividir funciones con criterio
|
|
|
|
Evita las extracciones artificiales.
|
|
|
|
**Señales de una división innecesaria:**
|
|
|
|
1. La función extraída únicamente cambia el nombre de una operación trivial.
|
|
2. Comprender el flujo requiere navegar entre demasiadas funciones.
|
|
3. No existe un nombre significativo para la nueva función.
|
|
|
|
```typescript
|
|
// EXCESIVAMENTE FRAGMENTADO.
|
|
async function createUser(email: string): Promise<void> {
|
|
await saveUser(email);
|
|
}
|
|
|
|
async function saveUser(email: string): Promise<void> {
|
|
const user = buildUser(email);
|
|
await userRepository.save(user);
|
|
}
|
|
|
|
function buildUser(email: string): User {
|
|
return new User(email);
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// MÁS DIRECTO.
|
|
async function createUser(email: string): Promise<void> {
|
|
const user = new User(email);
|
|
|
|
await userRepository.save(user);
|
|
}
|
|
```
|
|
|
|
Extrae código cuando:
|
|
|
|
- Represente una responsabilidad diferenciada.
|
|
- Oculte complejidad técnica significativa.
|
|
- Exprese una operación relevante del dominio.
|
|
- Facilite las pruebas o el mantenimiento.
|
|
- Permita reutilizar comportamiento sin crear acoplamientos artificiales.
|
|
|
|
### 4.5. Evitar efectos secundarios inesperados
|
|
|
|
Los efectos secundarios deben ser coherentes con el nombre y el contrato de la operación.
|
|
|
|
```typescript
|
|
// MAL: Una validación crea una sesión.
|
|
async function validateCredentials(
|
|
email: string,
|
|
password: string,
|
|
): Promise<void> {
|
|
await verifyCredentials(email, password);
|
|
await createSession();
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// BIEN: La operación expresa que inicia una sesión.
|
|
async function authenticateAndStartSession(
|
|
email: string,
|
|
password: string,
|
|
): Promise<void> {
|
|
await verifyCredentials(email, password);
|
|
await createSession();
|
|
}
|
|
```
|
|
|
|
Cuando sea posible, separa consultas, validaciones y modificaciones de estado.
|
|
|
|
No obstante, una operación puede combinar responsabilidades técnicas necesarias para satisfacer un caso de uso, siempre que su contrato sea claro.
|
|
|
|
---
|
|
|
|
## 5. Estructuras de control
|
|
|
|
### 5.1. Priorizar condiciones comprensibles
|
|
|
|
Las condiciones positivas suelen resultar más directas, pero no deben imponerse universalmente.
|
|
|
|
```typescript
|
|
// Claro: expresa directamente la condición.
|
|
if (isEmpty(content)) {
|
|
throw new Error('El contenido está vacío');
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// También correcto: negación natural del estado esperado.
|
|
if (!isOpen(transaction)) {
|
|
throw new Error('La transacción no está abierta');
|
|
}
|
|
```
|
|
|
|
Selecciona la expresión que mejor comunique la intención.
|
|
|
|
### 5.2. Evitar el anidamiento excesivo
|
|
|
|
El anidamiento profundo dificulta seguir el flujo de ejecución.
|
|
|
|
Utiliza cláusulas de guarda y retornos anticipados cuando mejoren la claridad.
|
|
|
|
```typescript
|
|
// MAL: Anidamiento innecesario.
|
|
async function messageUser(
|
|
user: User | null,
|
|
message: string,
|
|
): Promise<void> {
|
|
if (user) {
|
|
if (message) {
|
|
if (user.acceptsMessages) {
|
|
const success = await user.sendMessage(message);
|
|
|
|
if (success) {
|
|
console.log('Mensaje enviado');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// BIEN: Cláusula de guarda.
|
|
async function messageUser(
|
|
user: User | null,
|
|
message: string,
|
|
): Promise<void> {
|
|
if (!user || !message || !user.acceptsMessages) {
|
|
return;
|
|
}
|
|
|
|
const success = await user.sendMessage(message);
|
|
|
|
if (success) {
|
|
console.log('Mensaje enviado');
|
|
}
|
|
}
|
|
```
|
|
|
|
El objetivo es mejorar la lectura sin alterar el comportamiento.
|
|
|
|
### 5.3. Extraer estructuras de control complejas
|
|
|
|
Extrae una estructura de control cuando represente un comportamiento independiente.
|
|
|
|
```typescript
|
|
async function connectDatabase(uri: string): Promise<Connection> {
|
|
validateDatabaseUri(uri);
|
|
|
|
try {
|
|
return await connectPrimaryDatabase(uri);
|
|
} catch (error) {
|
|
if (!isRecoverableConnectionError(error)) {
|
|
throw error;
|
|
}
|
|
|
|
return connectFallbackDatabase(uri);
|
|
}
|
|
}
|
|
```
|
|
|
|
Este ejemplo presupone que el sistema admite una conexión alternativa.
|
|
|
|
La recuperación ante errores debe responder a una necesidad real y no ocultar fallos de configuración o seguridad.
|
|
|
|
No introduzcas funciones auxiliares cuando el condicional sea más claro directamente.
|
|
|
|
### 5.4. Utilizar polimorfismo cuando aporte valor
|
|
|
|
Los condicionales repetidos pueden indicar que existe una variación de comportamiento.
|
|
|
|
Sin embargo, no todo `if` o `switch` necesita sustituirse por polimorfismo.
|
|
|
|
```typescript
|
|
interface PaymentProcessor {
|
|
processPayment(transaction: Transaction): Promise<void>;
|
|
processRefund(transaction: Transaction): Promise<void>;
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
class CreditCardProcessor implements PaymentProcessor {
|
|
async processPayment(transaction: Transaction): Promise<void> {
|
|
await processCreditCardPayment(transaction);
|
|
}
|
|
|
|
async processRefund(transaction: Transaction): Promise<void> {
|
|
await processCreditCardRefund(transaction);
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
class PayPalProcessor implements PaymentProcessor {
|
|
async processPayment(transaction: Transaction): Promise<void> {
|
|
await processPayPalPayment(transaction);
|
|
}
|
|
|
|
async processRefund(transaction: Transaction): Promise<void> {
|
|
await processPayPalRefund(transaction);
|
|
}
|
|
}
|
|
```
|
|
|
|
Una factoría o registro puede seleccionar el procesador adecuado según el método de pago.
|
|
|
|
Este enfoque resulta útil cuando hay varias implementaciones, comportamiento significativo o posibilidades reales de extensión.
|
|
|
|
Para dos alternativas sencillas y estables, un condicional puede ser más claro.
|
|
|
|
### 5.5. Gestión de errores
|
|
|
|
Utiliza excepciones para fallos excepcionales y resultados tipados cuando representen mejor errores de negocio esperados.
|
|
|
|
**Ejemplo con excepciones:**
|
|
|
|
```typescript
|
|
async function createUser(email: string): Promise<User> {
|
|
validateEmail(email);
|
|
|
|
const existingUser = await userRepository.findByEmail(email);
|
|
|
|
if (existingUser) {
|
|
throw new EmailAlreadyExistsError(email);
|
|
}
|
|
|
|
return userRepository.save(new User(email));
|
|
}
|
|
```
|
|
|
|
**Ejemplo con resultados tipados:**
|
|
|
|
```typescript
|
|
type CreateUserResult =
|
|
| { success: true; user: User }
|
|
| { success: false; reason: 'EMAIL_ALREADY_EXISTS' };
|
|
```
|
|
|
|
Ambos enfoques pueden ser correctos según la arquitectura.
|
|
|
|
Evita:
|
|
|
|
- Capturar excepciones y descartarlas silenciosamente.
|
|
- Usar excepciones indiscriminadamente como flujo normal.
|
|
- Devolver códigos ambiguos sin significado.
|
|
- Registrar información sensible.
|
|
- Ocultar el motivo real de un fallo.
|
|
|
|
---
|
|
|
|
## 6. Clases y objetos
|
|
|
|
### 6.1. Objetos frente a contenedores de datos
|
|
|
|
Distingue entre:
|
|
|
|
**Objetos:** encapsulan estado y exponen comportamiento.
|
|
|
|
**Contenedores de datos:** representan y transportan información.
|
|
|
|
```typescript
|
|
// Contenedor de datos.
|
|
interface UserData {
|
|
name: string;
|
|
age: number;
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// Objeto que encapsula comportamiento.
|
|
class User {
|
|
constructor(
|
|
private readonly name: string,
|
|
private readonly age: number,
|
|
) {}
|
|
|
|
greet(): string {
|
|
return `Hola, soy ${this.name} y tengo ${this.age} años.`;
|
|
}
|
|
}
|
|
```
|
|
|
|
Ambos diseños son válidos.
|
|
|
|
Utiliza estructuras de datos para DTO, respuestas de API, modelos de presentación y otros escenarios donde exponer información sea su finalidad.
|
|
|
|
Utiliza objetos con comportamiento cuando sea necesario proteger invariantes o encapsular reglas del dominio.
|
|
|
|
Evita mezclar responsabilidades sin una justificación clara, pero no prohíbas que una entidad exponga datos necesarios mediante métodos o propiedades controladas.
|
|
|
|
### 6.2. Cohesión de clases
|
|
|
|
La cohesión describe hasta qué punto los elementos de una clase colaboran para cumplir una responsabilidad común.
|
|
|
|
Una clase cohesionada:
|
|
|
|
- Tiene una finalidad reconocible.
|
|
- Agrupa operaciones relacionadas.
|
|
- Mantiene un estado coherente.
|
|
- Presenta motivos de cambio relacionados.
|
|
|
|
Una cohesión aparentemente baja puede indicar responsabilidades mezcladas, pero no debe evaluarse únicamente contando cuántos atributos utiliza cada método.
|
|
|
|
Las clases sin estado, como determinados servicios, también pueden tener una cohesión elevada.
|
|
|
|
**Revisa las clases cuyo comportamiento no pueda describirse mediante una responsabilidad clara.**
|
|
|
|
---
|
|
|
|
## 7. Object Calisthenics: nueve heurísticas de diseño
|
|
|
|
*Object Calisthenics* es un conjunto de ejercicios orientados a mejorar el diseño orientado a objetos.
|
|
|
|
Sus reglas pueden aplicarse estrictamente durante ejercicios de aprendizaje, pero **no deben convertirse automáticamente en restricciones obligatorias de producción**.
|
|
|
|
### 7.1. Reducir los niveles de indentación
|
|
|
|
**Regla original:** un nivel de indentación por método.
|
|
|
|
**Aplicación recomendada:** mantener los flujos comprensibles y reducir el anidamiento cuando resulte excesivo.
|
|
|
|
```typescript
|
|
function processOrders(orders: Order[]): void {
|
|
for (const order of orders) {
|
|
if (!order.isValid()) {
|
|
continue;
|
|
}
|
|
|
|
processOrder(order);
|
|
}
|
|
}
|
|
|
|
function processOrder(order: Order): void {
|
|
for (const item of order.items) {
|
|
if (!item.inStock) {
|
|
continue;
|
|
}
|
|
|
|
processItem(item);
|
|
}
|
|
}
|
|
```
|
|
|
|
No es necesario eliminar todos los niveles de anidamiento si la alternativa perjudica la claridad.
|
|
|
|
### 7.2. Evitar `else` cuando sea innecesario
|
|
|
|
**Regla original:** no utilizar `else`.
|
|
|
|
**Aplicación recomendada:** preferir retornos anticipados cuando simplifiquen el flujo.
|
|
|
|
```typescript
|
|
function getDiscount(user: User): number {
|
|
if (user.isPremium) {
|
|
return 20;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
Utilizar `else` sigue siendo válido cuando representa claramente dos ramas complementarias.
|
|
|
|
No introduzcas funciones auxiliares o lógica artificial únicamente para eliminar esta palabra clave.
|
|
|
|
### 7.3. Encapsular primitivas con significado de dominio
|
|
|
|
**Regla original:** encapsular todos los valores primitivos.
|
|
|
|
**Aplicación recomendada:** utilizar objetos de valor cuando aporten validación, invariantes, operaciones o semántica.
|
|
|
|
```typescript
|
|
class Email {
|
|
constructor(readonly value: string) {
|
|
if (!isValidEmail(value)) {
|
|
throw new InvalidEmailError();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
class Age {
|
|
constructor(readonly value: number) {
|
|
if (!Number.isInteger(value) || value < 0 || value > 150) {
|
|
throw new InvalidAgeError();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
function createUser(email: Email, age: Age): User {
|
|
return new User(email, age);
|
|
}
|
|
```
|
|
|
|
No encapsules todos los números, cadenas o booleanos de forma indiscriminada.
|
|
|
|
En TypeScript, considera *branded types* cuando únicamente necesites diferenciar identificadores.
|
|
|
|
### 7.4. Colecciones de primera clase
|
|
|
|
**Regla original:** una clase que contenga una colección no debe tener otros atributos.
|
|
|
|
**Aplicación recomendada:** encapsular colecciones cuando tengan comportamiento o invariantes propios.
|
|
|
|
```typescript
|
|
class OrderItems {
|
|
private readonly items: OrderItem[];
|
|
|
|
constructor(items: readonly OrderItem[] = []) {
|
|
this.items = [...items];
|
|
}
|
|
|
|
add(item: OrderItem): void {
|
|
this.items.push(item);
|
|
}
|
|
|
|
total(): Money {
|
|
return calculateOrderTotal(this.items);
|
|
}
|
|
|
|
isEmpty(): boolean {
|
|
return this.items.length === 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
class Order {
|
|
constructor(
|
|
readonly id: OrderId,
|
|
private readonly items: OrderItems,
|
|
) {}
|
|
}
|
|
```
|
|
|
|
La copia del constructor protege la estructura del array frente a cambios a través de la referencia original. Es superficial: si los elementos contienen estado mutable que pueda comprometer invariantes, define también su propiedad, inmutabilidad o copia adecuada. `readonly` no convierte automáticamente los objetos anidados en inmutables.
|
|
|
|
Utiliza arrays y colecciones nativas cuando sean suficientes.
|
|
|
|
### 7.5. Ley de Deméter
|
|
|
|
**Regla original:** un único punto por línea.
|
|
|
|
**Aplicación recomendada:** evitar que un componente dependa de los detalles internos de estructuras ajenas.
|
|
|
|
```typescript
|
|
// Acoplamiento a la estructura interna.
|
|
const city = order.customer.address.city;
|
|
```
|
|
|
|
```typescript
|
|
// El dominio proporciona la operación necesaria.
|
|
const city = order.getShippingCity();
|
|
```
|
|
|
|
El segundo enfoque resulta adecuado cuando obtener la ciudad de envío constituye una responsabilidad del pedido.
|
|
|
|
No obstante, acceder a datos públicos de DTO o utilizar APIs fluidas puede ser perfectamente legítimo.
|
|
|
|
### 7.6. Evitar abreviaturas innecesarias
|
|
|
|
**Regla original:** no utilizar abreviaturas.
|
|
|
|
**Aplicación recomendada:** evitar abreviaturas ambiguas y mantener las ampliamente reconocidas.
|
|
|
|
```typescript
|
|
// MAL.
|
|
const custRepo = new CustRepo();
|
|
const ord = new Ord();
|
|
```
|
|
|
|
```typescript
|
|
// BIEN.
|
|
const customerRepository = new CustomerRepository();
|
|
const order = new Order();
|
|
```
|
|
|
|
Abreviaturas habituales como `id`, `url`, `api` o `http` pueden utilizarse cuando sean coherentes con las convenciones del proyecto.
|
|
|
|
### 7.7. Mantener entidades pequeñas
|
|
|
|
**Regla original:**
|
|
|
|
- Clases de menos de 50 líneas.
|
|
- Métodos de menos de 10 líneas.
|
|
- Archivos de menos de 100 líneas.
|
|
|
|
**Aplicación recomendada:**
|
|
|
|
- Revisa los métodos extensos cuando acumulen complejidad o responsabilidades.
|
|
- Como orientación, presta atención a los métodos que superen aproximadamente 10-20 líneas.
|
|
- Revisa las clases grandes cuando mezclen responsabilidades independientes.
|
|
- Organiza los archivos para facilitar su navegación.
|
|
- No establezcas límites absolutos de líneas como criterio de calidad.
|
|
|
|
Una implementación clara de 25 líneas puede ser preferible a cinco funciones artificiales de cinco líneas.
|
|
|
|
### 7.8. Limitar el estado de las clases
|
|
|
|
**Regla original:** no utilizar más de dos variables de instancia.
|
|
|
|
**Aplicación recomendada:** mantener un estado coherente con la responsabilidad de la clase.
|
|
|
|
```typescript
|
|
class Order {
|
|
constructor(
|
|
readonly id: OrderId,
|
|
readonly customerId: CustomerId,
|
|
readonly items: OrderItems,
|
|
readonly status: OrderStatus,
|
|
) {}
|
|
}
|
|
```
|
|
|
|
Esta clase puede ser correcta aunque tenga cuatro atributos.
|
|
|
|
Evalúa si el estado pertenece al mismo concepto, no si supera un límite numérico.
|
|
|
|
Extrae objetos cuando representen subconceptos reales y no únicamente para reducir el número de propiedades.
|
|
|
|
### 7.9. Favorecer comportamiento frente a acceso indiscriminado al estado
|
|
|
|
**Regla original:** no utilizar getters, setters ni propiedades públicas.
|
|
|
|
**Aplicación recomendada:** encapsular las operaciones que deban preservar invariantes.
|
|
|
|
```typescript
|
|
// Riesgo: La lógica de retirada queda fuera del objeto.
|
|
if (account.balance >= amount) {
|
|
account.balance -= amount;
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// Mejor: La cuenta controla sus reglas.
|
|
class Account {
|
|
constructor(private balance: Money) {
|
|
if (balance.isNegative()) {
|
|
throw new InvalidInitialBalanceError();
|
|
}
|
|
}
|
|
|
|
withdraw(amount: Money): WithdrawResult {
|
|
if (!amount.isPositive()) {
|
|
return WithdrawResult.invalidAmount();
|
|
}
|
|
|
|
if (!this.balance.isGreaterThanOrEqualTo(amount)) {
|
|
return WithdrawResult.insufficientFunds();
|
|
}
|
|
|
|
this.balance = this.balance.subtract(amount);
|
|
|
|
return WithdrawResult.success();
|
|
}
|
|
}
|
|
```
|
|
|
|
El ejemplo presupone un `Money` inmutable con comparación y resta entre monedas compatibles, y los resultados `success`, `invalidAmount` e `insufficientFunds`. Mantiene las mismas comprobaciones de importe y saldo inicial que `BankAccount` en [diseño-de-objetos.md](diseño-de-objetos.md). Si falla una validación o una operación monetaria, no debe modificarse el saldo.
|
|
|
|
Los getters y propiedades de solo lectura siguen siendo adecuados cuando exponen información sin comprometer invariantes.
|
|
|
|
Evita setters que permitan introducir estados inválidos.
|
|
|
|
---
|
|
|
|
## 8. Comentarios
|
|
|
|
### 8.1. Explicar decisiones y motivos
|
|
|
|
Los comentarios deben aportar información que no resulte evidente al leer el código.
|
|
|
|
Prioriza explicaciones sobre:
|
|
|
|
- Decisiones de diseño.
|
|
- Restricciones de negocio.
|
|
- Compatibilidad.
|
|
- Riesgos.
|
|
- Contratos.
|
|
- Comportamientos inesperados.
|
|
- Soluciones temporales justificadas.
|
|
|
|
```typescript
|
|
// MAL: Repite la operación.
|
|
counter++;
|
|
```
|
|
|
|
```typescript
|
|
// BIEN: Explica una particularidad relevante.
|
|
// La API heredada utiliza índices basados en cero.
|
|
const displayIndex = apiIndex + 1;
|
|
```
|
|
|
|
### 8.2. Preferir código expresivo
|
|
|
|
Cuando sea posible, utiliza nombres y abstracciones claras en lugar de comentarios que expliquen condiciones complejas.
|
|
|
|
```typescript
|
|
// MENOS CLARO.
|
|
if (user.subscriptionLevel >= 2 && !user.isBanned) {
|
|
// Permitir acceso a funciones premium.
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// MÁS CLARO.
|
|
if (user.canAccessPremiumFeatures()) {
|
|
enablePremiumFeatures();
|
|
}
|
|
```
|
|
|
|
No obstante, la documentación pública, los comentarios sobre decisiones técnicas y las explicaciones de contratos pueden ser necesarios incluso con código bien nombrado.
|
|
|
|
### 8.3. Comentarios que deben evitarse
|
|
|
|
- Comentarios redundantes.
|
|
- Explicaciones que repiten literalmente el código.
|
|
- Comentarios obsoletos.
|
|
- Información que debería expresarse mediante tipos o nombres.
|
|
- Código comentado que ya no se utiliza.
|
|
|
|
Los comentarios `TODO` deben identificar tareas concretas y, cuando el proyecto lo requiera, enlazar con su seguimiento correspondiente.
|
|
|
|
---
|
|
|
|
## 9. Formato y organización
|
|
|
|
### 9.1. Organización vertical
|
|
|
|
- Mantén juntos los elementos relacionados.
|
|
- Separa conceptos mediante líneas vacías cuando mejore la legibilidad.
|
|
- Agrupa propiedades, métodos y firmas de sobrecarga relacionadas.
|
|
- Mantén una organización coherente entre archivos.
|
|
- Sitúa las operaciones públicas y los detalles internos según las convenciones del proyecto.
|
|
|
|
### 9.2. Organización horizontal
|
|
|
|
- Utiliza una indentación consistente.
|
|
- Mantén espacios coherentes alrededor de operadores.
|
|
- Evita líneas innecesariamente extensas.
|
|
- Como orientación, considera longitudes aproximadas de 80-120 caracteres.
|
|
- Respeta la configuración del formateador oficial del proyecto.
|
|
|
|
No introduzcas modificaciones de formato ajenas a la tarea.
|
|
|
|
### 9.3. Organización narrativa
|
|
|
|
Cuando resulte apropiado, estructura el código desde operaciones de alto nivel hacia detalles de implementación.
|
|
|
|
```typescript
|
|
class OrderProcessor {
|
|
async process(order: Order): Promise<ProcessResult> {
|
|
this.validate(order);
|
|
|
|
const total = this.calculateTotal(order);
|
|
|
|
return this.save(order, total);
|
|
}
|
|
|
|
private validate(order: Order): void {
|
|
// Validar invariantes y requisitos.
|
|
}
|
|
|
|
private calculateTotal(order: Order): Money {
|
|
return order.calculateTotal();
|
|
}
|
|
|
|
private async save(
|
|
order: Order,
|
|
total: Money,
|
|
): Promise<ProcessResult> {
|
|
return this.repository.save(order, total);
|
|
}
|
|
}
|
|
```
|
|
|
|
El ejemplo ilustra la organización narrativa y presupone las dependencias y contratos correspondientes.
|
|
|
|
No es obligatorio colocar todos los métodos privados detrás de los públicos si las convenciones del proyecto establecen otra organización.
|
|
|
|
### 9.4. Convenciones del proyecto
|
|
|
|
Cuando el proyecto no establezca otra norma, aplica estas convenciones generales. Las preferencias adicionales y la prioridad de instrucciones se mantienen en el `SKILL.md` principal:
|
|
|
|
- Identificadores en inglés.
|
|
- Comentarios y documentación en castellano.
|
|
- Archivos y directorios en `kebab-case`.
|
|
- Clases, tipos e interfaces en `PascalCase`.
|
|
- Variables, funciones y métodos en `camelCase`.
|
|
- Constantes globales en `UPPER_SNAKE_CASE`.
|
|
- Llaves obligatorias en estructuras de control.
|
|
- Respeto de las convenciones existentes y de las herramientas de formato configuradas.
|
|
|
|
---
|
|
|
|
## 10. Lista de comprobación de código limpio
|
|
|
|
### Nomenclatura
|
|
|
|
- [ ] ¿Los identificadores expresan claramente su significado?
|
|
- [ ] ¿Utilizo términos coherentes con el dominio?
|
|
- [ ] ¿Los nombres son suficientemente específicos?
|
|
- [ ] ¿He evitado abreviaturas ambiguas?
|
|
- [ ] ¿Las variables booleanas expresan condiciones claras?
|
|
- [ ] ¿Se mantienen las convenciones del proyecto?
|
|
|
|
### Funciones y métodos
|
|
|
|
- [ ] ¿Cada función tiene una responsabilidad coherente?
|
|
- [ ] ¿Los parámetros son necesarios y comprensibles?
|
|
- [ ] ¿He agrupado parámetros cuando representan un concepto común?
|
|
- [ ] ¿Las operaciones mantienen un nivel de abstracción razonable?
|
|
- [ ] ¿He evitado dividir funciones artificialmente?
|
|
- [ ] ¿Los efectos secundarios son previsibles?
|
|
- [ ] ¿El código expresa claramente su intención?
|
|
|
|
### Estructuras de control
|
|
|
|
- [ ] ¿Las condiciones son comprensibles?
|
|
- [ ] ¿Puedo reducir el anidamiento sin complicar el código?
|
|
- [ ] ¿Las cláusulas de guarda aportan claridad?
|
|
- [ ] ¿Los condicionales complejos justifican una extracción?
|
|
- [ ] ¿He evitado introducir polimorfismo innecesario?
|
|
- [ ] ¿El tratamiento de errores es adecuado al contexto?
|
|
|
|
### Objetos y clases
|
|
|
|
- [ ] ¿La clase tiene una responsabilidad identificable?
|
|
- [ ] ¿Su estado es coherente con esa responsabilidad?
|
|
- [ ] ¿Las dependencias están justificadas?
|
|
- [ ] ¿Los objetos protegen correctamente sus invariantes?
|
|
- [ ] ¿Los contenedores de datos se utilizan cuando corresponde?
|
|
- [ ] ¿Los objetos de valor aportan semántica o seguridad real?
|
|
- [ ] ¿He evitado clases y abstracciones artificiales?
|
|
- [ ] ¿Se respetan los principios SOLID cuando son aplicables?
|
|
|
|
### Complejidad y mantenimiento
|
|
|
|
- [ ] ¿La solución es suficientemente sencilla?
|
|
- [ ] ¿Existe duplicación de reglas de negocio o conocimiento?
|
|
- [ ] ¿He evitado abstracciones prematuras?
|
|
- [ ] ¿Los métodos extensos siguen siendo comprensibles?
|
|
- [ ] ¿La organización facilita localizar y modificar funcionalidades?
|
|
- [ ] ¿He preservado el comportamiento y los contratos existentes?
|
|
- [ ] ¿Los cambios están limitados al alcance de la tarea?
|
|
|
|
### Comentarios y formato
|
|
|
|
- [ ] ¿Los comentarios aportan información relevante?
|
|
- [ ] ¿He eliminado comentarios redundantes u obsoletos?
|
|
- [ ] ¿El formato es consistente con el proyecto?
|
|
- [ ] ¿Las operaciones relacionadas están agrupadas?
|
|
- [ ] ¿He evitado reformatear código ajeno a la modificación?
|
|
|
|
---
|
|
|
|
## Principio final
|
|
|
|
> "El código limpio no es el que cumple más reglas, sino el que permite comprender, modificar y verificar el comportamiento con el menor coste razonable."
|
|
|
|
Utiliza las reglas de esta guía como herramientas para mejorar decisiones de ingeniería.
|
|
|
|
No sacrifiques la claridad, la simplicidad o la mantenibilidad para cumplir una métrica aislada.
|
|
|
|
**Una solución sencilla, explícita y correctamente verificada es preferible a una arquitectura artificialmente compleja que únicamente aparenta seguir buenas prácticas.**
|