Em relação à orientação a objetos, queries devem ter uma classe própria ou podem ficar em uma classe específica? – sql banco-de-dados orientação-a-objetos

Pergunta:


O que eu tenho é o seguinte: uma classe usuário e uma classe de conexão com o banco.

Preciso inserir um usuário em um banco de dados, a queryinsert into usuarios values (..)” deve ficar em minha classe de usuários ou na classe de banco de dados?

Penso no seguinte, eu estou usando MySQL, se um dia precisar usar outro banco de dados e que utilizar uma diferente sintaxe SQL, eu terei que alterar todo meu programa se deixar na classe de usuário, porém se deixar na classe de conexão, eu poderia apenas criar uma outra classe de conexão e modificar as queries, porém achei isso confuso, pelo fato de que eu estaria atribuindo à classe banco de dados algo que seria da classe usuários, ou não?

Autor da pergunta Renan Cavalieri

Comunidade

Não há regra que determine isto. Principalmente o paradigma de orientação a objetos nada tem a dizer sobre isto. É possível chegar a uma conclusão adotando outros conceitos.

Como não há regra, tudo pode estar certo. É possível fazer tanto um, quanto outro. Mas também é possível ter outras opções.

Opção de separação de conceitos

Pode ter uma classe usuario que apenas mantém a estrutura de dados (DTO) e uma classe para manipular o acesso ao banco de dados para esta classe. Isto atende ao princípio de coesão e desacoplamento das funções. Muitas vezes esta classe pode ser chamada de DAOUsuario já que ela adota o padrão de data access objects (não gosto deste tipo de nomenclatura). Isto vai ao encontro do princípio da responsabilidade única.

Ou pode melhorar ainda mais e implementar o padrão de repositório. Pode tentar descobrir a diferença entre as abordagens em uma pergunta existente (tenho ressalvas nela).

Fique atento porque da maneira que parece querer fazer é importante separar bem o que é regra de negócio e o que é o acesso aos dados propriamente ditos. O padrão acima facilita isto. Mas ele não centraliza tudo em um ponto. Vide comentário do mgibsonbr.

Como diz a resposta do lbotinelly, procure separar os conceitos. Isto é o que realmente trará flexibilidade ao software. Mas não tem almoço grátis. Organizar traz suas dificuldades.

Modularizar

Ao contrário da crença popular (justificável), OOP incentiva a modularização e não o agrupamento das funcionalidades. A modularização é que realmente permitiu os softwares ficarem melhores, mais organizados, mais fáceis de dar manutenção. E a modularização é possível em qualquer paradigma. Calhou das pessoas aprenderem fazer isto melhor em OOP, mas é uma questão cultural.

Devo agrupar tudo em um lugar?

A conexão e manipulações gerais do banco de dados deveriam ficar em outra classe.

É raro colocar tudo na classe do banco de dados. Na verdade nunca vi em sistemas bem projetados. Isto causa um alto acoplamento e a classe vira um god object. Isto sim iria contra um dos princípios do paradigma OO. Isto dificulta a manutenção e a extensibilidade da aplicação. A orientação a objeto foi criada justamente para evitar os códigos que anteriormente eram agrupados em um único local (ainda que não precise deste paradigma para fazer isto). Isto dá uma falsa sensação de organização. Isto é programação procedural na sua pior forma.

Separando o acesso à tabela do banco, da entidade que ela representa e do controle global do banco até facilitaria a troca de um sistema gerenciador de banco de dados por outro. Facilitaria o teste formal.

Abstrair o meio de armazenamento

Sugiro utilizar algo que abstraia o uso do banco de dados, como é o caso dos ORMs. Ou avaliar se realmente precisa deixar isto em aberto. É muito comum as pessoas acharem que precisam disto e nunca utilizarem de fato. Isto provavelmente ocorre por elas não confiarem na própria decisão. Claro que há casos de real necessidade de ter outros bancos disponíveis, em softwares genuinamente genéricos.

Particularmente não gosto disto, mas entendo que as pessoas se beneficiam dele. Então se adotar um ORM, procure um que já implemente o padrão de repositório de forma mais automática possível. Assim terá que se preocupar menos com a abstração e mais com as regras de negócio.

Em PHP as pessoas costumam usar PDO. Um dos problemas dele, entre outros, é que ele é apenas meia solução. Você ainda precisará cuidar de vários aspectos desta abstração. As consultas não se adaptarão sozinhas porque usou o tal do PDO.

Conclusão

Compensa sempre fazer isto? Não. Depende do projeto, dos objetivos. Muitas pessoas colocam o comportamento de acesso na própria classe da estrutura da entidade. E funciona bem também, mas tem que saber o que está fazendo. Obviamente dificulta a troca do banco de dados que talvez nunca ocorra mesmo.

