A veces, los métodos invocados por diferentes grupos de clientes se superponen. Si la superposición es pequeña, las interfaces para los grupos deben permanecer separadas. Las funciones comunes deben declararse en todas las interfaces superpuestas. La clase de servidor heredará las funciones comunes de cada una de esas interfaces, pero las implementará solo una vez.
Tío Bob, habla sobre el caso cuando hay una superposición menor.
¿Qué debemos hacer si hay una superposición significativa?
Di que tenemos
Class UiInterface1;
Class UiInterface2;
Class UiInterface3;
Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};
¿Qué debemos hacer si hay una superposición significativa entre UiInterface1
y UiInterface2
?
Respuestas:
Fundición
Es casi seguro que será una tangente completa al enfoque del libro citado, pero una forma de ajustarse mejor al ISP es adoptar una mentalidad de fundición en un área central de su base de código utilizando un
QueryInterface
enfoque de estilo COM.Muchas de las tentaciones para diseñar interfaces superpuestas en un contexto de interfaz puro a menudo provienen del deseo de hacer que las interfaces sean "autosuficientes" más que desempeñar una responsabilidad precisa, similar a un francotirador.
Por ejemplo, puede parecer extraño diseñar funciones de cliente como esta:
... así como bastante feo / peligroso, dado que estamos perdiendo la responsabilidad de realizar una conversión propensa a errores al código del cliente usando estas interfaces y / o pasando el mismo objeto como argumento varias veces a múltiples parámetros de la misma función. Por lo tanto, a menudo deseamos diseñar una interfaz más diluida que consolide las inquietudes
IParenting
yIPosition
en un lugar, comoIGuiElement
o algo así, que luego se vuelve susceptible de superponerse con las inquietudes de las interfaces ortogonales que también estarán tentadas a tener más funciones miembro para la misma razón de "autosuficiencia".Mezcla de responsabilidades versus casting
Cuando se diseñan interfaces con una responsabilidad totalmente singular y ultradifilada, la tentación a menudo será aceptar algunas interfaces de downcasting o consolidar para cumplir con múltiples responsabilidades (y, por lo tanto, pisar tanto en ISP como en SRP).
Al usar un enfoque de estilo COM (solo la
QueryInterface
parte), jugamos con el enfoque de downcasting pero consolidamos la conversión a un lugar central en la base de código, y podemos hacer algo más como esto:... por supuesto con suerte con envoltorios de tipo seguro y todo lo que puede construir centralmente para obtener algo más seguro que los punteros sin formato.
Con esto, la tentación de diseñar interfaces superpuestas a menudo se mitiga al mínimo absoluto. Le permite diseñar interfaces con responsabilidades muy singulares (a veces solo una función miembro dentro) que puede mezclar y combinar todo lo que quiera sin preocuparse por ISP, y obtener la flexibilidad de escribir pseudo-pato en tiempo de ejecución en C ++ (aunque, por supuesto, con la compensación de las penalizaciones de tiempo de ejecución para consultar objetos para ver si admiten una interfaz particular). La parte de tiempo de ejecución puede ser importante, por ejemplo, en una configuración con un kit de desarrollo de software donde las funciones no tendrán la información en tiempo de compilación de los complementos de antemano que implementan estas interfaces.
Plantillas
Si las plantillas son una posibilidad (tenemos la información necesaria en tiempo de compilación por adelantado que no se pierde para el momento en que agarramos un objeto, es decir), entonces simplemente podemos hacer esto:
... por supuesto, en tal caso, el
parent
método tendría que devolver el mismoEntity
tipo, en cuyo caso probablemente queremos evitar las interfaces por completo (ya que a menudo querrán perder información de tipo a favor de trabajar con punteros de base).Sistema de entidad-componente
Si comienza a seguir el enfoque de estilo COM desde un punto de vista de flexibilidad o rendimiento, a menudo terminará con un sistema de componente de entidad similar al que aplican los motores de juego en la industria. En ese punto, estará completamente perpendicular a muchos enfoques orientados a objetos, pero ECS podría ser aplicable al diseño de GUI (un lugar que he contemplado usar ECS fuera de un enfoque orientado a escena, pero lo consideré demasiado tarde después decidirse por un enfoque de estilo COM para probar allí).
Tenga en cuenta que esta solución de estilo COM está completamente disponible en lo que respecta a los diseños del kit de herramientas GUI, y ECS sería aún más, por lo que no es algo que esté respaldado por muchos recursos. Sin embargo, definitivamente le permitirá mitigar las tentaciones de diseñar interfaces que tienen responsabilidades superpuestas al mínimo absoluto, lo que a menudo lo convierte en una preocupación.
Enfoque pragmático
La alternativa, por supuesto, es relajar un poco la guardia o diseñar interfaces en un nivel granular y luego comenzar a heredarlas para crear interfaces más gruesas que use, como las
IPositionPlusParenting
que se derivan de ambosIPosition
yIParenting
(Ojalá con un nombre mejor que ese). Con interfaces puras, no debería violar a ISP tanto como esos enfoques monolíticos de jerarquía profunda comúnmente aplicados (Qt, MFC, etc.), donde la documentación a menudo siente la necesidad de ocultar miembros irrelevantes dado el nivel excesivo de violación de ISP con esos tipos de diseños), por lo que un enfoque pragmático podría simplemente aceptar cierta superposición aquí y allá. Sin embargo, este tipo de enfoque de estilo COM evita la necesidad de crear interfaces consolidadas para cada combinación que usará. La preocupación de "autosuficiencia" se elimina por completo en tales casos, y eso a menudo eliminará la fuente última de tentación para diseñar interfaces que tengan responsabilidades superpuestas que quieran luchar con SRP e ISP.fuente
Esta es una decisión judicial que debe hacer, caso por caso.
En primer lugar, recuerde que los principios SÓLIDOS son solo eso ... principios. No son reglas. No son una bala de plata. Son solo principios. Eso no quita importancia, siempre debes inclinarte por seguirlos. Pero en el momento en que introducen un nivel de dolor, debes deshacerte de ellos hasta que los necesites.
Con eso en mente, piense por qué está separando sus interfaces en primer lugar. La idea de una interfaz es decir "Si este código de consumo requiere que se implemente un conjunto de métodos en la clase que se consume, necesito establecer un contrato para la implementación: si me proporciona un objeto con esta interfaz, puedo trabajar con eso."
El propósito del ISP es decir "Si el contrato que requiero es solo un subconjunto de una interfaz existente, no debería aplicar la interfaz existente en ninguna clase futura que pueda pasar a mi método".
Considere el siguiente código:
Ahora tenemos una situación en la que, si queremos pasar un nuevo objeto a ConsumeX, tiene que implementar X () e Y () para ajustarse al contrato.
Entonces, ¿deberíamos cambiar el código, ahora mismo, para que se vea como el siguiente ejemplo?
ISP sugiere que deberíamos, por lo que deberíamos inclinarnos hacia esa decisión. Pero, sin contexto, es difícil estar seguro. ¿Es probable que extendamos A y B? ¿Es probable que se extiendan independientemente? ¿Es probable que B implemente métodos que A no requiere? (Si no, podemos hacer que A se derive de B.)
Este es el juicio que debes hacer. Y, si realmente no tiene suficiente información para hacer esa llamada, probablemente debería tomar la opción más simple, que bien podría ser el primer código.
¿Por qué? Porque es fácil cambiar de opinión más tarde. Cuando necesite esa nueva clase, simplemente cree una nueva interfaz e implemente ambas en su clase anterior.
fuente