garotosopa

Fluent Interface no PHP

Publicado em OOP, PHP por garotosopa em Outubro 29, 2008
Uma implementação mais recente, já com interface para consulta pelo usuário, está disponível no post sobre Domain Specific Language externa.

Esses dias testei o uso de Fluent Interface para consulta de dados de forma semântica, tentando criar uma API legível e reutilizável que eventualmente servisse diretamente ao usuário.

Em uma API comum, uma classe de negócio tem diversos métodos que retornam uma lista de acordo com diferentes critérios. As consultas ficam próximas do seguinte:

$alunos = new Alunos();
 
$alunosDoRio = $alunos->getPorCidade('Rio de Janeiro');
/* … */
$lista = $alunos->getMatriculadosPorCursoCidade('Enfermagem', 'Rio de Janeiro');
/* … */
$emOrdem = $alunos->getMatriculadosPorCursoCidade('Enf…', 'Rio…', 'nome');

Em um sistema real são muitas as variações de critérios e a classe acaba crescendo em quantidade de código e dificuldade de uso e manutenção.

A proposta é eliminar a necessidade de um método para cada tipo de consulta. Os critérios apenas configuram a consulta que será realizada uma única vez quando necessário, possibilitando que esses critérios de busca funcionem independentemente.

As chamadas passam a ser realizadas assim:

$alunos = new Alunos();
 
$alunos->daCidade('Rio de Janeiro');
/* … */
$alunos->emOrdem()
       ->daCidade('Rio de Janeiro')
       ->matriculados()
       ->noCurso('Enfermagem');

Para conseguir esta API é preciso:

  1. encadear os métodos
  2. ter a possibilidade de configurar a query dinamicamente
  3. identificar quando a configuração termina para então realizar a consulta.

Ainda que existam outras técnicas para refatorar a primeira API, o uso de Fluent Interface é um avanço significativo até a implementação de Domain Specific Language para uso do usuário.

Uma implementação mais recente, já com interface para consulta pelo usuário, está disponível no post sobre Domain Specific Language externa.

Method Chaining

O primeiro passo é perceber a simplicidade do encadeamento de métodos. Geralmente as propriedades de uma classe são definidas por métodos setters que não retornam nada. O objetivo é retornar o próprio objeto a partir de cada um desses métodos.

Esta técnica por si só apenas reduz a quantidade de código uma vez que não precisamos repetir a instância do objeto para cada setter.

Fluent Interface

Feito o encadeamento, é necessária uma mudança conceitual. O ponto principal é que a nomenclatura dos métodos deixe que a interface flua naturalmente.

Renomeando os métodos e retornando o próprio objeto é possível fazer a chamada de forma amigável:

class Alunos {
    // anteriormente chamado de setFiltroCidade
    public function daCidade($cidade) {
        echo "Alunos serão filtrados pela cidade $cidade";
 
        return $this;
    }
 
    // anteriormente chamado de setOrderByNome
    public function emOrdem() {
        echo "Alunos serão ordenados pelo nome";
 
        return $this;
    }
}
 
$alunos = new Alunos();
$alunos->daCidade('Rio de Janeiro')
       ->emOrdem();

Acima de tudo é importante manter consistência para que fique claro quais são as classes que trabalham com Method Chaining, evitando que o programador espere o próprio objeto como retorno equivocadamente.

Nestes exemplos será utilizado o prefixo Catalogo como tentativa de identificar essas classes. O uso de namespaces seria extremamente bem-vindo se a linguagem permitisse.

Montando a query

Diferente de uma API tradicional, os métodos anteriores devem apenas configurar o critério da consulta.

Mesmo sendo possível montar a SQL manualmente, uma camada de persistência que permita queries orientadas a objeto diminui muito a complexidade do código com o qual você precisa se preocupar. Os exemplos utilizam a modesta Zend_Db, mas uma biblioteca mais madura pode ser necessária no futuro.

Sobre o Zend Framework, basta saber que cada tabela é representada por uma classe que estende Zend_Db_Table_Abstract. Nessa classe, o método select retorna uma representação de SELECT na tabela, que pode ser configurada até o momento de executar a SQL.

