Paquete por Componente: El Enfoque Híbrido
Agrupa toda la responsabilidad de un límite de funcionalidad limpio en un paquete — la persistencia interna y la lógica permanecen privadas, el compilador hace cumplir lo que es visible para el resto del sistema.
Por qué importa
Package by Component es la síntesis de Simon Brown de Package by Layer y Package by Feature. Un componente es una unidad de grano grueso con una interfaz pública bien definida e internos ocultos. Todo lo necesario para implementar una frontera de dominio completa — controlador, servicio, repositorio, entidad — vive en un paquete. La adición clave sobre Package by Feature: solo el servicio es público. Todo lo demás es privado de paquete.
El compilador se convierte en el aplicador. En Java, el acceso de paquete privado significa que ningún otro paquete puede acceder a la clase aunque conozca su nombre. En TypeScript, un barrel index.ts que solo exporta el servicio hace que el repositorio sea invisible para el resto del sistema — cualquier importación del archivo interno es capturada por reglas de ESLint. En Python, los módulos con prefijo de guion bajo y __all__ proveen la misma intención.
Este es el empaquetado por frontera arquitectónica, no por capa técnica o funcionalidad de dominio. Cada componente posee su porción vertical completa y aplica su propia superficie de área.
✗El problema
OrderRepository is fully public. A PaymentsController imports it directly — bypassing OrderService and all its business logic.
Bad
# orders/order_repository.py ← should be internal, but is fully public
class OrderRepository:
def find_by_id(self, order_id: str) -> dict:
return {"id": order_id, "status": "pending", "total": 99.0}
# orders/order_service.py ← intended public API
class OrderService:
def get_order_status(self, order_id: str) -> str:
return OrderRepository().find_by_id(order_id)["status"]
# payments/payments_controller.py ← violates the component boundary
from orders.order_repository import OrderRepository # ← bypasses OrderService!
class PaymentsController:
def process(self, order_id: str):
order = OrderRepository().find_by_id(order_id) # direct internal access
# ... no business rules applied, no validation ...
# The component has no boundary — it's just a folder with a name.
// orders/OrderRepository.ts ← should be internal, but is exported
export class OrderRepository {
findById(orderId: string): { id: string; status: string; total: number } {
return { id: orderId, status: "pending", total: 99.0 };
}
}
// orders/OrderService.ts ← intended public API
export class OrderService {
getOrderStatus(orderId: string): string {
return new OrderRepository().findById(orderId).status;
}
}
// payments/PaymentsController.ts ← violates the component boundary
import { OrderRepository } from "../orders/OrderRepository"; // ← bypasses service!
export class PaymentsController {
process(orderId: string): void {
const order = new OrderRepository().findById(orderId); // direct internal access
// no business rules applied
}
}
// The component has no boundary — it's just a folder with a name.
✓La solución
Only OrdersService is exported from the component. OrderRepository is package-private. Payments can only call OrdersService.get_order_status() — the boundary is compiler-enforced.
Good
# orders/__init__.py ← enforced public API
from orders._order_service import OrderService
__all__ = ["OrderService"] # only OrderService is exported
# orders/_order_repository.py ← private (underscore prefix)
class _OrderRepository:
def find_by_id(self, order_id: str) -> dict:
return {"id": order_id, "status": "pending", "total": 99.0}
# orders/_order_service.py ← private implementation
from orders._order_repository import _OrderRepository
class OrderService:
def get_order_status(self, order_id: str) -> str:
return _OrderRepository().find_by_id(order_id)["status"]
def place_order(self, data: dict) -> dict:
order = {"id": data["id"], "total": data["total"], "status": "pending"}
_OrderRepository().save(order)
return order
# payments/payments_controller.py ← only sees the public API
from orders import OrderService # _OrderRepository is not accessible
class PaymentsController:
def process(self, order_id: str):
status = OrderService().get_order_status(order_id) # goes through business logic
// orders/OrderRepository.ts ← NOT in the barrel (internal only)
class OrderRepository { // no export keyword at top level
findById(orderId: string): { id: string; status: string; total: number } {
return { id: orderId, status: "pending", total: 99.0 };
}
}
export { OrderRepository }; // visible inside orders/ only
// orders/OrdersService.ts
import { OrderRepository } from "./OrderRepository";
export class OrdersService {
getOrderStatus(orderId: string): string {
return new OrderRepository().findById(orderId).status;
}
placeOrder(data: { id: string; total: number }): object {
const order = { id: data.id, total: data.total, status: "pending" };
// new OrderRepository().save(order);
return order;
}
}
// orders/index.ts ← barrel: only public surface
export { OrdersService } from "./OrdersService";
// OrderRepository is NOT re-exported
// payments/PaymentsController.ts ← only sees the barrel
import { OrdersService } from "../orders"; // barrel import
export class PaymentsController {
process(orderId: string): void {
const status = new OrdersService().getOrderStatus(orderId); // through business logic
}
}
// ESLint: import/no-internal-modules catches any "../orders/OrderRepository" import.
💡Conclusión clave
Si las clases internas de un componente son públicas, el componente no tiene frontera — es solo una carpeta con un nombre. Usa modificadores de acceso, archivos barrel y reglas de linting para aplicar que el único punto de entrada a un componente es su API pública declarada. El compilador debe hacer las violaciones imposibles, no meramente descorteses.
🔧 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: Si las clases internas de un componente son públicas, el componente no tiene límite — es solo una carpeta con un nombre.
✗ Tu versión