aplicando los principios SOLID

13

Soy bastante nuevo en los S.O.L.I.D. principios de diseño. Entiendo su causa y sus beneficios, pero aún así no los aplico a un proyecto más pequeño que quiero refactorizar como un ejercicio práctico para usar los principios de SOLID. Sé que no hay necesidad de cambiar una aplicación que funcione perfectamente, pero quiero refactorizarla de todos modos para obtener experiencia en diseño para futuros proyectos.

La aplicación tiene la siguiente tarea (en realidad, mucho más que eso, pero seamos sencillos): Tiene que leer un archivo XML que contiene definiciones de la tabla de base de datos / columna / vista, etc. y crear un archivo SQL que se pueda usar para crear un esquema de base de datos ORACLE.

(Nota: absténgase de discutir por qué lo necesito o por qué no uso XSLT, etc., hay razones, pero están fuera de tema)

Para empezar, elegí mirar solo Tablas y Restricciones. Si ignora las columnas, puede indicarlo de la siguiente manera:

Una restricción forma parte de una tabla (o, más precisamente, forma parte de una declaración CREATE TABLE), y una restricción también puede hacer referencia a otra tabla.

Primero, explicaré cómo se ve la aplicación en este momento (sin aplicar SOLID):

En este momento, la aplicación tiene una clase "Tabla" que contiene una lista de punteros a Restricciones que pertenecen a la tabla y una lista de punteros a Restricciones que hacen referencia a esta tabla. Cada vez que se establece una conexión, la conexión hacia atrás también se establecerá. La tabla tiene un método createStatement () que a su vez llama a la función createStatement () de cada restricción. Dicho método utilizará las conexiones a la tabla del propietario y la tabla a la que se hace referencia para recuperar sus nombres.

Obviamente, esto no se aplica a SOLID en absoluto. Por ejemplo, hay dependencias circulares, que hinchan el código en términos de los métodos "agregar" / "eliminar" requeridos y algunos destructores de objetos grandes.

Así que hay un par de preguntas:

  1. ¿Debo resolver las dependencias circulares utilizando la inyección de dependencia? Si es así, supongo que la Restricción debería recibir la tabla de propietario (y opcionalmente la referenciada) en su constructor. Pero, ¿cómo podría repasar la lista de restricciones para una sola tabla?
  2. Si la clase de la tabla almacena el estado de sí mismo (por ejemplo, el nombre de la tabla, el comentario de la tabla, etc.) y los enlaces a las restricciones, ¿son estas una o dos "responsabilidades", pensando en el principio de responsabilidad única?
  3. En el caso 2. es correcto, ¿debería crear una nueva clase en la capa empresarial lógica que administre los enlaces? Si es así, 1. obviamente ya no sería relevante.
  4. ¿Deben los métodos "createStatement" formar parte de las clases de Tabla / Restricción o debo moverlos también? Si es así, ¿a dónde? ¿Una clase de administrador por cada clase de almacenamiento de datos (es decir, tabla, restricción, ...)? ¿O más bien crear una clase de administrador por enlace (similar a 3)?

Cuando intento responder una de estas preguntas, me encuentro corriendo en círculos en alguna parte.

El problema, obviamente, se vuelve mucho más complejo si incluyes columnas, índices, etc., pero si ustedes me ayudan con la simple Tabla / Restricción, quizás pueda resolver el resto por mi cuenta.

    
pregunta Tim Meyer 02.09.2011 - 11:21

3 respuestas

8

Puede comenzar desde un punto de vista diferente para aplicar el "Principio de Responsabilidad Única" aquí. Lo que nos ha mostrado es (más o menos) solo el modelo de datos de su aplicación. SRP aquí significa: asegúrese de que su modelo de datos sea responsable solo de mantener los datos, ni menos, ni más.

Entonces, cuando vaya a leer su archivo XML, cree un modelo de datos y escriba SQL, lo que no debería hacer es implementar cualquier cosa en su clase Table , que es XML o SQL. específico. Tu quieres que tu flujo de datos se vea así:

[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]

Por lo tanto, el único lugar donde se debe colocar el código XML específico es una clase llamada, por ejemplo, Read_XML . El único lugar para el código específico de SQL debe ser una clase como Write_SQL . Por supuesto, tal vez vaya a dividir esas 2 tareas en más sub-tareas (y dividirá sus clases en múltiples clases de administrador), pero su "modelo de datos" no debería asumir ninguna responsabilidad de esa capa. Así que no agregue un createStatement a ninguna de sus clases de modelo de datos, ya que esto le otorga la responsabilidad del modelo de datos para el SQL.

No veo ningún problema al describir que una tabla es responsable de mantener todas sus partes (nombre, columnas, comentarios, restricciones ...), esa es la idea detrás de un modelo de datos. Pero usted describió "Tabla" también es responsable de la administración de la memoria de algunas de sus partes. Ese es un problema específico de C ++, que no enfrentarías tan fácilmente en lenguajes como Java o C #. La forma en C ++ de deshacerse de esa responsabilidad es mediante el uso de punteros inteligentes, delegando la propiedad a una capa diferente (por ejemplo, la biblioteca boost o su propia capa de puntero "inteligente"). Pero tenga cuidado, sus dependencias cíclicas pueden "irritar" algunas implementaciones de punteros inteligentes.

