Divide interfaces grandes

9

Estoy usando una interfaz grande con aproximadamente 50 métodos para acceder a una base de datos. La interfaz ha sido escrita por un colega mío. Discutimos esto:

Yo: 50 métodos es demasiado. Es un olor a código.
Colega: ¿Qué debo hacer al respecto? Desea el acceso a la base de datos, lo tiene.
Yo: Sí, pero no está claro y difícil de mantener en el futuro.
Colega: OK, tienes razón, no es agradable. ¿Cómo debería ser la interfaz entonces?
Yo: ¿Qué tal 5 métodos que devuelven objetos que tienen, como, 10 métodos cada uno?

Mmmh, pero ¿no sería esto lo mismo? ¿Esto realmente conduce a más claridad? ¿Vale la pena el esfuerzo?

De vez en cuando me encuentro en una situación en la que quiero una interfaz y lo primero que me viene a la mente es una gran interfaz. ¿Hay un patrón de diseño general para esto?


Actualización (en respuesta al comentario de SJuan):

El "tipo de métodos": es una interfaz para obtener datos de una base de datos. Todos los métodos tienen la forma (pseudocódigo)

List<Typename> createTablenameList()

Los métodos y las tablas no están exactamente en una relación 1-1, el énfasis está más en el hecho de que siempre obtienes algún tipo de lista que proviene de una base de datos.

TobiMcNamobi
fuente
12
Falta información relevante (qué tipo de métodos tiene). De todos modos, supongo: si divide solo por número, entonces su colega tiene razón, no está mejorando nada. Un posible criterio de división sería por la "entidad" (casi el equivalente de una mesa) devuelto (así, una UserDaoy una CustomerDaoy una ProductDao)
SJuan76
De hecho, algunas tablas están semánticamente cerca de otras tablas que forman "camarillas". También los métodos.
TobiMcNamobi
¿Es posible ver el código? Sé que tiene 4 años y probablemente ya lo haya solucionado: D Pero me encantaría pensar en este problema. Resolví algo como esto antes.
clankill3r
@ clankill3r De hecho, ya no tengo acceso a la interfaz específica que me hizo publicar la pregunta anterior. Sin embargo, el problema es más general. Imagine un DB con alrededor de 50 tablas y para cada tabla un método comoList<WeatherDataRecord> createWeatherDataTable() {db.open(); return db.select("*", "tbl_weatherData");}
TobiMcNamobi

Respuestas:

16

Sí, 50 métodos es un olor a código, pero un olor a código significa echarle un segundo vistazo, no es que sea automáticamente incorrecto. Si cada cliente que usa esa clase necesita potencialmente los 50 métodos, es posible que no haya un caso para dividirlo. Sin embargo, esto es poco probable. Mi punto es que dividir una interfaz arbitrariamente puede ser peor que no dividirla en absoluto.

No hay un solo patrón para solucionarlo, pero el principio que describe el estado deseado es el Principio de segregación de interfaz (la 'I' en SÓLIDO), que establece que ningún cliente debe verse obligado a depender de métodos que no utiliza .

La descripción del ISP le da una pista sobre cómo solucionarlo: mire al cliente . A menudo, con solo mirar una clase, parece que todo pertenece, pero surgen divisiones claras cuando miras a los clientes que usan esa clase. Siempre considere a los clientes primero cuando diseñe una interfaz.

La otra forma de determinar si una interfaz debe dividirse y dónde se debe dividir es mediante una segunda implementación. Lo que a menudo termina sucediendo es que su segunda implementación no necesita muchos de los métodos, por lo que claramente deberían dividirse en su propia interfaz.

Karl Bielefeldt
fuente
Esta y la respuesta de utnapistim son realmente geniales.
TobiMcNamobi
13

Colega: OK, tienes razón, no es agradable. ¿Cómo debería ser la interfaz entonces?

Yo: ¿Qué tal 5 métodos que devuelven objetos que tienen, como, 10 métodos cada uno?

Ese no es un buen criterio (en realidad no hay ningún criterio en esa declaración). Puede agruparlos por (suponiendo que su aplicación es una aplicación de transacciones financieras, para mis ejemplos):

  • funcionalidad (inserciones, actualizaciones, selecciones, transacciones, metadatos, esquema, etc.)
  • entidad (usuario DAO , depósito DAO, etc.)
  • área de aplicación (transacciones financieras, gestión de usuarios, totales, etc.)
  • nivel de abstracción (todo el código de acceso a la tabla es un módulo separado; todas las API seleccionadas están en su propia jerarquía, el soporte de transacciones es separado, todo el código de conversión en un módulo, todo el código de validación en un módulo, etc.)

Mmmh, pero ¿no sería esto lo mismo? ¿Esto realmente conduce a más claridad? ¿Vale la pena el esfuerzo?

Si elige el criterio correcto, definitivamente. Si no lo haces, definitivamente no :).

