Ouracademy

El patron Repository: implementacion y buenas practicas

El patrón Repository (o Repositorio, en español) es probablemente uno de los más populares entre los patrones catalogados por Eric Evans. ¿La razón? Muy probablemente se debe a que uno de los temas más tocados en el desarrollo de software es la persistencia de datos...un tema que está fuertemente relacionado al patron Repository.

Orientacion a Objetos y otros paradigmas (Relacional y NoSQL)

Al usar el enfoque de programación Orientado a Objetos, uno de sus principales problemas se da cuando se trabaja a su vez con un sistema de almacenamiento externo, como una base de datos relacional o incluso NoSQL, ya que no es facil adecuar (o mapear) el formato entre ellas, a esto a menudo se le conoce como el problema de impedance mismatch, por ello toda una serie de herramientas y técnicas han surgido para lidiar con ella.

En muchos sistemas, en especial los empresariales (enterprise systems) -aquellas que realizan tareas críticas de negocio pero usualmente a una escala no tan alta- a menudo se usan herramientas de mapeo de objetos (ORM en caso de una BD relacional, ODM en caso de BD documental...) como Hibernate (en Java), Eloquent o Doctrine (en PHP), TypeORM (en javascript), etc. Estas herramientas hacen un excelente trabajo en lidiar con todos los tornillos y tuercas que conlleva convertir tipos y formatos de datos (Objetos a Tablas o Documentos), sin embargo, a menudo existen problemas...uno de ellos es la tarea de guardar (persistir) y obtener objetos de nuestro Dominio (esos objetos como Pedido, Producto...) de una manera entendible relacionada al negocio, es decir siguiendo el Lenguaje Ubicuo del sistema que desarrollamos.

DAO, Data Mappers y el patrón Repository

Por muchos años, la forma usual de persistir un Modelo de Dominio era a traves de usar objetos especializados, como usar el patron DAO y otros tipos de Data Mappers, para convertir objetos de tu modelo de dominio a sus equivalentes persistidos (e.g. tablas). Estos patrones de diseño resuelven muchos de los díficiles problemas de diseño como el acoplamiento y la cohesión, sin embargo estos pertenecen a la capa de Infrastructura, y como tal integrarlas al Lenguaje Ubicuo no es algo sencillo.

El patron Repository, como lo cataloga Eric Evans y Martin Fowler, ofrece una buena forma de integrar las necesidades de persistencia de datos y el Lenguaje Ubicuo. En su libro, Domain-Driven Design, Evans define el patron Repository como un "mecanismo para encapsular el comportamiento de almacenamiento, obtención y búsqueda, de una forma similar a una colección de objetos (si parecida a una lista o arreglo)". Es más, una caracteristica importante es que estos objetos (los repositorios) son fácilmente vistos como parte del Lenguaje Ubicuo es decir son fáciles de entender para los expertos del dominio!

Práctica #1: Poniendo nombres

El concepto de ser el Repository como simplemente una lista de objetos suena simple de entender, pero de alguna forma u otra es muy común ver que las clases Repository que escribimos terminen teniendo métodos que no se relacionan para nada a una lista. Varias veces cuando hago coaching a equipos sobre Domain-Driven Design, he visto una y otra vez clases que empiezan muy bien siendo Repositorios y terminan siendo versiones feas de DAOs...

class OrderRepository {
    List<Order> getOrders(Account a){...}
}

Una de las mejores formas de evitar este problema es al poner un buen nombre a tus clases, de tal forma que las haga super faciles de identificar cuando un método no debe estar dentro del Repository. Hace muchos años atrás, Rodrigo Yoshima, me mostró una forma muy interesante de nombrar Repositorios. En vez del estilo de nombrado usual, como se ve arriba, a él le gustaba modelar sus clases como:

class AllOrders {
    List<Order> belongingTo(Account a){...}
}

Lo de arriba pareciese que fuese un cambio pequeñito, pero en mi experiencia, es extremadamente útil en mantener Repositorios sanos.

Por ejemplo, veamos 2 formas distintas de implementar un Repository. Ambas contienen un método que consideró que debe estar en otro lugar, en otra clase. En cual de las 2 implementaciones crees que es más fácil saber donde está el error?

