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.
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 claseProductque tenga camposfinalparaid(int),name(String) yprice(double). Luego, define una clase abstractaProductRepositoryque declare el contrato para las operaciones de datos:getById(int id)que devuelve unProductopcional,getAll()que devuelve una lista de productos,save(Product product)para almacenar un producto ydelete(int id)para eliminar uno. Finalmente, crea unInMemoryProductRepositoryque implemente esta interfaz utilizando unMap<int, Product>para el almacenamiento.main.dart: Importa tu archivo de repositorio y demuestra el patrón Repository en acción. Crea unInMemoryProductRepositorypero almacénalo en una variable de tipoProductRepository; 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) yKeyboard(id: 3, price: 79.99). Luego, recupera e imprime el producto con id 2 en el formatoFound: [name] - $[price]. Elimina el producto con id 1. Finalmente, imprime el recuento de los productos restantes comoProducts 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: 2Hoja 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]"
}
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
1Fundamentos de POO
Archivos externosLibrerías e importacionesIntroducción a la POOClases vs. ObjetosLa palabra clave thisMétodosVariables de instanciaConceptos básicos de constructoresResumen - Calculadora sencilla4Seguridad de nulos
Introducción a la seguridad de nulosNulables vs No nulablesLos operadores ? y !Palabra clave Late y Null SafetyOperadores Null-awareNull Safety en clasesResumen - Sistema de perfil de usuario7Clases abstractas e interfaces
Clases abstractasMétodos abstractosInterfaces en DartInterfaces implícitasImplementar vs. ExtenderInterfaces múltiplesRepaso - Calculadora de figuras10Colecciones y Genéricos
Visión general de List, Set y MapColecciones con seguridad de tiposClases genéricasMétodos genéricosRestricciones genéricasIterable e IteratorResumen - Almacenamiento genérico2Constructores en Dart
Constructor por defectoConstructores nombradosListas de inicializaciónConstructores constantesConstructores de fábricaConstructores de redireccionamientoResumen - Constructor de formas5Encapsulamiento
Miembros públicos vs. privadosLa convención del prefijo _Privacidad a nivel de libreríaProfundidad en Getters y SettersOcultamiento de informaciónResumen - Registros de estudiantes8Mixins
Introducción a los MixinsCreación de MixinsUso de múltiples MixinsPalabra clave on en MixinsMixin vs HerenciaMixin vs InterfazResumen - Sistema Animal11Métodos especiales
Sobrescritura de toString()Sobrescritura de hashCode y ==Interfaz ComparableMétodo call()Sobrescritura de noSuchMethodResumen - Colección personalizada14Patrones de diseño Parte 1
Introducción a los patrones de diseñoPatrón SingletonPatrón FactoryPatrón ObserverPatrón Strategy3Propiedades de clase
Miembros de instancia vs. estáticosCampos Final y ConstVariables LateMétodos y campos estáticosGetters y SettersResumen - Gestor de cuentas bancarias6Herencia
Herencia básicaLa palabra clave superSobrescritura de métodosLa anotación @overrideLa palabra clave final en clasesConstructores y herenciaResumen - Jerarquía de empleados9Polimorfismo
Fundamentos del polimorfismoPolimorfismo mediante interfacesComprobación de tipos en tiempo de ejecuciónLos operadores is y asPalabra clave covariantResumen - Procesador de pagos12POO Asíncrona
Futures y async/awaitFundamentos de StreamsStream ControllersConstructores asíncronosAsincronía en métodos de claseRepaso - Data Fetcher15Patrones de diseño Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositePatrón Repository