Algunos ejemplos:

  • mire los objetos ADODB para obtener un ejemplo simplista de primitivas OO (su API DB probablemente ya lo ofrece)

  • mire el modelo de datos de Django ( https://docs.djangoproject.com/en/dev/topics/db/models/ ) para una idea de modelo de datos con un alto nivel de abstracción (en C ++ probablemente necesitará un poco más de placa de caldera código, pero es una buena idea). Esta implementación está diseñada con un rol de "modelo" en mente, dentro del patrón de diseño MVC.

  • mire la API sqlite para una idea API plana ( http://www.sqlite.org/c3ref/funclist.html ), que consta de primitivas funcionales (API C).

utnapistim
fuente
3

De vez en cuando me encuentro en una situación en la que quiero una interfaz y lo primero que me viene a la mente es una gran interfaz. ¿Existe un patrón de diseño general para esto?

Es un antipatrón de diseño llamado clase monolítica . Tener 50 métodos en una clase o interfaz es una violación probable del SRP . La clase monolítica surge porque trata de ser todo para todos.

DCI aborda la hinchazón del método. Esencialmente, las muchas responsabilidades de una clase podrían asignarse a roles (descargados a otras clases) que solo son relevantes en ciertos contextos. La aplicación de roles se puede lograr de varias maneras, incluyendo mixins o decoradores . Este enfoque mantiene las clases enfocadas y esbeltas.

¿Qué tal 5 métodos que devuelven objetos que tienen, como, 10 métodos cada uno?

Esto sugiere crear instancias de todos los roles cuando el objeto mismo se instancia. Pero, ¿por qué crear instancias de roles que quizás no necesites? En cambio, crea una instancia de un rol en el contexto en el que realmente lo necesitas.

Si encuentra que refactorizar hacia DCI no es obvio, podría seguir un patrón de visitante más simple . Proporciona un beneficio similar sin enfatizar la creación de contextos de casos de uso.

EDITAR: Mi pensamiento sobre esto ha cambiado un poco. Proporcioné una respuesta alternativa.

Mario T. Lanza
fuente
1

Me parece que cualquier otra respuesta está perdiendo el punto. El punto es que una interfaz idealmente debería definir una porción atómica de comportamiento. Ese es el yo en SÓLIDO.

Una clase debería tener una responsabilidad, pero esto aún podría incluir múltiples comportamientos. Para seguir con un objeto de cliente de base de datos típico, esto puede ofrecer una funcionalidad CRUD completa. Serían cuatro comportamientos: crear, leer, actualizar y eliminar. En un mundo SOLIDO puro, el cliente de la base de datos implementaría no IDatabaseClient sino ICreator, IReader, IUpdater e IDeleter.

Esto tendría una serie de beneficios. Primero, con solo leer la declaración de la clase, uno aprendería instantáneamente mucho sobre la clase, las interfaces que implementa cuentan toda la historia. En segundo lugar, si el objeto del cliente se pasara como argumento, uno ahora tiene diferentes opciones útiles. Podría pasarse como un IReader y uno podría estar seguro de que la persona que llama solo podrá leer. Se pueden probar diferentes comportamientos por separado.

Sin embargo, cuando se trata de pruebas, la práctica común es simplemente colocar una interfaz en una clase que es una réplica 1 a 1 de la interfaz de clase completa. Si las pruebas son lo único que le importa, este puede ser un enfoque válido. Te permite hacer muñecos con bastante rapidez. Pero casi nunca es SÓLIDO y realmente es un abuso de las interfaces para un propósito específico.

Entonces, sí, 50 métodos es un olor, pero depende de la intención y el propósito si es malo o no. Ciertamente no es lo ideal.

Martin Maat
fuente
0

Las capas de acceso a datos tienden a tener muchos métodos asociados a una clase. Si alguna vez trabajó con Entity Framework u otras herramientas ORM, verá que generan cientos de métodos. Supongo que usted y su colega lo están implementando manualmente. No es necesario un olor a código, pero no es bonito de ver. Sin conocer su dominio, es difícil de decir.

Roman Mik
fuente
Métodos o propiedades?
JeffO
0

Utilizo protocolos (llámelos interfaces si lo desea) casi universalmente para todas las API con FP y OOP. (¿Recuerda la matriz? ¡No hay concreciones!) Existen, por supuesto, tipos concretos, pero dentro del alcance de un programa, cada tipo se considera algo que desempeña un papel dentro de algún contexto.

Esto significa que los objetos pasados ​​a través de programas, funciones, etc. pueden considerarse entidades abstractas con conjuntos de comportamientos con nombre. Se puede pensar que el objeto desempeña un papel que es un conjunto de protocolos. Una persona (tipo concreto) podría ser un hombre, un padre, un esposo, un amigo y un empleado, pero no puedo imaginar muchas funciones que consideren a la entidad como la suma de más de 2 de ellas.

Supongo que es posible que un objeto complejo pueda cumplir una serie de protocolos diferentes, pero aún así sería difícil llegar a una API de 50 métodos. La mayoría de los protocolos tienen 1 o 2 métodos, quizás 3, ¡pero nunca 50! Cualquier entidad que tenga 50 métodos debe integrarse en un conjunto de componentes más pequeños, cada uno con sus propias responsabilidades. La entidad en general presentaría una interfaz más simple que abstrae la suma total de las API dentro de ella.

En lugar de pensar en términos de objetos y métodos, comience a pensar en términos de abstracciones y contratos y qué roles juega un sujeto en algún contexto.

Mario T. Lanza
fuente