Olá, pessoal!
Bom, este é o meu primeiro artigo aqui no iMasters, espero que gostem e que seja útil.
Os puristas dos Design Patterns podem não gostar muito do conteúdo deste artigo, pois ele foge um pouco da implementação padrão conhecida, que é definida por Martin Fowler no clássico Patterns of Enterprise Application Architecture.
Entretanto, segundo a minha concepção, não se trata de uma violação do padrão já conhecido, mas de uma variação. Muitas pessoas têm a ideia errônea de que os padrões são receitas prontas, que devem ser seguidos ao pé da letra.
A Gang of Four (Erich Gamma et al.) define um design pattern como:
Um Design Pattern sistematicamente nomeia, motiva e explica o design geral que identifica um problema recorrente no desenvolvimento de sistemas orientados a objetos. Ele descreve o problema, a solução, quando aplicar a solução e as consequências de seu uso. Também é possível que haja dicas de implementação e exemplos. A solução é um arranjo geral de objetos e classes que resolvem o problema. A solução é customizada e implementada para resolver o problema em um contexto particular.
Diante disso, reservo-me ao direito de apresentar aqui uma solução adaptada ao contexto do PHP e SGBDs relacionais mais comuns, como MySQL, PostgreSQL, SQLite e Oracle.
Por simplicidade, nosso estudo de caso hoje se restringe somente ao MySQL. Em um futuro artigo, podemos adicionar suporte a outros SGBDs sem mudanças drásticas na estrutura que mostrarei aqui hoje.
Definições
Table Data Gateway
O problema
Uma das partes mais tediosas e repetitivas do desenvolvimento é a geração de sentenças SQL para acesso a uma tabela. Especialmente no caso de operações como inserção, atualização e remoção, a estrutura das sentenças é praticamente a mesma para qualquer tabela, o que se altera são apenas os nomes dos campos. Operações de busca (SELECT) também podem ter muito em comum, quando o objetivo é apenas retornar os dados das tabelas, sem fazer nenhum tipo de operação sobre eles.
Além disso, apesar da linguagem SQL ser um padrão ANSI há quase 30 anos, a maioria dos SGBDs tem suas próprias implementações para certas operações. Por exemplo: para fazer casting de tipos no MySQL utiliza-se a função cast enquanto que no PostgreSQL isso pode ser feito utilizando-se a construção campo::tipo. Outro exemplo: o MySQL não possui sequências explícitas, tudo se resume ao modificador AUTO_INCREMENT, que internamente utiliza uma sequência. Oracle e PostgreSQL não possuem o modificador AUTO_INCREMENT e utilizam entidades que são chamadas sequências e podem ser tratadas de forma parecida com as tabelas.
Nota: nas versões mais recentes do Postgre existe o tipo SERIAL, que cria uma sequência para a tabela automaticamente e seta o valor padrão do campo serial para o próximo valor da sequência. Isso apenas poupa o desenvolvedor/DBA de realizar esta tarefa.
Com isso, fica complicado tornar aplicações independentes de SGBD realizando a implementação tradicional, escrevendo sentenças à mão. Outro problema é que a cada mudança na estrutura da tabela, todas as queries devem ser revisadas a fim de evitar erros de execução.
A solução
Abstrair o acesso aos dados de uma tabela no banco de dados mapeando-a a um objeto da aplicação. É importante ressaltar que o mapeamento é SEMPRE 1:1, ou seja, cada objeto diz respeito a uma única tabela.
O objeto que representa a tabela deve conter todas as informações necessárias para se realizar as operações fundamentais sobre ela, o famoso CRUD (**Create**, Retrieve, Update e Delete). Essas informações normalmente são: o nome da tabela, os nomes e tipos das colunas e a chave primária. Com isso, é possível executar todas as operaçẽos CRUD sem que o desenvolvedor precise utilizar uma única sentença SQL.
Row Data Gateway
O problema
Como representar registros de uma tabela do banco de dados na aplicação?
Soluções possíveis
Existem diversas soluções propostas para esse problema. Uma delas defende que, por facilidade na codificação, tanto os dados, quanto as operações sobre eles, quanto a lógica do negócio deve permanecer no mesmo objeto. Essa abordagem é chamada de Active Record. No mundo Java, esse pattern é bastante difundido através do framework Hibernate. Para PHP existe o Doctrine.
Eu pessoalmente não sou muito fã do Active Record por algumas razões:
- Ele viola o SRP;
- Apesar da codificação ser relativamente fácil, sua estrutura é deveras complexa;
- É impossível alterar o meio de armazenamento persistente sem ter que alterar toda a classe;
- Acredite, o responsável pela atividade de testes vai querer te matar. Se for você mesmo quem realiza os testes, você vai querer cometer suicídio.
No extremo oposto, outra abordagem nos diz que os objetos mapeados na linguagem de programação não devem ter “consciência” que podem ser armazenados de forma persistente em um banco de dados ou similares. Esses objetos “burros”, também chamados de Plain Old Objects, basicamente são devem ser totalmente dependentes de um mapeador para a persistência. A esse pattern dá-se o nome de Data Mapper.
Vantagens do uso do pattern Data Mapper:
- Separação clara de responsabilidades;
- Total desacoplamento entre o meio de armazenamento e a aplicação;
- Caso ocorram alterações no armazenamento, apenas o Mapper precisará ser alterado.
Desvantagens:
- O processo de criação dos mapeadores e dos objetos mapeados é normalmente manual, o que gera um trabalho braçal bem grande, muitas vezes com uma boa dose de CTRL+C/CTRL+V
- Imagine uma aplicação com 20 entidades. São no mínimo 40 classes na brincadeiras (20 mapeadores e 20 plain old xxxxx objects).
- Torna aplicações rodando sobre linguagens dinâmicas como o PHP bastante inflexíveis.
A não ser que você tenha uma boa chance de que a sua aplicação num futuro próximo irá parar de utilizar bancos de dados e vai utilizar outro meio de persistência, um Data Mapper é extremamente custoso.
A alternativa aos dois métodos apresentados é conhecida como Row Data Gateway (ou RDG para os íntimos =]). Na verdade, tanto este quanto o Data Mapper se tratam de refatorações do Active Record, visando uma melhor separação de responsabilidades. A diferença entre ambos é que com o pattern Row Data Gateway, os objetos que mapeiam os registros da tabela “sabem” que fazem parte de um contexto maior: uma tabela de banco de dados. Com base nesse conhecimento, o objeto pode executar operações sobre os próprios dados, sem saber, no entanto, como essas operações são feitas, pois as mesmas são delegadas a um objeto Table Data Gateway (TDG).
Agora é possível entender porquê um objeto RDG é dependente de um TDG, mas a recíproca não é verdadeira. No PHP, por exemplo, é muito fácil implementar o TDG e mapear os dados da tabela a simples arrays associativos. Embora, na minha opinião, estes sejam flexíveis demais para este fim, isso é perfeitamente possível. Entretanto, é mais comum vermos ambos os padrões implementados em conjunto, com o TDG agindo como uma Factory de objetos RDG.
Só para ressaltar: não é possível existir um objeto RDG sem que haja um objeto TDG associado a ele. Alguém aí já conseguiu pegar no ar um aspecto da implementação por essa afirmação? Desafio pra vocês, é coisa simples. Postem nos comentários.
Implementação
Muito bem, bonitão… Você falou, falou, falou mais um pouco, mas até agora eu não vi nada de prática aqui. Como que eu implemento isso tudo?
É fato que descrever o padrão é muito fácil. Difícil é fazer o trabalho sujo de implementá-lo de forma totalmente (ou quase) funcional.
Antes de entrar em detalhes do código, gostaria de mostrar o diagrama de classes simplificado da nossa estrutura:
Definindo as responsabilidades:
- Driver: repare que essa classe não figura em nenhum momento da descrição dos padrões. Na verdade, ela não faz parte do padrão, pois trata-se da camada mais baixa de acesso aos dados. É o único componente do sistema que vai lidar com a API do MySQL para PHP (mais especificamente, a extensão MySQLi). Todas as operações sobre o banco de dados obrigatoriamente passarão por esta classe.
- Table: é no nosso TDG. Ela utilizar-se-á de uma instância de Driver para realizar as operações sobre os dados da tabela.
- Row: é o querido RDG. Todas as operações sobre os seus dados são na verdade delegadas ao TDG. Irá se referenciar a Driver unicamente para recuperar o último ID inserido em uma query do tipo INSERT em uma tabela que possua chave primária com auto-incremento.
A “mágica” por trás de tudo
Nossa ideia aqui é poupar o desenvolvedor de definir a estrutura dos dados na aplicação, pois isso já está feito no banco de dados.
O que muita gente não sabe é que cada SGBD armazena metadados sobre a sua estrutura, os quais tornam possível o gerenciamento dos dados existentes na base. E a melhor notícia é que quase sempre é possível recuperar esses metadados. Para cada tabela, a partir de seu nome, precisamos obter a sua estrutura (colunas, tipos de coluna, valores padrão, chaves primárias, etc). No MySQL, isso é trivial, pode ser feito através do statement:
DESCRIBE tabela;
Em outros SGBDs, isso se torna um pouco mais complicado, é preciso procurar diretamente nas tabelas internas contendo os metadados, muitas vezes sua estrutura é um tanto confusa, todavia, é totalmente possível fazê-lo.
Vamos ao que interessa: códigos, códigos e mais códigos
Peço que não se assustem com o tamanho do exemplo. No total há aproximadamente 1000 linhas de código, mas boa parte disso é o trivial, com setters e getters. Caso haja alguma dúvida sobre questões de implementação, comentem que terei prazer em respondê-los.
Usos
Para o fim de testes, sugiro que utilizem a seguinte tabela:
Trata-se de um sistema de gerenciamento de um restaurante, a tabela que armazena os usuários do sistema.
Selecionando dados da tabela
// Supondo que o arquivo está no mesmo nível das classes require_once 'Driver.php'; require_once 'Table.php'; require_once 'Row.php'; error_reporting(E_ALL | E_STRICT); // Não se esqueça de configurar aqui!!! $connector = new MySQLi('localhost', '****', '****', '****'); $driver = new Driver($connector); $table = new Table('usuario', $driver); // Até aqui deve ser incluído em todos os arquivos de teste $allRows = $table->getAll(); var_dump($allRows); // Mostra todas as linhas da tabela // Selecionando colunas, ordenando e buscando uma certa quandidade limite de registros $rowsWithOptions = $table->getAll(array('cpf', 'nome_completo', 'cargo'), 'nome_completo ASC', 3); var_dump($row); // Selecionando dados por ID $row = $table->getById('87548965210'); var_dump($row);
Criando um novo registro
$newRow = $table->createRow(array( 'cpf' => '00000000000', 'nome_completo' => 'Franscisvânio Sá Silva', 'endereco' => 'Rua dos Bobos, 0', 'telefone' => '0000001111', 'senha' => 'acbd18db4cc2f85cedef654fccc4a4d8', 'cargo' => 'gerente' )); $newRow->save(); // Irá inserir um novo registro no banco de dados
Atualizando um registro
$row = $table->getById('11111111111'); $row->set('cargo', 'gerente'); // ou $row['cargo'] = 'gerente'; // Row implementa ArrayAccess (Se não sabe o que é isso, procure no manual, é bem interessante =]) $row->save(); // Irá atualizar o registro no banco de dados
Removendo um registro
$row = $table->getById('53627787392'); $row->delete(); // Remove o registro do banco
Outros usos
Não incluí uma tabela com auto-incremento nem com chave composta, mas ambas são igualmente suportadas. Se você possui alguma tabela com essas características, faça o teste:
Selecionando por ID de uma tabela com chave composta
// Suponha que a chave primária de MINHA_TABELA possui 2 campos: $table = new Table('MINHA_TABELA', $driver); $row = $table->getById(array(1, 'Foo')); // Dessa forma você deve fornecer os valores na ordem em que aparecem na tabela var_dump($row); $row = $table->getById(array( 'campo2' => 'Foo', 'campo1' => 1 )); // Dessa forma, utilizando os nomes dos campos como chave, você pode fornecer os valores em qualquer ordem
Inserindo dados em uma tabela com chave auto-incrementada
$table = new Table('OUTRA_TABELA', $driver); $row = $table->createRow(array( 'campo2' => 'valor2', 'campo3' => 'valor3', // ... )); // O valor da chave primária pode ser omitido, pois irá ser gerado automaticamente... $row->save(); var_dump($row->getPk()); // Irá retornar a chave autogerada pelo SGBD
Eu realizei a maioria dos testes aqui e está tudo em ordem aparentemente. Caso alguém encontre algum bug, peço que avise nos comentários para que eu possa corrigir.
Nem todas as features da estrutura estão explanadas nos exemplos, fica como desafio para quem quiser dar uma brincada com o código.
Por hoje é só pe-pessoal!!!