Algo más acerca de SOLID: aquí es un buen artículo

enlace

explicando SOLID por un pequeño ejemplo. Intentemos aplicar eso a su caso:

  • no solo necesitarás las clases Read_XML y Write_SQL , sino también una tercera clase que administra la interacción de esas 2 clases. Vamos a llamarlo ConversionManager .

  • La aplicación del principio DI podría significar aquí: ConversionManager no debe crear instancias de Read_XML y Write_SQL por sí mismo. En su lugar, esos objetos pueden ser inyectados a través del constructor. Y el constructor debe tener una firma como esta

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

donde IDataModelReader es una interfaz de la que Read_XML hereda, y IDataModelWriter lo mismo para Write_SQL . Esto hace que ConversionManager esté abierto para extensiones (es muy fácil que proporcione diferentes lectores o escritores) sin tener que cambiarlo, así que tenemos un ejemplo para el principio Abrir / Cerrado. Piense en lo que tendrá que cambiar cuando quiera dar soporte a otro proveedor de bases de datos; de manera ideal, no tiene que cambiar nada en su modelo de datos, simplemente proporcione otro SQL-Writer en su lugar.

    
respondido por el Doc Brown 02.09.2011 - 15:29
2

Bueno, deberías aplicar la S de SOLID en este caso.

Una tabla contiene todas las restricciones definidas en ella. Una restricción contiene todas las tablas a las que hace referencia. Modelo sencillo y sencillo.

Lo que se adhiere a eso, es la capacidad de realizar búsquedas inversas, es decir, para averiguar a qué restricciones se hace referencia en una tabla.
Entonces, lo que realmente quieres es un servicio de indexación. Esa es una tarea completamente diferente y, por lo tanto, debe llevarse a cabo por un objeto diferente.

Para descomponerlo en una versión muy simplificada:

class Table {
      void addConstraint(Constraint constraint) { ... }
      bool removeConstraint(Constraint constraint) { ... }
      Iterator<Constraint> getConstraints() { ... }
}
class Constraint {
      //actually I am not so sure these two should be exposed directly at all
      void addReference(Table to) { ... }
      bool removeReference(Table to) { ... }
      Iterator<Table> getReferencedTables() { ... }
}
class Database {
      void addTable(Table table) { ... }
      bool removeTable(Table table) { ... }
      Iterator<Table> getTables() { ... }
}
class Index {
      Iterator<Constraint> getConstraintsReferencing(Table target) { ... }
}

En cuanto a la implementación del índice, hay 3 formas de proceder:

  • el método getContraintsReferencing realmente podría rastrear todo Database para Table instancias y rastrear su Constraint s para obtener el resultado. Dependiendo de lo costoso que sea y de la frecuencia con que lo necesite, puede ser una opción.
  • también podría usar un caché. Si su modelo de base de datos puede cambiar una vez definido, puede mantener el caché disparando señales desde las instancias respectivas Table y Constraint , cuando cambien. Una solución un poco más simple sería hacer que Index construya un "índice de instantáneas" de todo el Database para trabajar, que luego descartaría. Por supuesto, eso solo es posible si su aplicación hace una gran distinción entre "tiempo de modelado" y "tiempo de consulta". Si es bastante probable que hagan esos dos al mismo tiempo, entonces esto no es viable.
  • Otra opción sería utilizar AOP para interceptar todas las llamadas de creación y mantener el índice en consecuencia.
respondido por el back2dos 02.09.2011 - 11:51
1

La cura para las dependencias circulares es prometer que nunca las crearás. Me parece que la prueba de codificación en primer lugar es un fuerte elemento disuasivo.

De todos modos, las dependencias circulares siempre se pueden romper introduciendo una clase base abstracta. Esto es típico de las representaciones gráficas. Aquí las tablas son nodos y las restricciones de clave externa son bordes. Así que cree una clase de tabla abstracta y una clase de restricción abstracta y tal vez una clase de columna abstracta. Entonces todas las implementaciones pueden depender de las clases abstractas. Puede que esta no sea la mejor representación posible, pero es una mejora con respecto a las clases acopladas entre sí.

Pero, como sospecha, la mejor solución a este problema puede no requerir ningún seguimiento de las relaciones de los objetos. Si solo desea traducir XML a SQL, entonces no necesita una representación en memoria del gráfico de restricciones. El gráfico de restricción sería bueno si quisieras ejecutar algoritmos de gráfico, pero no lo mencionaste, así que asumiré que no es un requisito. Solo necesita una lista de tablas y una lista de restricciones y un visitante para cada dialecto SQL que desee admitir. Genere las tablas, luego genere las restricciones externas a las tablas. Hasta que los requisitos no hayan cambiado, no tendría ningún problema en acoplar el generador de SQL al XML DOM. Guarda mañana para mañana.

    
respondido por el kevin cline 02.09.2011 - 22:27

Lea otras preguntas en las etiquetas