MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Padrões de design para camada de acesso a dados


Bem, a abordagem comum para armazenamento de dados em Java não é, como você observou, muito orientada a objetos. Isso em si não é nem ruim nem bom:"orientação a objetos" não é vantagem nem desvantagem, é apenas um dos muitos paradigmas, que às vezes ajuda no bom projeto de arquitetura (e às vezes não).

A razão pela qual os DAOs em java geralmente não são orientados a objetos é exatamente o que você deseja alcançar - relaxar sua dependência do banco de dados específico. Em uma linguagem mais bem projetada, que permitia herança múltipla, isso, claro, pode ser feito de maneira muito elegante e orientada a objetos, mas com java, parece ser mais problemático do que vale a pena.

Em um sentido mais amplo, a abordagem não OO ajuda a dissociar seus dados no nível do aplicativo da maneira como são armazenados. Isso é mais do que (não) dependência das especificidades de um banco de dados específico, mas também dos esquemas de armazenamento, o que é especialmente importante ao usar bancos de dados relacionais (não quero começar com ORM):você pode ter um esquema relacional bem projetado perfeitamente traduzido para o modelo OO do aplicativo pelo seu DAO.

Então, o que a maioria dos DAOs está em java hoje em dia é essencialmente o que você mencionou no início - classes, cheias de métodos estáticos. Uma diferença é que, em vez de tornar todos os métodos estáticos, é melhor ter um único "método de fábrica" ​​estático (provavelmente em uma classe diferente), que retorne uma instância (singleton) do seu DAO, que implementa uma interface específica , usado pelo código do aplicativo para acessar o banco de dados:
public interface GreatDAO {
    User getUser(int id);
    void saveUser(User u);
}
public class TheGreatestDAO implements GreatDAO {
   protected TheGeatestDAO(){}
   ... 
}
public class GreatDAOFactory {
     private static GreatDAO dao = null;
     protected static synchronized GreatDao setDAO(GreatDAO d) {
         GreatDAO old = dao;
         dao = d;
         return old;
     }
     public static synchronized GreatDAO getDAO() {
         return dao == null ? dao = new TheGreatestDAO() : dao;
     }
}

public class App {
     void setUserName(int id, String name) {
          GreatDAO dao =  GreatDAOFactory.getDao();
          User u = dao.getUser(id);
          u.setName(name);
          dao.saveUser(u);
     }
}

Por que fazer dessa maneira em oposição aos métodos estáticos? Bem, e se você decidir mudar para um banco de dados diferente? Naturalmente, você criaria uma nova classe DAO, implementando a lógica para seu novo armazenamento. Se você estivesse usando métodos estáticos, agora teria que passar por todo o seu código, acessando o DAO, e alterá-lo para usar sua nova classe, certo? Isso pode ser uma dor enorme. E se você mudar de ideia e quiser voltar para o banco de dados antigo?

Com essa abordagem, tudo o que você precisa fazer é alterar o GreatDAOFactory.getDAO() e faça com que ele crie uma instância de uma classe diferente, e todo o código do seu aplicativo estará usando o novo banco de dados sem nenhuma alteração.

Na vida real, isso geralmente é feito sem nenhuma alteração no código:o método fábrica obtém o nome da classe de implementação por meio de uma configuração de propriedade e o instancia usando reflexão, portanto, tudo o que você precisa fazer para alternar implementações é editar uma propriedade Arquivo. Na verdade, existem estruturas - como spring ou guice - que gerenciam esse mecanismo de "injeção de dependência" para você, mas não entrarei em detalhes, primeiro, porque está realmente além do escopo de sua pergunta e também porque não estou necessariamente convencido de que o benefício que você obtém ao usar esses frameworks valem o trabalho de integração com eles para a maioria dos aplicativos.

Outro benefício (provavelmente, mais provável de ser aproveitado) dessa "abordagem de fábrica" ​​em oposição à estática é a testabilidade. Imagine que você está escrevendo um teste de unidade, que deve testar a lógica do seu App class independentemente de qualquer DAO subjacente. Você não quer que ele use nenhum armazenamento subjacente real por vários motivos (velocidade, ter que configurá-lo e limpar posfácios, possíveis colisões com outros testes, possibilidade de poluir resultados de testes com problemas no DAO, não relacionado ao App , que está realmente sendo testado, etc.).

Para fazer isso, você quer um framework de teste, como Mockito , que permite "simular" a funcionalidade de qualquer objeto ou método, substituindo-o por um objeto "fictício", com comportamento predefinido (vou pular os detalhes, porque, novamente, isso está além do escopo). Então, você pode criar este objeto fictício substituindo seu DAO e fazer o GreatDAOFactory retorne seu boneco em vez do real chamando GreatDAOFactory.setDAO(dao) antes do teste (e restaurando-o depois). Se você estivesse usando métodos estáticos em vez da classe de instância, isso não seria possível.

Mais um benefício, que é um pouco semelhante à troca de bancos de dados que descrevi acima, é "aprimorar" seu dao com funcionalidade adicional. Suponha que seu aplicativo fique mais lento à medida que a quantidade de dados no banco de dados aumenta e você decide que precisa de uma camada de cache. Implemente uma classe wrapper, que usa a instância do dao real (fornecida a ela como um parâmetro construtor) para acessar o banco de dados e armazena em cache os objetos que lê na memória, para que possam ser retornados mais rapidamente. Você pode então fazer seu GreatDAOFactory.getDAO instanciar esse wrapper, para que o aplicativo tire proveito dele.

(Isso é chamado de "padrão de delegação" ... e parece uma dor de cabeça, especialmente quando você tem muitos métodos definidos em seu DAO:você terá que implementar todos eles no wrapper, mesmo para alterar o comportamento de apenas um Alternativamente, você pode simplesmente subclassificar seu dao e adicionar cache a ele dessa maneira. Isso seria muito menos chato de codificação inicial, mas pode se tornar problemático quando você decidir alterar o banco de dados ou, pior, ter uma opção de alternando implementações para frente e para trás.)

Uma alternativa igualmente amplamente usada (mas, na minha opinião, inferior) ao método "fábrica" ​​é fazer o dao uma variável de membro em todas as classes que precisam:
public class App {
   GreatDao dao;
   public App(GreatDao d) { dao = d; }
}

Dessa forma, o código que instancia essas classes precisa instanciar o objeto dao (ainda poderia usar a fábrica) e fornecê-lo como parâmetro do construtor. As estruturas de injeção de dependência que mencionei acima, geralmente fazem algo semelhante a isso.

Isso fornece todos os benefícios da abordagem do "método de fábrica", que descrevi anteriormente, mas, como eu disse, não é tão bom na minha opinião. As desvantagens aqui são ter que escrever um construtor para cada uma de suas classes de aplicativo, fazer exatamente a mesma coisa repetidamente, e também não poder instanciar as classes facilmente quando necessário, e alguma legibilidade perdida:com uma base de código grande o suficiente , um leitor do seu código, não familiarizado com ele, terá dificuldade em entender qual implementação real do dao é usada, como ele é instanciado, se é um singleton, uma implementação thread-safe, se mantém estado ou cache qualquer coisa, como são tomadas as decisões sobre a escolha de uma implementação específica, etc.