sql >> Base de Datos >  >> NoSQL >> MongoDB

Patrones de diseño para la capa de acceso a datos

Bueno, el enfoque común para el almacenamiento de datos en Java es, como notó, no muy orientado a objetos. Esto en sí mismo no es ni malo ni bueno:la "orientación a objetos" no es ni una ventaja ni una desventaja, es solo uno de los muchos paradigmas que a veces ayuda con un buen diseño de arquitectura (y otras veces no).

La razón por la que los DAO en Java no suelen estar orientados a objetos es exactamente lo que desea lograr:relajar su dependencia de la base de datos específica. En un lenguaje mejor diseñado, que permitía la herencia múltiple, esto, por supuesto, se puede hacer muy elegantemente de forma orientada a objetos, pero con Java, parece ser más problemático de lo que vale.

En un sentido más amplio, el enfoque no orientado a objetos ayuda a desvincular los datos de nivel de aplicación de la forma en que se almacenan. Esto es más que (no) dependencia de los detalles de una base de datos en particular, sino también de los esquemas de almacenamiento, lo cual es especialmente importante cuando se usan bases de datos relacionales (no me hagas empezar con ORM):puedes tener un esquema relacional bien diseñado traducido sin problemas al modelo OO de la aplicación por su DAO.

Entonces, la mayoría de los DAO en Java hoy en día son esencialmente lo que mencionaste al principio:clases, llenas de métodos estáticos. Una diferencia es que, en lugar de hacer que todos los métodos sean estáticos, es mejor tener un solo "método de fábrica" ​​estático (probablemente, en una clase diferente), que devuelve una instancia (singleton) de su DAO, que implementa una interfaz particular , utilizado por el código de la aplicación para acceder a la base de datos:

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 qué hacerlo de esta manera a diferencia de los métodos estáticos? Bueno, ¿qué sucede si decide cambiar a una base de datos diferente? Naturalmente, crearía una nueva clase DAO, implementando la lógica para su nuevo almacenamiento. Si estuviera usando métodos estáticos, ahora tendría que revisar todo su código, acceder al DAO y cambiarlo para usar su nueva clase, ¿verdad? Esto podría ser un gran dolor. ¿Y si cambia de opinión y quiere volver a la base de datos anterior?

Con este enfoque, todo lo que necesita hacer es cambiar el GreatDAOFactory.getDAO() y haga que cree una instancia de una clase diferente, y todo el código de su aplicación usará la nueva base de datos sin ningún cambio.

En la vida real, esto a menudo se hace sin ningún cambio en el código:el método de fábrica obtiene el nombre de la clase de implementación a través de una configuración de propiedad y lo instancia mediante la reflexión, por lo que todo lo que necesita hacer para cambiar las implementaciones es editar una propiedad. expediente. En realidad, hay marcos, como spring o guice - que administran este mecanismo de "inyección de dependencia" por usted, pero no entraré en detalles, primero, porque realmente está más allá del alcance de su pregunta, y también, porque no estoy necesariamente convencido de que el beneficio que obtiene al usar vale la pena integrar esos marcos con ellos para la mayoría de las aplicaciones.

Otro beneficio (probablemente, más probable que se aproveche) de este "enfoque de fábrica" ​​en lugar de estático es la capacidad de prueba. Imagina que estás escribiendo una prueba unitaria, que debería probar la lógica de tu App clase independientemente de cualquier DAO subyacente. No desea que use ningún almacenamiento subyacente real por varias razones (velocidad, tener que configurarlo y limpiar las palabras posteriores, posibles colisiones con otras pruebas, posibilidad de contaminar los resultados de las pruebas con problemas en DAO, no relacionados con App , que en realidad se está probando, etc.).

Para hacer esto, necesita un marco de prueba, como Mockito , que le permite "simular" la funcionalidad de cualquier objeto o método, reemplazándolo con un objeto "ficticio", con un comportamiento predefinido (Omitiré los detalles, porque esto nuevamente está fuera del alcance). Entonces, puede crear este objeto ficticio reemplazando su DAO y hacer la GreatDAOFactory devuelva su ficticio en lugar de lo real llamando a GreatDAOFactory.setDAO(dao) antes de la prueba (y restaurarla después). Si estuviera usando métodos estáticos en lugar de la clase de instancia, esto no sería posible.

Un beneficio más, que es un poco similar al cambio de bases de datos que describí anteriormente, es "mejorar" su dao con funcionalidad adicional. Suponga que su aplicación se vuelve más lenta a medida que crece la cantidad de datos en la base de datos y decide que necesita una capa de caché. Implemente una clase contenedora, que use la instancia de dao real (proporcionada como un parámetro de constructor) para acceder a la base de datos y almacene en caché los objetos que lee en la memoria, para que puedan devolverse más rápido. A continuación, puede crear su GreatDAOFactory.getDAO crear una instancia de este contenedor, para que la aplicación lo aproveche.

(Esto se llama "patrón de delegación"... y parece una molestia, especialmente cuando tiene muchos métodos definidos en su DAO:tendrá que implementarlos todos en el contenedor, incluso para alterar el comportamiento de uno solo . Alternativamente, podría simplemente subclasificar su dao y agregarle almacenamiento en caché de esta manera. Esto sería una codificación mucho menos aburrida por adelantado, pero puede volverse problemático cuando decida cambiar la base de datos o, peor aún, tener la opción de cambiar implementaciones de un lado a otro).

Una alternativa igualmente ampliamente utilizada (pero, en mi opinión, inferior) al método "de fábrica" ​​es hacer que el dao una variable miembro en todas las clases que la necesitan:

public class App {
   GreatDao dao;
   public App(GreatDao d) { dao = d; }
}

De esta manera, el código que instancia estas clases necesita instanciar el objeto dao (todavía podría usar la fábrica) y proporcionarlo como un parámetro de constructor. Los marcos de inyección de dependencia que mencioné anteriormente, generalmente hacen algo similar a esto.

Esto proporciona todos los beneficios del enfoque del "método de fábrica", que describí anteriormente, pero, como dije, no es tan bueno en mi opinión. Las desventajas aquí son tener que escribir un constructor para cada una de las clases de su aplicación, hacer exactamente lo mismo una y otra vez, y también no poder crear instancias de las clases fácilmente cuando sea necesario, y algo de legibilidad perdida:con una base de código lo suficientemente grande , un lector de su código, que no esté familiarizado con él, tendrá dificultades para comprender qué implementación real del dao se usa, cómo se crea una instancia, si es un singleton, una implementación segura para subprocesos, si mantiene el estado o cachés cualquier cosa, cómo se toman las decisiones sobre la elección de una implementación en particular, etc.