Skip to main content

Inicia sesión en CleanKata

Sigue tu progreso, gana XP y desbloquea todas las lecciones.

Al iniciar sesión aceptas nuestros Términos de uso y Política de privacidad.

Arquitectura Limpia70 XP7 min

Reglas de Negocio y Entidades

Las Reglas de Negocio Críticas existirían incluso sin un ordenador — las Entidades encapsulan estas reglas y datos, formando el núcleo más estable del sistema, independiente de la UI y la persistencia.

Por qué importa

Existen dos tipos de reglas de negocio. Las Reglas de Negocio Críticas son las reglas que existirían aunque el software no existiera — un banco calcula intereses de préstamos independientemente de si el sistema funciona en libros de papel o servidores en la nube. Estas reglas son lo más valioso del sistema. Deben estar aisladas, protegidas e intocadas por cualquier otra cosa. Las Reglas de Negocio de Aplicación son reglas específicas de casos de uso que solo existen porque hay software — flujos de validación, pasos de transformación de datos, lógica de orquestación.

Las Entidades son los objetos que encarnan las Reglas de Negocio Críticas. Una Entidad es un concepto de negocio puro: un Préstamo, un Pedido, una Factura. Contiene datos y las reglas que operan sobre esos datos. No sabe nada de bases de datos, interfaces de usuario, frameworks ni HTTP. Puede instanciarse en un archivo de prueba de Python sin más importaciones que la biblioteca estándar. Si una Entidad importa SQLAlchemy, ha sido corrompida. El framework ha invadido la parte más estable y valiosa del sistema.

✗El problema

A Loan class that has calculate_interest() but also save_to_db(), to_html(), and send_email_notification() — the critical business rule is buried with technical concerns it should never know about.

Bad

from sqlalchemy import Column, Float, Integer
from sqlalchemy.ext.declarative import declarative_base
import smtplib
from decimal import Decimal

Base = declarative_base()

class Loan(Base):
    __tablename__ = "loans"
    id        = Column(Integer, primary_key=True)
    principal = Column(Float)
    rate      = Column(Float)
    term      = Column(Integer)

    def calculate_interest(self) -> float:
        # Critical business rule — would exist on a paper ledger too
        return self.principal * self.rate * self.term

    def save_to_db(self, session):
        session.add(self)
        session.commit()

    def to_html(self) -> str:
        interest = self.calculate_interest()
        return f"<p>Loan {self.id}: interest = {interest:.2f}</p>"

    def send_email_notification(self, recipient: str):
        smtp = smtplib.SMTP("localhost")
        smtp.sendmail("bank@co.com", recipient,
                      f"Interest: {self.calculate_interest():.2f}")

# The critical rule is correct but corrupted.
# To test calculate_interest: SQLAlchemy, DB session, and smtplib must exist.
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import nodemailer from "nodemailer";

@Entity()
export class Loan {
  @PrimaryGeneratedColumn() id!: number;
  @Column("float") principal!: number;
  @Column("float") rate!: number;
  @Column("int")   term!: number;

  calculateInterest(): number {
    return this.principal * this.rate * this.term;
  }

  toHtml(): string {
    return `<p>Loan ${this.id}: interest = ${this.calculateInterest().toFixed(2)}</p>`;
  }

  async sendEmailNotification(recipient: string): Promise {
    const t = nodemailer.createTransport({ host: "localhost", port: 25 });
    await t.sendMail({ from: "bank@co.com", to: recipient,
      subject: "Loan interest", text: `${this.calculateInterest()}` });
  }
}
// To test calculateInterest: TypeORM decorators require DB setup.
// The Entity has been corrupted by three technical concerns.

✓La solución

A pure Loan entity with only the critical business rules — no ORM, no HTML, no email. Persistence, presentation, and notification are separate concerns handled by separate classes.

Good

from decimal import Decimal
from dataclasses import dataclass

@dataclass
class Loan:
    principal: Decimal
    rate: Decimal   # annual interest rate, e.g. Decimal("0.05") for 5%
    term: int       # years

    def calculate_interest(self) -> Decimal:
        """Critical Business Rule — would be calculated on paper without software."""
        return self.principal * self.rate * self.term

    def monthly_payment(self) -> Decimal:
        """Another critical rule — depends only on principal, rate, and term."""
        monthly_rate = self.rate / 12
        n = self.term * 12
        if monthly_rate == 0:
            return self.principal / n
        return self.principal * monthly_rate / (1 - (1 + monthly_rate) ** -n)

# Test with plain Python — zero infrastructure:
# loan = Loan(Decimal("10000"), Decimal("0.05"), 2)
# assert loan.calculate_interest() == Decimal("1000.00")

# Technical concerns are separate, isolated classes:
# class SqlLoanRepository(LoanRepository): ...  — persistence
# class LoanPresenter: def to_html(loan: Loan) -> str: ...  — presentation
# class LoanNotifier: async def send(loan: Loan, email: str): ...  — notification
# The Entity knows none of them exist.
// Pure domain entity — no imports from any framework or library
export class Loan {
  constructor(
    readonly principal: number,
    readonly rate: number,  // annual interest rate, e.g. 0.05 for 5%
    readonly term: number,  // years
  ) {}

  calculateInterest(): number {
    // Critical Business Rule — exists in banking regardless of software
    return this.principal * this.rate * this.term;
  }

  monthlyPayment(): number {
    const monthlyRate = this.rate / 12;
    const n           = this.term * 12;
    if (monthlyRate === 0) return this.principal / n;
    return this.principal * monthlyRate / (1 - Math.pow(1 + monthlyRate, -n));
  }
}

// Test with zero dependencies:
// const loan = new Loan(10_000, 0.05, 2);
// expect(loan.calculateInterest()).toBe(1000);

// Technical concerns live elsewhere:
// class TypeOrmLoanRepository implements LoanRepository { ... }  — persistence
// class LoanPresenter { toHtml(loan: Loan): string { ... } }     — presentation
// class LoanNotifier { async send(loan: Loan, email: string) }   — notification
// The Entity imports nothing. Framework upgrades cannot corrupt it.

💡Conclusión clave

Una Entidad es un concepto de negocio que existe independientemente de la tecnología. Si importa un framework, ha sido corrompida. La Entidad es la capa más interna del sistema — no debe saber nada de bases de datos, interfaces de usuario ni servicios externos. Todo lo demás depende de ella. Ella no depende de nada.

🔧 Algunos ejercicios pueden tener errores. Si algo parece incorrecto, usa el botón Feedback (abajo a la derecha) para reportarlo — nos ayuda a corregirlo rápido.

Pista: Una Entidad es un concepto de negocio que existe independientemente de la tecnología. Si importa un framework, ha sido corrompida.

✗ Tu versión