Aplicando principios SÓLIDOS

13

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

La aplicación tiene la siguiente tarea (en realidad, mucho más que eso, pero vamos a simplificarla): tiene que leer un archivo XML que contenga definiciones de tabla / columna / vista de base de datos, etc. y crear un archivo SQL que pueda usarse 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).

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

Una restricción es parte de una tabla (o más precisamente, parte de una instrucció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):

Por el momento, la aplicación tiene una clase "Tabla" que contiene una lista de punteros a Restricciones propiedad de la tabla y una lista de punteros a Restricciones que hacen referencia a esta tabla. Siempre que se establezca una conexión, también se establecerá la conexión hacia atrás. La tabla tiene un método createStatement () que a su vez llama a la función createStatement () de cada restricción. Dicho método usará las conexiones a la tabla del propietario y la tabla referenciada 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" necesarios y algunos destructores de objetos grandes.

Entonces hay un par de preguntas:

  1. ¿Debo resolver las dependencias circulares usando la inyección de dependencias? Si es así, supongo que la restricción debería recibir la tabla del propietario (y opcionalmente la referenciada) en su constructor. Pero, ¿cómo podría pasar sobre la lista de restricciones para una sola tabla entonces?
  2. Si la clase Tabla almacena el estado de sí misma (por ejemplo, el nombre de la tabla, el comentario de la tabla, etc.) y los enlaces a Restricciones, ¿estas son 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 comercial lógica que gestiona los enlaces? Si es así, 1. obviamente ya no sería relevante.
  4. ¿Deberían los métodos "createStatement" formar parte de las clases Tabla / Restricción o también debería eliminarlos? 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.)?

Cada vez que trato de responder una de estas preguntas, me encuentro corriendo en círculos en alguna parte.

Obviamente, el problema se vuelve mucho más complejo si incluye columnas, índices, etc., pero si me ayudan con la simple tabla / restricción, tal vez pueda resolver el resto por mi cuenta.

Tim Meyer
fuente
3
¿Qué idioma estás usando? ¿Podría publicar al menos un código esqueleto? Es muy difícil discutir la calidad del código y las posibles refactorizaciones sin ver el código real.
Péter Török
Estoy usando C ++ pero estaba tratando de mantenerlo fuera de discusión ya que podría tener este problema en cualquier idioma
Tim Meyer
Sí, pero la aplicación de patrones y refactorizaciones depende del idioma. Por ejemplo, @ back2dos sugirió AOP en su respuesta a continuación, que obviamente no se aplica a C ++.
Péter Török
Consulte programmers.stackexchange.com/questions/155852/… para obtener más información sobre los principios SÓLIDOS
LCJ

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 más ni menos.

Entonces, cuando va a leer su archivo XML, crear un modelo de datos a partir de él y escribir SQL, lo que no debe hacer es implementar nada en su Tableclase que sea específico de XML o SQL. Desea que su flujo de datos se vea así:

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

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

No veo ningún problema cuando estás describiendo 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ó que "Tabla" también es responsable de la administración de memoria de algunas de sus partes. Ese es un problema específico de C ++, que no enfrentaría tan fácilmente en lenguajes como Java o C #. La forma en C ++ de deshacerse de esa responsabilidad es utilizando punteros inteligentes, delegando la propiedad a una capa diferente (por ejemplo, la biblioteca de impulso o su propia capa de puntero "inteligente"). Pero tenga cuidado, sus dependencias cíclicas pueden "irritar" algunas implementaciones de punteros inteligentes.

Algo más sobre SOLID: aquí hay un buen artículo

http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game

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

  • necesitará no solo clases Read_XMLy Write_SQL, sino también una tercera clase que gestiona la interacción de esas 2 clases. Vamos a llamarlo a ConversionManager.

  • La aplicación del principio DI podría significar aquí: ConversionManager no debería crear instancias de Read_XMLy Write_SQLpor sí mismo. En cambio, esos objetos se pueden inyectar a través del constructor. Y el constructor debería tener una firma como esta

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

donde IDataModelReaderes una interfaz de la que Read_XMLhereda, y IDataModelWriterlo mismo para Write_SQL. Esto ConversionManagerabre las extensiones (proporciona muy fácilmente diferentes lectores o escritores) sin tener que cambiarlo, por lo que tenemos un ejemplo para el principio Abierto / Cerrado. Piense en lo que tendrá que cambiar cuando desee admitir a otro proveedor de bases de datos; en realidad, no tiene que cambiar nada en su modelo de datos, solo proporcione otro Escritor SQL.

Doc Brown
fuente
Si bien este es un ejercicio muy razonable de SOLID, tenga en cuenta que (viola) viola la "vieja escuela Kay / Holub OOP" al requerir captadores y establecedores para un modelo de datos bastante anémico. También me recuerda la infame perorata de Steve Yegge .
user949300
2

Bueno, debe aplicar la S de SÓLIDO 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 simple y llano.

En lo que te aferras a eso, es en la capacidad de realizar búsquedas inversas, es decir, averiguar por qué restricciones se hace referencia a alguna tabla.
Entonces, lo que realmente quieres es un servicio de indexación. Esa es una tarea completamente diferente y, por lo tanto, debe ser realizada por un objeto diferente.

Para desglosarlo 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 hacerlo:

  • el getContraintsReferencingmétodo realmente podría rastrear todo Databasepara Tableinstancias y rastrear sus Constraints para obtener el resultado. Dependiendo de lo costoso que sea y con qué frecuencia 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 la memoria caché disparando señales de las respectivas Tablee Constraintinstancias, cuando cambian. Una solución un poco más simple sería Indexcrear un "índice de instantáneas" del conjunto Databasepara 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 haga 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.
back2dos
fuente
Respuesta muy detallada, ¡hasta ahora me gusta tu solución! ¿Qué pensaría si realizara DI para la clase Tabla, dándole una lista de restricciones durante la construcción? De todos modos, tengo una clase TableParser, que podría actuar como una fábrica o trabajar junto con una fábrica para ese caso.
Tim Meyer
@Tim Meyer: DI no es necesariamente una inyección de constructor. DI también se puede hacer por funciones miembro. Si la tabla debe obtener todas sus partes a través del constructor depende de si desea que esas partes solo se agreguen en el momento de la construcción y nunca cambien más tarde, o si desea crear una tabla paso a paso. Esa debería ser la base de su decisión de diseño.
Doc Brown
1

La cura para las dependencias circulares es jurar que nunca, nunca las creará. Encuentro que la prueba de codificación primero es un fuerte elemento disuasorio.

De todos modos, las dependencias circulares siempre se pueden romper introduciendo una clase base abstracta. Esto es típico para las representaciones gráficas. Aquí las tablas son nodos y las restricciones de clave externa son aristas. Cree una clase de tabla abstracta y una clase de restricción abstracta y quizás 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 sobre las clases acopladas mutuamente.

Pero, como sospecha, la mejor solución a este problema puede no requerir ningún seguimiento de las relaciones de objeto. Si solo desea traducir XML a SQL, entonces no necesita una representación en memoria del gráfico de restricción. El gráfico de restricción sería bueno si quisieras ejecutar algoritmos gráficos, pero no lo mencionaste, así que supondré 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 cambiaran, no tendría ningún problema al acoplar el generador SQL al DOM XML. Ahorra mañana para mañana.

Kevin Cline
fuente
Aquí es donde entra en juego "(en realidad, mucho más que eso, pero seamos simples)". Por ejemplo, hay casos en los que necesito eliminar una tabla, por lo que debo verificar si alguna restricción hace referencia a esta tabla.
Tim Meyer