Curso: Patrones de Software
Basado en: Catalog of Patterns of Enterprise Application Architecture, Martin Fowler
1. Introducción
Martin Fowler, en su libro Patterns of Enterprise Application Architecture (2002), documentó un conjunto de soluciones recurrentes a problemas que aparecen una y otra vez al construir software empresarial: cómo organizar la lógica de negocio, cómo mapear objetos a bases de datos, cómo estructurar la capa de presentación, entre otros. Su catálogo online agrupa estos patrones en categorías como Domain Logic Patterns, Data Source Architectural Patterns, Object-Relational Behavioral Patterns, etc.
En este artículo desarrollo el patrón Repository, perteneciente a la categoría de Object-Relational Behavioral Patterns, y lo acompaño con una implementación real en Python.
2. ¿Qué es el patrón Repository?
Fowler define el Repository como una capa que media entre el dominio y la capa de acceso a datos, comportándose como una colección de objetos en memoria. La idea central es que el resto de la aplicación (los casos de uso, los servicios, los controladores) no debería saber si los datos vienen de una base de datos SQL, de un archivo, de una API externa o de una lista en memoria. El Repository oculta esa complejidad detrás de una interfaz simple: add(), get(), list(), remove().
Problema que resuelve
Sin este patrón, es común ver consultas SQL o código de ORM esparcido por toda la lógica de negocio. Esto genera:
- Acoplamiento fuerte entre la lógica de negocio y la tecnología de persistencia.
- Dificultad para probar el código (hay que levantar una base de datos real para cada test).
- Duplicación de queries similares en distintos puntos del sistema.
Solución
Se define una interfaz abstracta (un contrato) que declara las operaciones disponibles sobre una colección de objetos de dominio, y luego se implementan una o varias versiones concretas de esa interfaz (por ejemplo, una para SQLite, otra en memoria para pruebas, otra para MongoDB, etc.). El resto del sistema solo depende de la interfaz, nunca de la implementación concreta.
3. Ejemplo real de código (Python)
El ejemplo modela un sistema simple de gestión de pedidos (Orders) para una tienda. Se define:
- Una entidad de dominio
Order. - Una interfaz abstracta
OrderRepository. - Dos implementaciones:
InMemoryOrderRepository(para pruebas unitarias) ySqliteOrderRepository(para producción). - Un caso de uso (
OrderService) que depende únicamente de la interfaz.
3.1 Entidad de dominio
# src/domain.py
from dataclasses import dataclass, field
from datetime import datetime
from uuid import uuid4
@dataclass
class Order:
customer_name: str
total_amount: float
id: str = field(default_factory=lambda: str(uuid4()))
created_at: datetime = field(default_factory=datetime.utcnow)
def apply_discount(self, percentage: float) -> None:
if not 0 <= percentage <= 100:
raise ValueError("El descuento debe estar entre 0 y 100")
self.total_amount -= self.total_amount * (percentage / 100)
3.2 Interfaz del Repository (el contrato)
# src/repository.py
from abc import ABC, abstractmethod
from typing import List, Optional
from .domain import Order
class OrderRepository(ABC):
"""Contrato que toda implementación de persistencia debe cumplir.
La lógica de negocio solo conoce esta interfaz, nunca los detalles
de SQLite, memoria, MongoDB, etc.
"""
@abstractmethod
def add(self, order: Order) -> None: ...
@abstractmethod
def get(self, order_id: str) -> Optional[Order]: ...
@abstractmethod
def list_all(self) -> List[Order]: ...
@abstractmethod
def remove(self, order_id: str) -> None: ...
3.3 Implementación en memoria (ideal para tests)
# src/in_memory_repository.py
from typing import Dict, List, Optional
from .domain import Order
from .repository import OrderRepository
class InMemoryOrderRepository(OrderRepository):
def __init__(self) -> None:
self._orders: Dict[str, Order] = {}
def add(self, order: Order) -> None:
self._orders[order.id] = order
def get(self, order_id: str) -> Optional[Order]:
return self._orders.get(order_id)
def list_all(self) -> List[Order]:
return list(self._orders.values())
def remove(self, order_id: str) -> None:
self._orders.pop(order_id, None)
3.4 Implementación con SQLite (persistencia real)
# src/sqlite_repository.py
import sqlite3
from datetime import datetime
from typing import List, Optional
from .domain import Order
from .repository import OrderRepository
class SqliteOrderRepository(OrderRepository):
def __init__(self, db_path: str = "orders.db") -> None:
self._conn = sqlite3.connect(db_path)
self._create_table()
def _create_table(self) -> None:
self._conn.execute(
"""
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
customer_name TEXT NOT NULL,
total_amount REAL NOT NULL,
created_at TEXT NOT NULL
)
"""
)
self._conn.commit()
def add(self, order: Order) -> None:
self._conn.execute(
"INSERT OR REPLACE INTO orders VALUES (?, ?, ?, ?)",
(order.id, order.customer_name, order.total_amount,
order.created_at.isoformat()),
)
self._conn.commit()
def get(self, order_id: str) -> Optional[Order]:
row = self._conn.execute(
"SELECT id, customer_name, total_amount, created_at FROM orders WHERE id = ?",
(order_id,),
).fetchone()
return self._row_to_order(row) if row else None
def list_all(self) -> List[Order]:
rows = self._conn.execute(
"SELECT id, customer_name, total_amount, created_at FROM orders"
).fetchall()
return [self._row_to_order(row) for row in rows]
def remove(self, order_id: str) -> None:
self._conn.execute("DELETE FROM orders WHERE id = ?", (order_id,))
self._conn.commit()
@staticmethod
def _row_to_order(row) -> Order:
return Order(
id=row[0],
customer_name=row[1],
total_amount=row[2],
created_at=datetime.fromisoformat(row[3]),
)
3.5 Caso de uso desacoplado de la persistencia
# src/service.py
from .domain import Order
from .repository import OrderRepository
class OrderService:
"""Depende solo de la abstracción OrderRepository.
No sabe -ni le importa- si los pedidos viven en SQLite o en memoria.
"""
def __init__(self, repository: OrderRepository) -> None:
self._repository = repository
def place_order(self, customer_name: str, total_amount: float) -> Order:
order = Order(customer_name=customer_name, total_amount=total_amount)
self._repository.add(order)
return order
def apply_discount_to_order(self, order_id: str, percentage: float) -> Order:
order = self._repository.get(order_id)
if order is None:
raise ValueError(f"Pedido {order_id} no encontrado")
order.apply_discount(percentage)
self._repository.add(order) # actualiza
return order
3.6 Demostración: intercambiando implementaciones sin tocar la lógica
# main.py
from src.in_memory_repository import InMemoryOrderRepository
from src.sqlite_repository import SqliteOrderRepository
from src.service import OrderService
def run_demo(repository):
service = OrderService(repository)
order = service.place_order("Ariana Quispe", 250.00)
print(f"Pedido creado: {order}")
updated = service.apply_discount_to_order(order.id, 10)
print(f"Pedido con descuento: {updated}")
print("Todos los pedidos:", service._repository.list_all())
if __name__ == "__main__":
print("--- Usando repositorio en memoria ---")
run_demo(InMemoryOrderRepository())
print("\n--- Usando repositorio SQLite (persistente) ---")
run_demo(SqliteOrderRepository("orders.db"))
Punto clave del ejemplo: OrderService nunca cambia. Basta con inyectarle un repositorio distinto (InMemoryOrderRepository o SqliteOrderRepository) para cambiar completamente la forma en que se persisten los datos. Esto es exactamente el beneficio que describe Fowler: la lógica de dominio queda aislada de los detalles de infraestructura.
4. Pruebas unitarias (ventaja directa del patrón)
# tests/test_order_service.py
import pytest
from src.in_memory_repository import InMemoryOrderRepository
from src.service import OrderService
def test_place_order_stores_it_in_repository():
repo = InMemoryOrderRepository()
service = OrderService(repo)
order = service.place_order("Gustavo Mamani", 100.0)
assert repo.get(order.id) is not None
assert repo.get(order.id).customer_name == "Gustavo Mamani"
def test_apply_discount_updates_total_amount():
repo = InMemoryOrderRepository()
service = OrderService(repo)
order = service.place_order("Hashira", 200.0)
updated = service.apply_discount_to_order(order.id, 25)
assert updated.total_amount == 150.0
Gracias a la interfaz OrderRepository, estas pruebas corren en milisegundos, sin necesidad de una base de datos real — otro beneficio directo que Fowler atribuye al patrón.
5. Relación con otros patrones del catálogo
- Data Mapper: el Repository suele apoyarse en un Data Mapper para traducir entre el objeto de dominio y las filas de la base de datos.
- Unit of Work: en sistemas más grandes, el Repository trabaja junto a un Unit of Work que agrupa varias operaciones en una sola transacción.
-
Service Layer:
OrderServiceen este ejemplo cumple el rol de Service Layer, orquestando el dominio y delegando la persistencia al Repository.
6. Conclusión
El patrón Repository es una de las herramientas más usadas hoy en arquitecturas limpias (Clean Architecture, Hexagonal Architecture) precisamente porque resuelve un problema muy concreto: desacoplar el "qué" (lógica de negocio) del "cómo" (tecnología de persistencia). El ejemplo en Python demuestra que, con una interfaz bien definida, es trivial cambiar de una implementación en memoria a una persistente sin tocar una sola línea de la lógica de negocio.
Repository https://github.com/Hashiravc/repository-pattern.git
Referencias
- Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
- Fowler, M. Repository. Catalog of Patterns of Enterprise Application Architecture. https://martinfowler.com/eaaCatalog/repository.html
- Catálogo completo: https://martinfowler.com/eaaCatalog/
Top comments (0)