Claro que ainda cabem dúvidas mais específicas sobre o assunto.

Aplique Separação de Interesses até sua máxima extensão possível.

Em um mundo ideal sua classe de Usuários não precisaria saber como inserir um novo registro. Em situações assim, um adaptador ORM é a melhor opção: Você não precisa saber como a implementação atual do banco trabalha, apenas utilizando .Save() em uma instância já causaria o efeito desejado, por exemplo.

Se você não tem uma camada ORM, pergunte-se: O que é pior, uma camada genérica de banco saber o que é um usuário (e, por consequência lógica, aspectos de todas as outras classes espalhadas pela sua aplicação) ou uma classe de usuário saber emitir expressões SQL?

Minha escolha para conter danos e compartimentar escopos seria a segunda opção.

Se você for mesmo representar queries por classes[1], considere que “inserir um usuário no banco” é um conceito, mas a realização desse conceito depende de qual SGBD está sendo usado. Isso poderia portanto ser modelado através do padrão Abstract Factory.

Nesse caso, cada modelo do seu domínio poderia ter uma interface contendo os diversos métodos que cuidarão da sua persistência, e essa interface pode possuir N implementações (1 para SQL padrão e outras específicas). A classe que representa seu SGBD específico seria a fábrica dessas implementações. A princípio, supondo que SQL padrão seja suficiente para tratar de um modelo, essas fábricas precisariam somente retornar a implementação padrão. Mas sempre que um modelo específico precisar de customizações, essa fábrica retornaria uma implementação customizada.

Um exemplo (em pseudocódigo) com dois modelos e três SGBDs seria:

/* Modelos */
class Usuario
    username
    password

class Produto
    nome
    categoria

...

/* Persistência */
interface PersistenciaUsuario
    inserir(username, password): Usuario
    obter(username): Usuario
    remover(username): boolean
    verificar_senha(username, password): boolean
    listar(): Usuario[]

interface PersistenciaProduto
    inserir(nome, categoria): Produto
    obter(id): Produto
    pesquisar(nome): Produto[]
    remover(id): boolean
    listar(): Produto[]
    listar(categoria): Produto[]

...

/* Fábrica abstrata */
interface Fabrica:
    obter_persistencia_usuario(): PersistenciaUsuario
    obter_persistencia_produto(): PersistenciaProduto
    ...

Uma implementação padrão usaria SQL simples:

class PUsuarioSQL implements PersistenciaUsuario
    sql_engine
    PUsuarioSQL(engine) { sql_engine = engine; }
    inserir(username, password) { ... }
    obter(username) { ... }
    ...

class PProdutoSQL implements PersistenciaProduto
    sql_engine
    PProdutoSQL(engine) { sql_engine = engine; }
    inserir(nome, categoria) { ... }
    obter(id) { ... }
    ...

abstract class FabricaSQL implements Fabrica
    sql_engine
    obter_persistencia_usuario() { return new PUsuarioSQL(engine); }
    obter_persistencia_produto() { return new PProdutoSQL(engine); }
    ...

Uma implementação concreta só customizaria o que fosse necessário:

class PUsuarioMySQL extends PUsuarioSQL
    inserir(username, password) { ... }
    verificar_senha(username, password) { ... }
    /* Reaproveitou parte da implementação de PersistenciaUsuario */

class FabricaMySQL extends FabricaSQL
    FabricaMySQL() { super(...); }
    obter_persistencia_usuario() { return new PUsuarioMySQL(engine); }
    /* Reaproveitou toda a implementação de PersistenciaProduto */

class PProdutoPostgres extends PProduto
    pesquisar(nome): Produto[]
    /* Reaproveitou parte da implementação de PersistenciaProduto */

class FabricaPostgres extends FabricaSQL
    FabricaPostgres() { super(...); }
    obter_persistencia_produto() { return new PProdutoPostgres(engine); }
    /* Reaproveitou toda a implementação de PersistenciaUsuario */

class FabricaMongo implements Fabrica
    /* Aqui não dá pra reaproveitar nada, pois não é baseado em SQL... */

Algumas observações:

  • Se você tem M modelos e N SGBDs, potencialmente existirão MxN classes responsáveis pela persistência;
    • Se surgiu um novo modelo, terá que criar até N novas classes; se surgiu um novo SGBD, terá que criar até M novas classes.
    • Note que esse esquema não obedece necessariamente ao princípio open/closed – pois qualquer mudança na interface Fabrica exigiria mudanças nas N implementações específicas.
  • A persistência de cada modelo não está nem no modelo em si (o que “amarraria” o mesmo a uma implementação específica, e comprometeria a separação das responsabilidades) e nem na classe que faz a conexão com o SGBD (o que criaria um “god object”). Está numa classe separada, com uma interface homogênea (i.e. mesma interface para qualquer SGBD).
    • A consequência disso é que o código que precisar agir sobre o modelo (inserir, consultar, excluir, etc) será o mesmo seja qual for o meio de persistência utilizado. Tudo o que esse código consumidor precisa é receber uma instância de Fabrica e chamar os métodos relevantes.
  • Implementações concretas podem se aproveitar das similaridades, só escrevendo código novo pra casos realmente distintos dos já existentes. No exemplo acima, a FabricaMySQL aproveitou toda a implementação padrão de PersistenciaProduto, e ainda aproveitou parcialmente a implementação padrão de PersistenciaUsuario. Ou seja, só o que for diferente mesmo é que precisa ser reimplementado pelas classes finais, tudo o que dá pra reaproveitar é reaproveitado.

