El problema de partida
En el panel de administración existía este código:
// AdminPages.jsx — ANTES (68 líneas)
export const ProductPage = () => {
const { id } = useParams();
return (
<div className="space-y-6">
<h1 className="text-2xl font-semibold text-gray-900">
{id ? 'Edit Product' : 'Create Product'}
</h1>
<ProductForm productId={id} />
</div>
);
};
export const UserPage = () => {
const { id } = useParams();
return (
<div className="space-y-6">
<h1 className="text-2xl font-semibold text-gray-900">
{id ? 'Edit User' : 'Create User'}
</h1>
<UserForm userId={id} />
</div>
);
};
// ... x6 veces, el mismo patrón copiadoSeis componentes con estructura idéntica y solo tres diferencias entre ellos:
- El label del título (
'Product','User','Order'…) - El formulario a renderizar (
ProductForm,UserForm…) - El nombre del prop que recibe el id (
productId,userId,orderId…)
El patrón Factory
Qué es
Una Factory (Fábrica) es una función u objeto cuyo único trabajo es crear otros objetos o componentes centralizando la lógica de instanciación. Para entender en profundidad la teoría, ventajas y variantes de este patrón creacional, puedes consultar nuestra guía completa del Patrón Factory Method.
función normal: entrada de datos → salida de datos
factory: configuración → objeto / componente / función
En JavaScript, cuando una factory crea componentes React, también se la conoce como Higher-Order Component (HOC): una función que devuelve un componente.
Cómo funciona
createAdminPage({ label, Form, idProp }) → Componente React
La factory recibe la configuración que cambia y devuelve un componente con la lógica que es siempre igual:
const createAdminPage = ({ label, Form, idProp, editOnly = false, extraParams }) => {
const Page = () => {
const params = useParams();
const { id } = params;
const title = editOnly
? `Edit ${label}`
: `${id ? 'Edit' : 'Create'} ${label}`;
const formProps = {
[idProp]: id,
...extraParams?.(params),
};
return (
<div className="space-y-6">
<h1 className="text-2xl font-semibold text-gray-900">{title}</h1>
<Form {...formProps} />
</div>
);
};
Page.displayName = `${label.replace(/\s+/g, '')}Page`;
return Page;
};El resultado
// AdminPages.jsx — DESPUÉS (51 líneas)
export const ProductPage = createAdminPage({ label: 'Product', Form: ProductForm, idProp: 'productId' });
export const UserPage = createAdminPage({ label: 'User', Form: UserForm, idProp: 'userId' });
export const OrderPage = createAdminPage({ label: 'Order', Form: OrderForm, idProp: 'orderId' });
export const CategoryPage = createAdminPage({ label: 'Category', Form: CategoryForm, idProp: 'categoryId' });
export const SettingsPage = createAdminPage({ label: 'Settings', Form: SettingsForm, idProp: 'settingsId', editOnly: true });
export const ReportPage = createAdminPage({
label: 'Report',
Form: ReportForm,
idProp: 'reportId',
extraParams: (params) => ({ userId: params.userId }),
});Qué resuelve: el principio DRY
DRYGlosarioDRY (Don't Repeat Yourself)Principio fundamental de diseño de software formulado por Andy Hunt y Dave Thomas. Establece que toda pieza de conocimiento o lógica debe tener una representación única, no ambigua y definitiva en el sistema para evitar duplicidad y facilitar el mantenimiento.Ver término completo → = Don't Repeat Yourself
Antes: cambiar el diseño de las páginas → tocar 6 archivos.
Después: cambiar el diseño de las páginas → tocar 1 función.
Añadir una nueva página pasa de ser copiar y pegar a escribir una línea:
export const InvoicePage = createAdminPage({
label: 'Invoice',
Form: InvoiceForm,
idProp: 'invoiceId',
});Principios SOLID aplicados
SOLIDGlosarioSOLIDAcrónimo de cinco principios de diseño orientado a objetos y programación diseñados para hacer que el software sea más comprensible, flexible y mantenible: S - Single Responsibility Principle (Responsabilidad Única) O - Open/Closed Principle (Abierto/Cerrado) L - Liskov Substitution Principle (Sustitución de Liskov) I - Interface Segregation Principle (Segregación de Interfaz) D - Dependency Inversion Principle (Inversión de Dependencias) Ver término completo → es un conjunto de 5 principios para escribir código mantenible. En esta refactorización aplican principalmente dos.
S — Single Responsibility Principle
Cada módulo debe tener una sola razón para cambiar.
El archivo AdminPages.jsx ahora tiene dos responsabilidades conviviendo:
| Responsabilidad | Razón de cambio |
|---|---|
createAdminPage (la factory) | Si cambia el diseño o estructura de las páginas |
Los exports (ProductPage, etc.) | Si se añade o elimina un recurso |
¿Hay que separarlos? Solo si ambas razones de cambio ocurren independientemente con frecuencia. Ver sección YAGNI más abajo.
O — Open/Closed Principle
El código debe estar abierto a extensión, pero cerrado a modificación.
Antes, añadir una nueva página significaba modificar el código existente (copiar uno de los componentes y editar).
Ahora, añadir una nueva página es extender sin tocar lo que ya funciona: una línea nueva que llama a createAdminPage. El código existente no se toca.
YAGNI — You Aren't Gonna Need It
No construyas lo que podrías necesitar. Construye lo que necesitas ahora.
YAGNIGlosarioYAGNI (You Aren't Gonna Need It)Principio de Extreme Programming (XP) que establece que un programador no debe añadir funcionalidad hasta que sea estrictamente necesario. Su objetivo es evitar la sobre-ingeniería (over-engineering), previniendo código para funcionalidades hipotéticas.Ver término completo → es un principio de XP (Extreme Programming) que combate el over-engineering.
Qué es el over-engineering
Añadir complejidad, abstracciones o estructura por si acaso en el futuro, cuando no hay evidencia de que ese futuro vaya a llegar.
Aplicado a este caso
Después de la refactorización, surge la pregunta:
"¿Debería extraer
createAdminPagea su propio archivoutils/createAdminPage.jsx?"
La respuesta SOLID + YAGNI:
¿Se usa en más de un archivo? NO → déjalo donde está
¿Tiene más de ~30 líneas de lógica? NO → déjalo donde está
¿Quieres testearlo de forma aislada? NO → déjalo donde está
Moverlo sin una necesidad real añadiría:
- Un archivo más que gestionar
- Un import adicional en
AdminPages.jsx - Más indirección sin beneficio tangible
Separar cuando tienes dos razones reales de cambio, no cuando podrías tenerlas.
Cuándo sí extraerlo
Extraer createAdminPage a su propio módulo tendría sentido si:
- Otro archivo lo necesita importar
- Crece y contiene lógica compleja propia
- Quieres escribir unit tests específicos para la factory
Resumen visual
Problema: Solución: Dónde vive:
───────── ───────── ──────────
ProductPage ┐ createAdminPage() AdminPages.jsx
UserPage │ código → ──────────────── (junto a los exports,
OrderPage │ copiado una sola definición hasta que haya razón
CategoryPage │ x6 veces + 6 líneas de config para separarlo)
SettingsPage │
ReportPage ┘
| Concepto | Qué dice | Cómo aplica aquí |
|---|---|---|
| Factory | Crea componentes desde configuración | createAdminPage({ label, Form, idProp }) |
| DRY | No repitas código | Un cambio afecta a los 6 componentes |
| SRP | Una responsabilidad por módulo | Factory + exports en el mismo archivo porque cambian juntos |
| OCP | Abierto a extensión, cerrado a modificación | Nueva página = nueva línea, sin tocar la factory |
| YAGNI | No construyas lo que no necesitas | No extraer a utils/ sin una necesidad real |
| Over-engineering | Complejidad sin beneficio actual | Crear carpeta utils/ para una función de 18 líneas usada en un sitio |