JavaScript
Factory functions
Explica cómo las factory functions crean objetos sin new, validan datos, encapsulan estado con closures e inyectan dependencias o capacidades.
- Última actualización
- Actualizada
- Nivel
- Aplicación
- Fundamentos de programación
JavaScript
Explica cómo las factory functions crean objetos sin new, validan datos, encapsulan estado con closures e inyectan dependencias o capacidades.
Una factory function es una función que crea y devuelve objetos sin necesitar new.
function createUser(name, role) {
return {
name,
role,
isActive: true,
};
}
const user = createUser(
"Nicolás",
"developer",
);Una función que recibe o devuelve funciones es una función de orden superior.
function createMultiplier(multiplier) {
return (value) => value * multiplier;
}Una factory se reconoce por su propósito: crear y retornar un objeto.
function createProduct(name, price) {
return {
id: crypto.randomUUID(),
name,
price,
};
}Una función puede combinar ambos conceptos, pero no significan lo mismo.
function createOrder(id, items = []) {
return {
id,
items: [...items],
status: "pending",
};
}Cada llamada retorna un objeto nuevo.
const first = createOrder("order-1");
const second = createOrder("order-2");
first !== second; // truefunction createProduct({
id = crypto.randomUUID(),
name,
price,
stock = 0,
}) {
if (!name?.trim()) {
throw new TypeError(
"Product name is required",
);
}
if (!Number.isFinite(price) || price < 0) {
throw new TypeError(
"Price must be a non-negative number",
);
}
if (!Number.isInteger(stock) || stock < 0) {
throw new TypeError(
"Stock must be a non-negative integer",
);
}
return {
id,
name: name.trim(),
price,
stock,
isActive: true,
};
}La factory garantiza que los objetos creados cumplen una estructura mínima.
No evita que el objeto se modifique después. Si el dominio necesita proteger cambios, deben exponerse operaciones, usar closures, clases o restricciones adicionales.
function createCart() {
return {
items: [],
addItem(item) {
this.items.push(item);
},
calculateTotal() {
return this.items.reduce(
(total, item) =>
total + item.price * item.quantity,
0,
);
},
};
}Cada llamada crea también funciones nuevas.
const firstCart = createCart();
const secondCart = createCart();
firstCart.addItem === secondCart.addItem; // falsePara pocas instancias esto puede ser irrelevante. Con muchas instancias o métodos grandes, puedes compartir funciones.
function addItem(item) {
this.items.push(item);
}
function calculateTotal() {
return this.items.reduce(
(total, item) =>
total + item.price * item.quantity,
0,
);
}
function createCart() {
return {
items: [],
addItem,
calculateTotal,
};
}firstCart.addItem === secondCart.addItem; // trueAhora los métodos dependen de this. Otra opción es compartir funciones que reciban el estado explícitamente.
function calculateCartTotal(items) {
return items.reduce(
(total, item) =>
total + item.price * item.quantity,
0,
);
}function createCounter(initialValue = 0) {
let value = initialValue;
return {
increment() {
value++;
return value;
},
decrement() {
value--;
return value;
},
read() {
return value;
},
};
}value no es una propiedad pública.
const counter = createCounter();
counter.value; // undefined
counter.increment(); // 1Los métodos comparten el binding mediante un closure.
Esto ofrece encapsulación, pero cada llamada crea nuevas funciones y un entorno léxico independiente.
function createCart() {
let items = [];
return {
addItem(item) {
items = [...items, item];
},
readItems() {
return [...items];
},
};
}Si readItems retornara directamente items, código externo podría modificar el array privado.
readItems() {
return items; // Expone la referencia interna.
}Una copia superficial protege el array exterior, pero no clona los objetos interiores. La estrategia debe ajustarse al dominio.
Una función de orden superior puede construir una factory.
function buildCreatePerson({
generateId,
calculateAge,
clock,
}) {
return function createPerson({
name,
birthdate,
}) {
return {
id: generateId(),
name,
birthdate,
age: calculateAge(
birthdate,
clock.now(),
),
};
};
}const createPerson = buildCreatePerson({
generateId: crypto.randomUUID,
calculateAge,
clock: {
now: () => new Date(),
},
});buildCreatePerson es una función de orden superior.createPerson es la factory de objetos persona.Las dependencias quedan explícitas y pueden sustituirse en pruebas.
function createOrderService({
orderRepository,
inventory,
paymentGateway,
}) {
return {
async complete(order) {
await inventory.reserve(order.items);
const payment = await paymentGateway.pay(
order.total,
);
await orderRepository.save({
...order,
status: "completed",
paymentId: payment.id,
});
},
};
}Esta factory crea un servicio con colaboradores configurados.
El objeto resultante no representa necesariamente una entidad de datos; también puede representar comportamiento o una API.
Un constructor no puede ser async, pero una factory sí.
async function createUserFromApi(userId) {
const response = await fetch(
`/api/users/${userId}`,
);
if (!response.ok) {
throw new Error(
"Could not load user",
);
}
const data = await response.json();
return createUser(
data.name,
data.role,
);
}const user = await createUserFromApi(
"user-123",
);La asincronía queda visible en la llamada.
Una clase también puede exponer una factory estática asíncrona.
class User {
static async load(userId) {
const data = await userApi.find(userId);
return new User(data);
}
}Varias factories pueden expresar casos de creación diferentes.
function createGuestUser() {
return createUser("Guest", "viewer");
}
function createAdministratorUser(name) {
return createUser(
name,
"administrator",
);
}Los nombres comunican mejor la intención que varios parámetros booleanos.
Evita:
createUser("Nicolás", true, false, true);Un objeto literal puede bastar para una única configuración.
const configuration = {
retryCount: 3,
};Una factory aporta valor cuando:
const user = createUser("Nicolás");new.prototype o instanceof.const user = new User("Nicolás");new.this.User.prototype.Una clase organiza instancias de un tipo.
const product = new Product(data);Una factory organiza el proceso de creación.
const product = createProduct(data);También pueden colaborar.
function createProductFromApi(data) {
return new Product({
id: data.product_id,
name: data.product_name,
price: Number(data.price),
});
}La factory adapta datos externos y retorna una instancia de clase.
Una factory normalmente retorna el objeto completo en una llamada.
createProduct(data);Un builder construye por pasos.
const product = new ProductBuilder()
.withName("Keyboard")
.withPrice(120)
.build();Un builder puede servir cuando existen muchas combinaciones opcionales o una secuencia de configuración compleja. Para objetos sencillos, un objeto de opciones y una factory suelen ser suficientes.
function withIdentity(name) {
return {
id: crypto.randomUUID(),
name,
};
}
function withTimestamp(clock) {
return {
createdAt: clock.now(),
};
}
function createCustomer(name, clock) {
return {
...withIdentity(name),
...withTimestamp(clock),
type: "customer",
};
}Spread combina las propiedades, pero la última clave repetida sobrescribe a la anterior.
Cuando las capacidades tienen estado y comportamiento complejo, delegar a colaboradores puede ser más claro que copiar propiedades.
Una factory puede devolver estructuras distintas.
function createResult(success) {
if (success) {
return { data: [] };
}
return { error: "Failed" };
}Es válido, pero obliga al consumidor a reconocer varias formas.
Un contrato discriminado puede ser más claro.
function createResult(success) {
return success
? { status: "success", data: [] }
: { status: "error", error: "Failed" };
}La flexibilidad de una factory no elimina la necesidad de contratos consistentes.
const user = createUser(
"Nicolás",
"developer",
);
user instanceof createUser; // falseLa factory no actúa como constructor.
Si la identidad del tipo es importante, puede retornar una instancia de clase o un objeto con un prototipo específico.
function createUser(name) {
const user = Object.create(userMethods);
user.name = name;
return user;
}function createConfiguration(input) {
return Object.freeze({
environment: input.environment,
timeout: input.timeout ?? 5000,
});
}Puede ser útil para configuraciones planas.
No congela objetos anidados y no debe utilizarse como sustituto de un buen contrato de estado.
function createSession({
initialToken,
clock,
}) {
let token = initialToken;
let expiresAt = null;
return {
authenticate(nextToken, duration) {
token = nextToken;
expiresAt =
clock.now() + duration;
},
clear() {
token = null;
expiresAt = null;
},
isAuthenticated() {
return (
token !== null &&
expiresAt !== null &&
clock.now() < expiresAt
);
},
};
}La factory configura dependencias y mantiene el estado accesible solo mediante operaciones definidas.
new con una factory normal.new.instanceof automáticamente.¿Qué conceptos aparecen aquí?
function buildCreateUser({ generateId }) {
return function createUser(name) {
return {
id: generateId(),
name,
};
};
}buildCreateUser es una función de orden superior porque devuelve otra función. createUser es una factory porque crea y retorna objetos usuario. generateId se conserva mediante closure.
Programación orientada a objetos reúne objetos, encapsulación, polimorfismo, clases y composición como decisiones de diseño, no solo como sintaxis.