DDD: Implementando Repository com Specifications
Já tinha lido alguns posts e artigos sobre Domain-Driven Design, mas ainda não tinha dado atenção suficiente ao assunto. Recentemente comecei a ler o livro Domain-Driven Design: Tackling Complexity int ther Heart of Software e, depois de alguns capítulos, percebi que um dos conceitos mais importantes que se pode extrair da leitura é como se comunicar com os elementos do domínio de forma clara, declarativa e flexível. A solução apresentada é o pattern Repository, que delega ao cliente a responsabilidade de construir queries e submetê-las ao repositório para satisfação, além de prover separação de dependência de única direção entre o domínio e as camadas de acesso a dados.
No post passado, falei um pouco sobre como algumas implementações de Repository se assemelham a implementações de DAO, por expor um método para cada critério de busca, e como isso diverge do que me parece ser o objetivo do pattern. Então, ao invés disso, a implementação a seguir utiliza o pattern Spefication, para criação de uma linguagem declarativa de queries.
Durante a leitura do livro, tenho buscado implementar alguns conceitos em uma aplicação simples e pretendo compartilhar essa experiência. Então, sempre que achar um tema interessante, vou trazer uma implementação.
O domínio da a aplicação é um player de música, capaz de criar listas de reprodução dinâmicas a partir de uma especificação.
A figura abaixo traz os elementos do domínio mapeados, até agora, para essa aplicação:

Figura 1: Domínio
A interface primária do repositório tem um método Match, que recebe uma especificação como parâmetro de entrada e retorna uma coleção de objetos do domínio que satisfazem à especificacão.
public interface IRepository<T>
{
IEnumerable<T> Match(Specification<T> specification);
}
public interface ISongRepository : IRepository<Song>
{
IEnumerable<Song> getSongsFromAlbum(string album);
IEnumerable<Song> getSongsFromGenre(string genre);
...
}
public class SongRepository : ISongRepository
{
internal List<Song> songs;
public SongRepository()
{
songs = createRepositoryData();
}
private List<Song> createRepositoryData()
{
...
}
#region IRepository<Song> Members
public IEnumerable<Song> getSongsFromAlbum(string album)
{
Specification<Song> fromAlbumSpec = new SongFromAlbumSpecification(album);
return this.Match(fromAlbumSpec);
}
public IEnumerable<Song> getSongsFromGenre(string genre)
{
Specification<Song> fromGenreSpec = new SongFromGenreSpecification(genre);
return this.Match(fromGenreSpec);
}
public IEnumerable<Song> Match(Specification<Song> specification)
{
List<Song> resultSet = new List<Song>();
foreach (Song song in songs)
{
if (specification.IsSatisfiedBy(song))
resultSet.Add(song);
}
return resultSet;
}
#endregion
}
Uma das grandes vantagens de se usar Specification é a possiblidade de compor critérios de busca sem a necessidade de alteração da interface do repositório. Essa composição se dá pela combinação de especificacões através de operações lógicas.
public interface Specification<T>
{
bool IsSatisfiedBy(T candidate);
Specification<T> And(Specification<T> other);
Specification<T> Or(Specification<T> other);
Specification<T> Not();
}
public abstract class CompositeSpecification<T> : Specification<T>
{
#region ISpecification<T> Members
public abstract bool IsSatisfiedBy(T candidate);
public Specification<T> And(Specification<T> other)
{
return new AndSpecification<T>(this, other);
}
public Specification<T> Or(Specification<T> other)
{
return new OrSpecification<T>(this, other);
}
public Specification<T> Not()
{
return new NotSpecification<T>(this);
}
#endregion
}
A partir dessas interfaces, devemos buscar identificar quais os principais critérios de busca utilizados pela aplicação e implementá-los como pequenas regras, para possibilitar a criação de novas regras via composição. Para ilustrar essa idéia, criei as especificações SongFromAlbumSpecification, SongFromArtistSpecification e SongTopRatedSpecification.
public class SongFromArtistSpecification : CompositeSpecification<Song>
{
private string artist;
public SongFromArtistSpecification(string artist)
{
this.artist = artist;
}
public override bool IsSatisfiedBy(Song candidate)
{
return candidate.Artist == artist;
}
}
public class SongFromAlbumSpecification : CompositeSpecification<Song>
{
private string album;
public SongFromAlbumSpecification(string album)
{
this.album = album;
}
public override bool IsSatisfiedBy(Song candidate)
{
return candidate.Album == album;
}
}
public class SongTopRatedSpecification : CompositeSpecification<Song>
{
public SongTopRatedSpecification()
{}
public override bool IsSatisfiedBy(Song candidate)
{
return (candidate.Rate == 5 );
}
}
Podemos combinar essas especificações de várias maneiras diferentes de forma a construir conceitos mais complexos. Digamos que queiramos construir uma playlist que contenha todas as músicas de um determinado artista, e que sejam as que possuem melhor classificação.
ISongRepository repository = new SongRepository();
Specification<Song> topRated = new SongTopRatedSpecification();
Specification<Song> fromMaiden = new SongFromArtistSpecification("Iron Maiden");
Specification<Song> topRatedFromMaiden = topRated.And(fromMaiden);
IEnumerable<Song> selectedSongs = repository.Match(topRatedFromMaiden));
Ou uma playlist contendo músicas com melhor classificação de mais de um artista.
Specification<Song> fromCake = new SongFromArtistSpecification("Cake");
Specification<Song> topRatedFromCake = topRated.And(fromCake);
Specification<Song> topRatedFromCakeOrMaiden = topRatedFromCake.Or(topRatedFromMaiden);
IEnumerable<Song> selectedSongs = repository.Match(topRatedFromCakeOrMaiden));
Identificar os principais critérios de busca utilizados pela aplicação e mapeá-los de maneira extensível são as principais tarefas na construção de uma boa linguagem de comunicação com o domínio, pois, a partir desses critérios, vários outros conceitos podem e devem emergir.
O principal intuito aqui é apresentar implementações dos conceitos expostos no livro, portanto, sugestões e críticas são bem vindas.
Brevemente disponibilizarei o fonte da aplicação.
Até agora não entendi quais sao os ganhos usando repository patterns. Quais problemas eu resolveria.
Alexsandro
22 22UTC abril 22UTC 2010 em 4:30 pm