[1]: Eu não faria isso, ainda que fugisse do modelo OO. Em vez disso eu utilizaria um meio mais leve, por exemplo um arquivo de recursos onde cada query é uma simples string (parametrizada, é claro). Mas um bom ORM poderia ser ainda melhor.

Um princípio básico amplamento aceito na Orientação a Objetos é a separação de responsabilidades.

Veja Princípio da Separação de Responsabilidades para mais detalhes.

Quatro responsabilidades

Um padrão de projeto amplamente utilizado identifica pelo menos 4 responsabilidades na solução deste problema:

1) Entidade

Objeto que representa a entidade; no caso, um usuário do sistema. Possui os atributos e comportamentos de negócio da entidade.

2) Repositório

Objeto que abstrai a persistência e recuperação da entidade.

Possui métodos CRUD, se necessários, e outros métodos de pesquisa.

Exemplo:

class UsuarioRepo
    Usuario[] usuariosAtivos()

O repositório também pode ser genérico, onde um único objeto abstrai a persistência e recuperação de todos os tipos de entidade do sistema:

class Repositorio
    T[] obtem<T>(CriteriosPesquisa)

Mas o Repositório não detém a informação da estrutura do banco de dados, tampouco conhece as especificidades do banco que está sendo utilizado. Em outras palavras, o repositório não sabe nada sobre as tabelas no banco e não sabe fazer comandos SQL.

Para fazer o seu trabalho, o repositório se utiliza dos objetos a seguir (e depois de falarmos sobre eles, voltaremos ao repositório).

3) Mapeamento Entidade para Banco de dados

Baseado nas definições em arquivos XML ou em metadados declarados na classe da entidade, este objeto entrega um mapeamento entre entidades e tabelas e colunas do banco de dados.

Este objeto tem o conhecimento da estrutura do banco e da sua relação com as entidades, mas ele também não sabe fazer comandos SQL.

4) Interação com a base de dados

Aqui nós temos um ou mais objetos especializados em montar comandos SQL.

Pode haver um objeto ou um conjunto de objetos tratando as especifidades de cada servidor de banco de dados suportado.

Pode haver ainda uma abstração dessa interação com a base de dados, principalmente no caso de ser suportado mais de um banco.

De volta ao Repositório

Como eu disse, o repositório utiliza os outros objetos para fazer o seu trabalho. Por exemplo:

class UsuarioRepo
    Usuario[] usuariosAtivos() 
    {
        var mapeamentoUsuario = mapeamento.get<Usuario>()
        var sql = sqlBuilder.select(mapeamentoUsuario).criterio("ATIVO = 'S'")
        return conexao.executeSelect<Usuario>(sql)
    }

Veja que eu abstraí outras responsabilidades nesta resposta, como o objeto de conexão e os objetos que executam comandos contra o banco de dados.

ORM

Se você implementar esta solução, você terá desenvolvido um framework de ORM, mas você também pode usar um de mercado.

Um ORM oferece recursos de mapeamento Entidade/Banco de dados e abstrai a conexão com o banco, a construção de comandos SQL e a execução destes comandos no banco de dados.

Os ORMs para Java, Ruby (on Rails*) e .Net são muito simples de se utilizar e, embora sejam complexos, não adicionam complexidade ao projeto porque esta complexidade está encapsulada dentro deles.

Usando um ORM, o código fica mais ou menos assim:

@Entity("TAB_USUARIO")
class Usuario
    @Column("LOGIN")
    username
    @Column("SENHA")
    password      

class UsuarioRepo
   Usuario[] usuariosAtivos() 
   {
     return orm.query<Usuario>("select u from Usuario u where u.ativo = true").result()
   }

Repare que este comando não é código SQL nativo do banco de dados e nem é sobre tabelas, mas sim sobre nomes de entidades conforme sua classe. O ORM, por sua vez, tratou o mapeamento, as especifidades do banco em questão, pool de conexões, etc.

Ainda há a opção de usar lambda e outros patterns em vez de escrever o comando como string; e o .Net ainda oferece o LINQ.

