Zona .Net

Refatorando idéias, produzindo conhecimento.

Posts Tagged ‘Programação

DDD: Implementando Repository com Specifications

with one comment

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:

Diagrama

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.

Anúncios

Written by Hamon Vitorino

2 de setembro de 2009 at 10:37 pm