Programación orientada a objetos en JavaScript | Nicolás Garzón
La programación orientada a objetos organiza un sistema alrededor de objetos que tienen identidad, estado y comportamiento.
JavaScript
Copiar const order = {
id : "order-123" ,
status : "pending" ,
items : [ ] ,
complete ( ) {
this . status = "completed" ;
} ,
} ; El objeto representa una orden concreta y define qué operaciones puede realizar.
En JavaScript, la orientación a objetos puede implementarse con:
Objetos literales.
Factories y closures.
Delegación prototípica.
Funciones constructoras.
Clases.
No todo objeto es una instancia de una clase, y utilizar objetos no vuelve automáticamente orientado a objetos cualquier diseño.
Identidad: qué entidad concreta representa.
Estado: información que puede cambiar.
Comportamiento: operaciones permitidas.
Reglas: condiciones que deben mantenerse.
JavaScript
Copiar class BankAccount {
#balance = 0 ;
constructor ( id, owner ) {
this . id = id;
this . owner = owner;
}
deposit ( amount ) {
if ( ! Number. isFinite ( amount) || amount <= 0 ) {
throw new TypeError (
"Amount must be positive" ,
) ;
}
this . #balance += amount;
}
readBalance ( ) {
return this . #balance;
}
} La cuenta no expone libremente el saldo. Define operaciones que protegen sus reglas.
JavaScript permite combinar orientación a objetos con programación funcional, procedural y basada en eventos.
JavaScript
Copiar const visibleProducts = products
. filter ( isVisible)
. map ( createProductViewModel) ; Puede convivir con una entidad de clase:
JavaScript
Copiar const product = new Product ( data) ; Y con un servicio creado mediante factory:
JavaScript
Copiar const productService = createProductService ( {
repository,
logger,
} ) ; No necesitas escoger un único paradigma para toda la aplicación. Elige la herramienta que comunique mejor cada responsabilidad.
JavaScript
Copiar const counter = {
value : 0 ,
increment ( ) {
this . value++ ;
} ,
} ; Es un objeto con estado y comportamiento aunque no provenga de una clase personalizada.
También puede crearse mediante closure.
JavaScript
Copiar function createCounter ( ) {
let value = 0 ;
return {
increment ( ) {
value++ ;
} ,
read ( ) {
return value;
} ,
} ;
} JavaScript no obliga a usar clases para aplicar principios orientados a objetos.
La encapsulación controla cómo se accede y modifica el estado.
JavaScript
Copiar class Order {
#status = "pending" ;
complete ( ) {
if ( this . #status !== "pending" ) {
throw new Error (
"Only pending orders can be completed" ,
) ;
}
this . #status = "completed" ;
}
readStatus ( ) {
return this . #status;
}
} El objetivo no es únicamente esconder datos. Es proteger reglas y ofrecer una API pequeña y comprensible.
También puede lograrse con closures.
JavaScript
Copiar function createOrder ( ) {
let status = "pending" ;
return {
complete ( ) {
if ( status !== "pending" ) {
throw new Error (
"Only pending orders can be completed" ,
) ;
}
status = "completed" ;
} ,
readStatus ( ) {
return status;
} ,
} ;
} Una abstracción expone lo necesario y oculta detalles que el consumidor no debería manejar.
JavaScript
Copiar await paymentGateway. pay ( {
amount : order. total,
currency : "COP" ,
} ) ; El consumidor no necesita conocer:
Cómo se firma la solicitud.
Qué endpoint se utiliza.
Cómo se renueva un token.
Cómo se reintenta una operación.
Una buena abstracción reduce conocimiento necesario. Una mala abstracción solo cambia nombres y sigue filtrando todos los detalles internos.
Dos objetos pueden contener los mismos datos y representar entidades distintas.
JavaScript
Copiar const firstOrder = {
id : "order-1" ,
total : 100 ,
} ;
const secondOrder = {
id : "order-2" ,
total : 100 ,
} ; El total es igual, pero las órdenes no son la misma entidad.
JavaScript
Copiar firstOrder === secondOrder; La identidad del dominio suele expresarse mediante un identificador estable, no únicamente mediante la referencia de memoria.
JavaScript
Copiar function isSameOrder ( first, second ) {
return first. id === second. id;
} Un objeto orientado al dominio no debería permitir cualquier cambio arbitrario.
JavaScript
Copiar order. status = "completed" ; JavaScript
Copiar order. complete ( ) ; La operación puede validar reglas, registrar tiempo y producir eventos.
JavaScript
Copiar complete ( ) {
if ( this . status !== "pending" ) {
throw new Error (
"Invalid order transition" ,
) ;
}
this . status = "completed" ;
this . completedAt = new Date ( ) ;
} El método comunica una transición del dominio, no solo una asignación.
Una invariante es una condición que debe ser verdadera para que el objeto siga siendo válido.
El stock no puede ser negativo.
Una orden completada no puede volver a pendiente.
Un porcentaje debe estar entre 0 y 100.
Un correo debe tener un formato aceptable.
JavaScript
Copiar class InventoryItem {
#stock;
constructor ( initialStock = 0 ) {
this . #assertStock ( initialStock) ;
this . #stock = initialStock;
}
decrease ( quantity ) {
const nextStock = this . #stock - quantity;
this . #assertStock ( nextStock) ;
this . #stock = nextStock;
}
#assertStock ( value ) {
if ( ! Number. isInteger ( value) || value < 0 ) {
throw new RangeError (
"Stock cannot be negative" ,
) ;
}
}
} La clase mantiene la regla en todas las entradas que pueden modificar el stock.
La herencia permite que una clase se especialice desde otra.
JavaScript
Copiar class User {
constructor ( name ) {
this . name = name;
}
describe ( ) {
return this . name;
}
}
class Administrator extends User {
describe ( ) {
return ` ${ super . describe ( ) } (administrator) ` ;
}
} Puede ser apropiada cuando existe una relación de tipo estable y la clase hija conserva el contrato del padre.
No debe utilizarse únicamente para reutilizar líneas de código.
La composición reúne objetos que colaboran.
JavaScript
Copiar class OrderService {
constructor ( {
inventory,
payments,
notifier,
} ) {
this . inventory = inventory;
this . payments = payments;
this . notifier = notifier;
}
async complete ( order ) {
await this . inventory. reserve ( order. items) ;
const payment = await this . payments. pay (
order. total,
) ;
await this . notifier. send ( order) ;
return payment;
}
} OrderService utiliza esos colaboradores; no es un tipo de inventario, pago o notificador.
La composición suele ser adecuada para relaciones “tiene un” o “usa un”.
El polimorfismo permite usar implementaciones diferentes mediante un contrato común.
JavaScript
Copiar class EmailNotifier {
send ( message ) {
return emailClient. send ( message) ;
}
}
class SlackNotifier {
send ( message ) {
return slackClient. send ( message) ;
}
} JavaScript
Copiar function notify ( notifier, message ) {
return notifier. send ( message) ;
} notify no necesita conocer la clase específica.
En JavaScript, el polimorfismo puede basarse en comportamiento, sin una clase padre común.
JavaScript
Copiar const consoleNotifier = {
send ( message ) {
console. log ( message) ;
} ,
} ; Si cumple el contrato esperado, puede utilizarse.
JavaScript comparte comportamiento mediante prototipos.
JavaScript
Copiar const userMethods = {
describe ( ) {
return this . name;
} ,
} ;
const user = Object. create ( userMethods) ;
user. name = "Nicolás" ; La clase es una sintaxis estructurada sobre este modelo, pero también puede utilizarse directamente.
El lenguaje está basado en prototipos, incluso cuando el código utiliza clases.
JavaScript
Copiar class User {
constructor ( data ) {
Object. assign ( this , data) ;
}
} Esta clase solo copia propiedades y quizá no aporta reglas, comportamiento ni abstracción.
Una clase puede ser válida como modelo de datos, pero no obtiene automáticamente buen diseño por usar class.
Construcción controlada.
Métodos de dominio.
Invariantes.
Encapsulación.
Identidad de tipo.
Comportamiento compartido.
Un modelo anémico contiene datos, pero casi toda la lógica vive fuera.
JavaScript
Copiar const order = {
status : "pending" ,
total : 100 ,
} ;
function completeOrder ( order ) {
order. status = "completed" ;
} Esto no siempre es malo. DTOs, view models y payloads pueden ser estructuras de datos simples.
El problema aparece cuando una entidad con reglas importantes permite que cualquier parte del sistema modifique su estado sin control.
El extremo contrario es un objeto que hace demasiado.
Texto
Copiar ApplicationManager
- autenticación
- pagos
- inventario
- correos
- reportes
- caché
- configuraciónUn objeto grande acumula dependencias y responsabilidades.
Muchos motivos diferentes para cambiar.
Demasiados métodos no relacionados.
Dependencias hacia casi todo el sistema.
Estado difícil de mantener consistente.
Pruebas que requieren preparar demasiados colaboradores.
Divide por responsabilidades y utiliza composición.
Se distingue por identidad.
JavaScript
Copiar class User {
constructor ( id, name ) {
this . id = id;
this . name = name;
}
} Dos usuarios con el mismo nombre pueden ser entidades diferentes.
Se define principalmente por sus valores.
JavaScript
Copiar function createMoney ( amount, currency ) {
return Object. freeze ( {
amount,
currency,
} ) ;
} Dos valores monetarios con la misma cantidad y moneda pueden considerarse equivalentes conceptualmente.
JavaScript no impone estas categorías. Son herramientas de modelado.
JavaScript
Copiar const productDto = {
product_id : "product-1" ,
product_name : "Keyboard" ,
unit_price : "120.00" ,
} ; JavaScript
Copiar const product = new Product ( {
id : productDto. product_id,
name : productDto. product_name,
price : Number ( productDto. unit_price) ,
} ) ;
Adaptar nombres externos.
Convertir tipos.
Validar invariantes.
Evitar que cambios de API contaminen todo el dominio.
No siempre necesitas una clase; sí necesitas una frontera clara cuando los contratos difieren.
Una entidad representa estado y reglas propias.
JavaScript
Copiar order. complete ( ) ; Un servicio coordina operaciones que no pertenecen naturalmente a una sola entidad.
JavaScript
Copiar await orderCheckout. complete ( order) ; Evita colocar red, persistencia y coordinación de múltiples sistemas dentro de cada entidad solo para “hacerla orientada a objetos”.
Una clase que usa globals ocultos es difícil de probar.
JavaScript
Copiar class ReportService {
generate ( data ) {
logger. info ( "Generating report" ) ;
return globalFormatter. format ( data) ;
}
} JavaScript
Copiar class ReportService {
constructor ( { logger, formatter } ) {
this . logger = logger;
this . formatter = formatter;
}
generate ( data ) {
this . logger. info ( "Generating report" ) ;
return this . formatter. format ( data) ;
}
} La orientación a objetos no justifica ocultar dependencias.
Muchos objetos orientados a objetos mantienen estado mutable.
JavaScript
Copiar cart. addItem ( product) ; También pueden diseñarse objetos inmutables.
JavaScript
Copiar const updatedCart = cart. withItem ( product) ;
Quién comparte la referencia.
Necesidad de historial.
Integración con UI.
Costos de copia.
Claridad del contrato.
POO no significa obligatoriamente mutabilidad.
Un objeto con responsabilidades claras puede probarse mediante su API pública.
JavaScript
Copiar const account = new BankAccount (
"account-1" ,
"Nicolás" ,
) ;
account. deposit ( 100 ) ;
expect ( account. readBalance ( ) ) . toBe ( 100 ) ; Evita probar detalles internos que podrían cambiar sin afectar el comportamiento observable.
Para servicios con dependencias:
JavaScript
Copiar const notifier = {
send : vi. fn ( ) ,
} ;
const service = new AlertService ( {
notifier,
} ) ; La composición facilita sustituir colaboradores.
Entidades con identidad y lifecycle.
Reglas que deben protegerse.
Varias instancias con comportamiento compartido.
Integraciones que cumplen un contrato común.
Componentes con estado interno controlado.
APIs orientadas a mensajes y colaboración.
Transformaciones simples de datos.
Utilidades puras.
Configuraciones estáticas.
Payloads de red.
Funciones pequeñas sin estado.
Pipelines donde la composición funcional es más clara.
JavaScript
Copiar const total = orders
. filter ( isCompleted)
. map ( readTotal)
. reduce ( sum, 0 ) ; Convertir cada paso en una clase haría el código más complejo sin beneficio.
JavaScript
Copiar class Order {
#status = "pending" ;
constructor ( id, items ) {
this . id = id;
this . items = [ ... items] ;
}
calculateTotal ( ) {
return this . items. reduce (
( total, item ) =>
total + item. price * item. quantity,
0 ,
) ;
}
complete ( ) {
if ( this . #status !== "pending" ) {
throw new Error (
"Order cannot be completed" ,
) ;
}
this . #status = "completed" ;
}
}
class CheckoutService {
constructor ( { inventory, payments } ) {
this . inventory = inventory;
this . payments = payments;
}
async complete ( order ) {
await this . inventory. reserve ( order. items) ;
const payment = await this . payments. pay (
order. calculateTotal ( ) ,
) ;
order. complete ( ) ;
return payment;
}
} Order protege su estado y calcula datos propios. CheckoutService coordina sistemas externos mediante composición.
Decir que todos los objetos son instancias de clases personalizadas.
Creer que usar class garantiza orientación a objetos bien diseñada.
Crear herencia solo para reutilizar código.
Exponer todo el estado públicamente y llamar encapsulado al resultado.
Colocar todas las responsabilidades en una entidad.
Convertir DTOs y configuraciones simples en clases sin necesidad.
Ocultar dependencias globales dentro de métodos.
Usar POO como enemigo de programación funcional.
Diseñar jerarquías profundas antes de entender el dominio.
Confundir privacidad con seguridad o autorización.
Aplicar patrones de otros lenguajes sin considerar prototipos y flexibilidad de JavaScript.
POO organiza código alrededor de objetos con identidad, estado y comportamiento.
JavaScript permite POO sin clases.
Las clases funcionan sobre prototipos.
Encapsulación protege reglas, no solo propiedades.
Abstracción reduce el conocimiento necesario para usar una capacidad.
Herencia modela especialización; composición modela colaboración.
Polimorfismo puede basarse en comportamiento sin una clase padre.
No todo dato necesita una clase.
Entidades y servicios suelen tener responsabilidades diferentes.
JavaScript permite combinar POO y programación funcional.
¿Qué responsabilidad debería pertenecer a cada parte?
Texto
Copiar Order:
- Conocer sus items.
- Calcular su total.
- Validar su transición a completed.
CheckoutService:
- Reservar inventario.
- Solicitar el pago.
- Coordinar la finalización.Respuesta Order debe proteger su propio estado y reglas. CheckoutService debe coordinar dependencias externas. Así la entidad no conoce infraestructura y el servicio no modifica libremente reglas internas.
El siguiente bloque estudia Arrays y colecciones , donde veremos índices, métodos de transformación, búsqueda, reducción, ordenamiento, Map, Set e iteración.