Você ainda pode optar por não encapsular persistência e recuperação em repositórios mas, ao invés, escrever as queries cada vez que precisar delas – como são queries contra entidades e não SQL nativo contra tabelas, você não estaria espalhando código de banco de dados pela sua aplicação. Eu geralmente uso repositórios devido a algumas características do meu contexto.

*O Rails usa o modelo ActiveRecord, onde os comportamentos de persistência e recuperação pertencem à entidade e sua classe, respectivamente, e não a um objeto em separado.

Conclusão

A separação de responsabilidades pode ir muito além de escrever o SQL em uma classe diferente da classe da entidade.

Você deve fazer esta separação conforme as características do seu contexto.

Por exemplo, se o software atende uma necessidade complexa, separar bem as responsabilidades pode ajudar a evitar que a complexidade do domínio contamine o código. Quanto mais complexo o domínio, mais compensa a separação de responsabilidades, a qual ajuda a manter o código mais simples (embora esta afirmação possa ser um pouco contraituitiva).

Por fim, geralmente você não precisará desenvolver um ORM para ajudar na separação das responsabilidades pois há bons frameworks para isso disponíveis no mercado.

Fonte

Related Posts:

Qual a diferença entre AppCompatActivity e Activity? – android android-activity
Pergunta: Qual a diferença da AppCompatActivity para Activity ? A partir de qual versão a AppCompatActivity foi adicionada ao Android? Autor da pergunta Luhhh A diferença reside ...
Como abreviar palavras em PHP? – php string
Pergunta: Possuo informações comuns como nome de pessoas e endereços, e preciso que elas contenham no máximo 30 caracteres sem cortar palavras. Exemplo: 'Avenida Natalino João Brescansin' ...
Qual é a finalidade de um parêntese vazio numa declaração Lambda? – c# expressões-lambda característica-linguagem
Pergunta: Criei um exemplo de uma declaração Lambda sem argumentos, entretanto, estou com duvidas referente a omissão do parêntese vazio () na declaração. Veja o exemplo: class ...
Boas práticas para URI em API RESTful – api rest restful
Pergunta: Estou com dúvida em relação às URIs de alguns recursos da api que estou desenvolvendo. Tenho os recursos projetos e atividades com relação 1-N, ...
Dúvidas sobre a integração do MySQL com Java – java mysql netbeans
Pergunta: Estou criando um sistema no NetBeans, utilizando a linguagem Java e o banco de dados MySQL. Escrevi o seguinte código para realizar a conexão ...
Qual é a finalidade da pasta Model do framework Inphinit? – php inphinit
Pergunta: No Inphinit micro-framework existe a pasta Model que fica dentro da pasta application, e nela é onde ficam as classes, mas eu estou muito ...
Uso do ‘@’ em variáveis – javascript typescript coffeescript
Pergunta: Vejo em algumas linguagens que compilam para javascript, como TypeScript e CoffeeScript, o uso do @ em variáveis, como também, casos em que o ...
Qual tamanho máximo um arquivo JSON pode ter? – json arquivo
Pergunta: Vou dar um exemplo para conseguir explicar minha duvida: Preciso recuperar informação de imagens vindas de uma API, esse banco de imagens me retorna JSON's ...
O que é Teste de Regressão? – terminologia engenharia-de-software testes
Pergunta: Na matéria de Teste de Software o professor abordou um termo chamado Teste de Regressão, isto dentro da disciplina de teste de software. Sendo ...
O que é um construtor da linguagem? – php característica-linguagem
Pergunta: Em PHP, já li e ouvi várias vezes a respeito dos Construtores da Linguagem. Os casos que sempre ouvi falar deles foi em casos ...
Função intrínseca para converter numérico para string – cobol
Pergunta: Estou a tentar saber se existe alguma função intrínseca do COBOL para converter um data numérico para string sem precisar usar a cláusula REDEFINES: ( ...
Porque usar implements? – java android
Pergunta: Qual a diferença entre usar btn.setOnClickListener(new OnClickListener() { e public class MainActivity extends Activity implements OnClickListener{ Estive fazendo um curso de Android e meu professor falou que ...
O que é XHTML e quando deve ser usado? – html xml xhtml
Pergunta: O que eu sei é que o XHTML precisa ser XML válido. Isso implica, por exemplo, que todas as tags precisam ser fechadas. Por ...
Uma placa aceleradora de vídeo pode melhorar o desempenho não-gráfico? [fechada] – desempenho
Pergunta: Para desenvolver em Ruby on Rails, eu utilizo aqui uma máquina virtual do VirtualBox com Ubuntu Server 14.04 sem interface gráfica instalada. Recentemente descobri uma ...
Concat() VS Union() – c# .net
Pergunta: Qual a diferença entre Concat() e Union() ? Quando usar Concat() e quando usar Union() ? Somente pode ser usado em list ? ...

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *