Menu
Coddy logo textTech

Patrón Repository

Parte de la sección Programación Orientada a Objetos del Journey de Dart de Coddy — lección 100 de 110.

El patrón Repository separa la lógica de acceso a datos de la lógica de negocio creando una capa de abstracción entre su aplicación y las fuentes de datos. En lugar de que sus clases interactúen directamente con bases de datos, APIs o archivos, estas trabajan con un repositorio que maneja todas las operaciones de datos.

El patrón define una interfaz para operaciones de datos, y las implementaciones concretas manejan el mecanismo de almacenamiento real. Esto facilita el intercambio de fuentes de datos sin cambiar su lógica de negocio:

// Entidad
class User {
  final int id;
  final String name;
  User(this.id, this.name);
}

// Interfaz del repositorio
abstract class UserRepository {
  User? getById(int id);
  List<User> getAll();
  void save(User user);
  void delete(int id);
}

// Implementación en memoria
class InMemoryUserRepository implements UserRepository {
  final Map<int, User> _users = {};

  @override
  User? getById(int id) => _users[id];

  @override
  List<User> getAll() => _users.values.toList();

  @override
  void save(User user) => _users[user.id] = user;

  @override
  void delete(int id) => _users.remove(id);
}

void main() {
  UserRepository repo = InMemoryUserRepository();
  
  repo.save(User(1, 'Alice'));
  repo.save(User(2, 'Bob'));
  
  print(repo.getById(1)?.name);  // Alice
  print(repo.getAll().length);    // 2
}

Tu lógica de negocio depende únicamente de la interfaz UserRepository. Más adelante podrías crear un DatabaseUserRepository o un ApiUserRepository que implemente la misma interfaz, e intercambiar las implementaciones sin modificar el código que utiliza el repositorio. Este patrón es esencial para aplicaciones testeables y mantenibles donde las fuentes de datos pueden cambiar con el tiempo.

challenge icon

Desafío

Fácil

¡Vamos a construir un sistema de inventario de productos utilizando el patrón Repository! Crearás una capa de abstracción que separa cómo se almacenan los productos de cómo se utilizan, facilitando el intercambio de implementaciones de almacenamiento sin cambiar la lógica de negocio.

Organizarás tu código en dos archivos:

  • product_repository.dart: Este archivo contiene tu entidad y las clases del repositorio. Comienza con una clase Product que tenga campos final para id (int), name (String) y price (double). Luego, define una clase abstracta ProductRepository que declare el contrato para las operaciones de datos: getById(int id) que devuelve un Product opcional, getAll() que devuelve una lista de productos, save(Product product) para almacenar un producto y delete(int id) para eliminar uno. Finalmente, crea un InMemoryProductRepository que implemente esta interfaz utilizando un Map<int, Product> para el almacenamiento.
  • main.dart: Importa tu archivo de repositorio y demuestra el patrón Repository en acción. Crea un InMemoryProductRepository pero almacénalo en una variable de tipo ProductRepository; esto muestra cómo tu código depende de la interfaz, no de la implementación. Guarda tres productos: Laptop (id: 1, price: 999.99), Mouse (id: 2, price: 29.99) y Keyboard (id: 3, price: 79.99). Luego, recupera e imprime el producto con id 2 en el formato Found: [name] - $[price]. Elimina el producto con id 1. Finalmente, imprime el recuento de los productos restantes como Products in stock: [count].

El poder de este patrón es que tu código principal solo conoce la interfaz ProductRepository. ¡Más adelante podrías crear un DatabaseProductRepository o ApiProductRepository e intercambiarlo sin cambiar nada de la lógica de negocio!

Salida esperada:

Found: Mouse - $29.99
Products in stock: 2

Hoja de referencia

El patrón Repository separa la lógica de acceso a datos de la lógica de negocio mediante la creación de una capa de abstracción entre su aplicación y las fuentes de datos.

Defina una interfaz para las operaciones de datos:

// Entidad
class User {
  final int id;
  final String name;
  User(this.id, this.name);
}

// Interfaz del repositorio
abstract class UserRepository {
  User? getById(int id);
  List<User> getAll();
  void save(User user);
  void delete(int id);
}

Cree implementaciones concretas que manejen el mecanismo de almacenamiento real:

// Implementación en memoria
class InMemoryUserRepository implements UserRepository {
  final Map<int, User> _users = {};

  @override
  User? getById(int id) => _users[id];

  @override
  List<User> getAll() => _users.values.toList();

  @override
  void save(User user) => _users[user.id] = user;

  @override
  void delete(int id) => _users.remove(id);
}

Utilice el repositorio a través de su interfaz:

void main() {
  UserRepository repo = InMemoryUserRepository();
  
  repo.save(User(1, 'Alice'));
  repo.save(User(2, 'Bob'));
  
  print(repo.getById(1)?.name);  // Alice
  print(repo.getAll().length);    // 2
}

La lógica de negocio depende únicamente de la interfaz, lo que le permite intercambiar implementaciones (por ejemplo, DatabaseUserRepository, ApiUserRepository) sin modificar el código que utiliza el repositorio.

Pruébalo tú mismo

import 'product_repository.dart';

void main() {
  // Crear repositorio - nota que el tipo es la interfaz, no la implementación
  // TODO: Crear un InMemoryProductRepository y almacenarlo en una variable ProductRepository
  
  // TODO: Guardar tres productos:
  // - Laptop (id: 1, price: 999.99)
  // - Mouse (id: 2, price: 29.99)
  // - Keyboard (id: 3, price: 79.99)
  
  // TODO: Recuperar el producto con id 2 e imprimir: "Found: [name] - $[price]"
  
  // TODO: Eliminar el producto con id 1
  
  // TODO: Imprimir el recuento de los productos restantes: "Products in stock: [count]"
}
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación Orientada a Objetos