//estilo clasico de nombrado
class UserRepository{
 User getUserByUsername(String username){...}
 void submitOrder(Order order){...}
}

//codigo que lo llama- codigo cliente
User u = userRepository.getUserByUsername("pcalcado");
userRepository.submitOrder(new Order());
//estilo de nombrado de Yoshima
class AllUsers{
 User withUsername(String lousernamegin){...}
 void submitOrder(Order order){...}
}

//codigo que lo llama- codigo cliente
User u = allUsers.withUsername("pcalcado");
allUsers.submitOrder(new Order());

Parece que algo no anda bien con allUsers.submitOrder(new Order()).

Para mi, usar un vocabulario más preciso cuando se nombra a objetos y métodos hace mucho más fácil atrapar incongruencias como la de arriba. Como un corolario, usar nombres muy genericos o prefijos como get, find o retrieve hacen mucho más díficil encontrar estos malos olores (bad smells) en nuestros modelos.

Práctica #2: Evitando una explosión de métodos

Más importante que usar un vocabulario reestringido (no usar esos get, find ...), un Repository bien definido debería exponer conceptos del Modelo de Dominio como parte de su interfaz pública.

Como ejemplo, asumamos que una regla de negocio dice que todo pedido hecho en un fin de semana tiene una carga de 10% más. Ahora si quisieramos obtener esos tipos de pedidos, podriamos hacer algo como:

// pedidos sobrecargados
List<Order> surchargedOrders = allOrders.placed(user, IN_A_SATURDAY);
surchargedOrders.addAll(allOrders.placed(user, IN_A_SUNDAY));
return surchargedOrders;

Estamos dejando conceptos clave aquí, que podrían aparecer en cualquier otra parte del sistema a medida que este crezca, que pasa si necesitamos los pedidos sobrecargados en muchos métodos ...bueno esto conlleva a que dupliquemos este mismo código en muchas partes, claro para 1 caso esta bien, pero para muchos no. Si un pedido sobrecargado es un concepto importante en tu dominio, deberías asegurarte que los objetos que implementan el concepto - el Repository es uno de esos objetos- pongan ese término como primera-clase. Esta es una de las ideas centrales de Domain-Driven design: hacer que conceptos implicitos del dominio sean explicitos, usualmente al modelarlas (ser parte de) como objetos y métodos.

Bajo ello, sería mejor hacer:

class AllOrders {
    surchargedOrders(User user) {
        return this.placed(user, IN_A_SATURDAY);
    };

    // quiza cambiarlo a private para que solo usen el surchargedOrders
    placed(User user) {
        // ...
    }
}

// lo llamarian de esta forma
return allOrders.surchargedFor(user);

Este enfoque trae sus propios problemas. Asumiendo que un sobrecargo es uno de los muchos estados que puede tener un pedido (por ejemplo pedidos con descuento, atrasados o de clientes premium), siguiendo este patron, nos llenariamos de métodos (una explosión!!), un método para cada estado. Claro que esto no es un problema si tenemos pocos estados, pero en sí este enfoque no escala muy bien para atributos con muchos estados posibles.

Evans y Fowler sugieren una forma para enfrentar este tipo de problemas: el patron Specification. Evans describe una Specification como "un [objeto] predicado que determina si un objeto cumple o no un criterio". Para evitar la explosión de métodos en nuestro Repository, podriamos agregar un método que tome como parametro un objeto Specification y retorne los objetos que cumplen con ella. Por ejemplo, podriamos hacer algo como:

return allOrders.thatAre(user, OrderSpecifications.SURCHARGED);

Además existe otra estrategia que me gusta: usar multiples Repositorios. En nuestro ejemplo, no hay razon para no tener 2 Repositorios, uno para todas las ordenes y otra para solo aquellas con sobrecargo.

Una forma de implementarla es parametrizando el Repository cuando creemos el objeto, algo como:

class Orders {
    private Orders.Status status = null;

    public Orders(){
        this(Order.Status.ANY);
    }

    public Orders(Order.Status status){
        this.status = status;
    }

    public List<Order> from(User user) {...}
    }
}

podriamos usarla así:

Orders allOrders = new Orders();
Orders surchargedOrders = new Orders(Order.Status.SURCHARGED);

//retorna todos los pedidos
return allOrders.from(user);

//retorna solo pedidos con sobrecarga
return surchargedOrders.from(user)

Podríamos implementar cada variación como una subclase. En este diseño, es importante asegurarse que no reemplacemos la explosión de métodos con una explosión de clases.

Práctica #3: Solo un Tipo

Otro problema común sucede cuando los Repositorios empiezan a parecerse a un objeto "base de datos" generico en vez de una colección cohesiva. Por ejemplo, en un sistema que trabaje, teniamos en nuestro Modelo de Dominio un Repository así:

public interface AllServices {
    List<Service> belongingTo(List<Account> accounts);
    Service withNumber(String serviceNumber);
    List<Service> relatedTo(Service otherService);
}

Despúes de muchas Sprints, estabamos avanzando historias de usuario que necesitaban crear otros objetos (a parte de nuestro objeto Service). Un compañero de mi equipo pensó "Oh, no creare una clase solo para este objeto. Solo agregaré un método aquí solo por ahora...". Eso sucedio varias veces, el principio YAGNI se fue para otro lado 😂, despues de unas semanas nuestro Repository se miraba como:

public interface AllServices {

    List<Service> belongingTo(List<Account> accounts);

    Service withNumber(String serviceNumber);

    List<Service> relatedTo(Service otherService);

    List<Product> allActiveProductsBelongingTo(List<Account> accounts);

    List<Product> allProductsBelongingTo(List<Account> accounts);

    ContractDetails retrieveContractDetails(String serviceNumber);
}

Algo interesante que paso es que seguiamos las convenciones de nombrado de Yoshima. Claro los métodos se leian raro cuando te ponias a pensar en el nombre del tipo (AllServices.allProductsBelongingTo??). Aprendí que nada para a un ingeniero cuando no se molesta refactorizar ...

// mind = blown 😂
AllServices allProducts = new AllServices();
// ...
return allProducts.allActiveProductsBelongingTo(accounts);

Podemos clasificar como design smell cuando los métodos de un Repository retornan más de un único tipo. Esta bien retornar tipos base como enteros, strings o booleanos, pero si tu Repository retorna más de un tipo de objeto de dominio, será mejor si las divides en colecciones distintas:

public interface AllServices {
    List<Service> belongingTo(List<Account> accounts);
    Service withNumber(String serviceNumber);
    List<Service> relatedTo(Service otherService);
}

public interface AllProducts {
    List<Product> activeBelongingTo(List<Account> accounts);
    List<Product> belongingTo(List<Account> accounts);
}

public interface AllContractDetails {
    ContractDetails forServiceNumber(String serviceNumber);
}

No solo Persistencia

El principal beneficio de usar Repositorios es hacer explicito de donde vienen los objetos y hacerlas parte del Lenguaje Ubicuo. Aunque los Repositorios se usen para modelar la persistencia de objetos en bases de datos o cosas similares, está no es el único lugar donde son útiles. Los Repositorios pueden ser usado para implementar colecciones en memoria (muchas veces no necesita guardarlo en una BD), pueden ser utiles para retornar ValueObjects:

public interface Currencies {
    of(Country contry); // obtiene la moneda de un pais
    // ..
}

E incluso encapsular código (del cliente) usado para invocar servicios remotos.

Traducido del articulo original de Phil Calçado: How to write a Repository

Cambios y revisiones:

23/02/2020: improve readability

11/08/2019: nuevo post el patron repositorio

Si te fue útil este artículo, por favor compártelo. Apreciamos los comentarios y el aliento.
Compartelo por:

Quiza te pueda interesar...

Mock, Stub, Fake, Dummy, Spy

Una historia de patrones de testing del articulo de Uncle Bob Martin el pequeño mocker

Cuando crear un Tipo de dato?

string, float, date...pero por que no crear tu propio tipo de dato?

¿Existe tal cosa llamada análisis orientado a objetos?

Una traducción del articulo de Martin Fowler: Is There Such a Thing as Object-Oriented Analysis? Recientemente, estaba dando un taller de…