O propósito do padrão Repository vai além de simplesmente separar as responsabilidades relacionadas com a persistência. Ele é responsável pela mediação e o desacoplamento entre o Domain Model, ou seja, objetos de domínio, e as operações de persistência de cada um desses objetos.
Isso quer dizer que só faz sentido utilizar o padrão Repository em conjunto com o Domain Model? Exatamente, caso contrário o que ele estaria persistindo seria algo diferente de um objeto de domínio e o padrão perderia o sentido e acabariam sendo utilizados outros padrões de persistência como o Table Data gatway ou Data Access Object (também conhecido como DAO), que são orientados a tabelas.
Para entender o Repository é necessário compreender antes que existem dois caminhos em termos de design, um deles é de abstrair as regras de negócio em procedimentos, métodos, funções, transferindo o fluxo de execução entre cada uma delas, apenas passando parâmetros e sem a utilização de abstrações. De acordo com a obra clássica do Martin Fowler, Patterns of Enterprise Application Architecture, essa abordagem se chama Transaction Script e é conhecida por ser anêmica, ou seja, ainda que seja implementada utilizando uma linguagem Orientada a Objetos o design acaba sendo procedural.
Outra abordagem é o Domain Model, que é utilizado no Clean Architecture e no Domain-Driven Design, onde a complexidade é distribuída em objetos de domínio, protegendo a invariância por meio do encapsulamento e a exposição apenas do comportamento necessário para realizar a mutação de estado, ou seja, reduzindo o acoplamento e dessa forma a fragilidade.
Isso cria um problema: como fazer para persistir esses objetos de domínio?
O padrão Repository existe exatamente para isso, realizar a persistência sem que as outras camadas precisem se preocupar com isso.
class User {
private email Email;
private password Password;
private status: string;
updatePassword (newPassword: string) {
if (status === "blocked") throw new Error("Cannot change password of a blocked user");
this.password = new Password(newPassword);
}
getPassword () {
return this.password.getValue();
}
}
Repare que a classe User é composta por Email e Password, criando um grupo de objetos de domínio. No Domain-Driven Design isso se chama Aggregate e é exatamente esse grupo que é persistido no Repository.
Dessa forma, teremos um UserRepository:
interface UserRepository {
save (user: User): Promise;
update (user: User): Promise;
get (userId: string): Promise;
list (): Promise;
}
Ah, mas eu queria simplesmente mudar a senha, não posso ter um updatePassword em UserRepository?
Aqui entra o conceito de Domain Model e da proteção da invariância. Repare que utilizamos um Value Object para abstrair a senha e para definí-la é necessário causar uma mutação no objeto User que passará pelo objeto Password.
class Password {
private value: string;
constructor (password: string) {
if (password.length < 8) throw new Error("Minimum length is 8");
if (!password.matches(/\d+/g)) throw new Error("Password must have as least one number");
if (!password.matches(/[A-Za-z]+/g)) throw new Error("Password must have as least one letter");
this.value = value;
}
getValue () {
return this.value;
}
}
Se o método updatePassword existisse ele quebraria a invariância do objeto User, levando-o a um estado inválido, ou seja, permitindo que alguém modifique a senha de um usuário bloqueado ou utilize uma senha com menos de 8 caracteres e que não contém uma combinação de números e letras.
O padrão Repository tem em sua interface o objeto User, impedindo que qualquer mutação de estado seja feita sem passar pelas regras de negócio implementadas nos objetos de domínio, preservando a integridade do Domain Model.
Utilizar o padrão Repository não tem nada a ver com certo ou errado, é simplesmente resultado da escolha de uma estratégia de design, que também não está certa ou errada, simplesmente vai apresentar vantagens e desvantagens dependendo da situação. Em um domínio muito complexo, utilizar Transaction Script faz com que a complexidade se acumule e com que seja mais difícil manter e evoluir o código-fonte com o passar do tempo enquanto o Domain Model proporciona uma melhor distribuição da complexidade, facilidade em testar no nível de unidade mas ao mesmo tempo requer uma capacidade maior de abstração por parte da equipe.