Cada catálogo utilizará uma classe abstrata como base e implementará um método para identificar a tabela à qual está associado:

abstract class Catalogo_Abstract {
    private $_table;
    private $_select;
 
    abstract protected function _factoryTable();
 
    public function table() {
        if (null === $this->_table) {
            $this->_table = $this->_factoryTable();
        }
 
        return $this;
    }
 
    public function select() {
        if (null === $this->_select) {
            $this->_select = $this->table()->select();
        }
 
        return $this->_select;
    }
}
class Model_Aluno_Table extends Zend_Db_Table_Abstract {
    protected $_name = 'aluno';
}
class Catalogo_Aluno extends Catalogo_Abstract {
    protected function _factoryTable() {
        return new Model_Aluno_Table();
    }
 
    public function daCidade($cidade) {
        $this->select()->where('aluno.cidade = ?', $cidade);
 
        return $this;
    }
 
    public function emOrdem() {
        $this->select()->order('aluno.nome ASC');
 
        return $this;
    }
}

Desta forma já é possível combinar os métodos para consulta de alunos:

$alunos = new Catalogo_Alunos();
 
$alunos->emOrdem();
/* … */
$alunos->daCidade('Rio de Janeiro');
/* … */
$alunos->emOrdem()->daCidade('Rio de Janeiro');
/* … */
$alunos->daCidade('Rio de Janeiro')->emOrdem();

As últimas duas consultas resultariam na seguinte SQL:

SELECT aluno.*
FROM aluno
WHERE (aluno.cidade = 'Rio de Janeiro')
ORDER BY aluno.nome ASC

Determinando o momento da consulta com SPL

Até então, os dois métodos do catálogo de alunos apenas configuram a consulta e retornam o próprio catálogo. Falta uma forma de executar a query.

Uma solução é adicionar um método consultar que vai utilizar a SQL gerada e retornar a lista de alunos. Esse método, contudo, não impressionaria em nada as gatinhas, sem contar que não fluiria tão bem se comparado a uma frase em português.

Tratando-se de um catálogo, espera-se dele uma lista de objetos que serão iterados em um loop. No cenário ideal, o código abaixo deve ser suficiente:

$alunos = new Catalogo_Aluno();
 
$alunos->emOrdem()
       ->daCidade('Rio de Janeiro');
 
foreach ($alunos as $aluno) {
    echo $aluno->nome, '<br />';
}

E esse de fato é o cenário real (Ohhh).

Para controlar a iteração de um objeto em um foreach, o PHP dispõe da interface Iterator da Standard PHP Library. Quando utilizada, é necessário implementar os seguintes métodos, que serão chamados nessa ordem:

  1. rewind: reinicia a iteração;
  2. valid: determina se a iteração deve continuar;
  3. current: retorna o valor da iteração atual;
  4. key: retorna o índice da iteração atual;
  5. next: avança a iteração.

A consulta no Zend_Db retorna um objeto Zend_Db_Table_Rowset_Abstract, que já implementa a interface Iterator da SPL. Como a iteração no catálogo deve representar uma iteração neste rowset, basta direcionar os métodos para o objeto da consulta.

abstract class Catalogo_Abstract implements Iterator {
    /* complementando o código anterior … */
 
    private $_rowset;
 
    public function rowset() {
        if (null === $this->_rowset) {
            $this->_rowset = $this->table()->fetchAll($this->select());
        }
 
        return $this->_rowset;
    }
 
    public function rewind() {
        return $this->rowset()->rewind();
    }
 
    public function valid() {
        return $this->rowset()->valid();
    }
 
    public function current() {
        return $this->rowset()->current();
    }
 
    public function key() {
        return $this->rowset()->key();
    }
 
    public function next() {
        return $this->rowset()->next();
    }
}

Para ficar mais claro como os métodos são chamados, vale a pena dar uma olhada nestes slides.

O problema do contexto

O filtro por cidade foi um exemplo simples para ilustrar o conceito de Fluent Interface, imaginando que exista a coluna cidade na tabela de alunos, sem chave estrangeira. No mundo real é mais complicado que isso.

No primeiro exemplo lá em cima, a proposta era filtrar os alunos matriculados no curso de Enfermagem, mas, na minha realidade, não existe vínculo direto entre curso e aluno, apenas pela tabela de matrícula.

A solução que encontrei foi criar um catálogo específico de matrículas que é instanciado pelo catálogo de alunos, já criando o relacionamento entre as duas classes, resultando em um JOIN entre as três tabelas.

Para evitar que este texto ficasse ainda mais longo, separei os detalhes em um post específico sobre problema de contexto nas Fluent Interfaces.

Domain Specific Language

Talvez ainda não pareça justificável todo o trabalho que uma Fluent Interface consistente vai gerar.

A grande vantagem só ficou clara pra mim quando o Eclipse começou a auto-completar os métodos dos catálogos. Se estou no catálogo de alunos, ele já mostra o filtro de cidade. Se eu escrevo a cidade, ele ainda permite filtrar pelos já matriculados. Uma vez na matrícula, posso filtrar pelo curso, e assim por diante.

Isso é o mínimo que se espera de uma IDE que ocupa tanta memória. Mas e se o usuário tivesse a mesma facilidade?

Ao invés de me ligarem falando “Garoto, preciso de uma lista de alunos da região norte inscritos essa semana no curso de Gestão em Saúde pelo edital da UAB que tenham blah blah blah”, a (quase) mesma fala seria digitada numa caixa de texto com auto-complete.

Não que o problema não possa ser resolvido com um formulário gigante incluindo todos os filtros de consulta possíveis, mas estamos falando de usabilidade. Interfaces amigáveis para o usuário, código amigável para o programador. Quem não achou o plugin Ubiquity minimamente interessante?

E pra quem concorda que digitar a busca nem sempre é a melhor opção, os catálogos seriam facilmente exportados para ambientes visuais. Combinados com ações específicas de cada entidade (como matricular aluno, aprovar inscrição, desativar tutor…), o código passa a ser extremamente plugável.

Para tornar isso possível, a idéia é utilizar reflexão nas classes e expor os métodos na interface do usuário. Assim que parar de fazer sol na praia e eu tiver algum tempo, vou tentar expandir estes exemplos para DSL externa. O post de Domain Specific Language externa no PHP demonstra a implementação.

De qualquer forma, no fundo vou sentir falta das tais ligações.

Exemplo completo

Uma implementação mais recente, já com interface para consulta pelo usuário, está disponível no post sobre Domain Specific Language externa.

Referências

9 Respostas

Subscribe to comments with RSS.

  1. Samuca said, on Outubro 29, 2008 at 9:50 pm

    Muuuuuito foda esse artigo, achei interessantíssimo, e farei meus testes aqui.

    Grande abraço.

  2. Guilherme Chapiewski said, on Outubro 29, 2008 at 9:53 pm

    Show de bola ;)

  3. Lawrence Lagerlof said, on Outubro 30, 2008 at 10:53 am

    Nossa. Estou para ver um manual tão bem escrito. Excelente didática, foi direto ao ponto, sem enrolação. Exatamente o que os programadores procuram.

    Valeu!

  4. Carlos Kazuo said, on Novembro 3, 2008 at 4:51 pm

    Muito interessante o artigo! =]

  5. Felipe Lucio said, on Novembro 10, 2008 at 4:43 pm

    Muito bom o artigo cara!
    Simples, direto e com exemplos!

    Parabéns e valeu!

  6. Leandro Silva said, on Novembro 19, 2008 at 4:45 pm

    hehehe… bem interessante… parabéns!

  7. Jonathas Scot said, on Dezembro 10, 2008 at 7:19 pm

    Diogo, como nosso amigo lawrence fala: “você foi arrasadoramente arrasador”.

    Muito bacana seu post, inteligível até para o Designer /pseudo-programador-junior-mirim aqui.

    Show de bola. O melhor foi ver fucionando na sua máquina.

    Parabéns.

  8. Bruno Viana said, on Janeiro 19, 2009 at 8:00 pm

    Muito bom seu artigo. Meu deu várias idéias para melhorar o framework interno da empresa que trabalho.

    Meus parabéns!

  9. Micael Estrázulas said, on Fevereiro 10, 2009 at 9:35 am

    Excelente artigo. Parabens pelo blog


Deixe um comentário