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:
// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `position` and `parenting` parameters should point to the
// same object.
Vec2i abs_position(IPosition* position, IParenting* parenting)
{
const Vec2i xy = position->xy();
auto parent = parenting->parent();
if (parent)
{
// If the entity has a parent, return the sum of the
// parent position and the entity's local position.
return xy + abs_position(dynamic_cast<IPosition*>(parent),
dynamic_cast<IParenting*>(parent));
}
return xy;
}
... 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
y IPosition
en un lugar, como IGuiElement
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:
// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should implement `IPosition` and optionally `IParenting`.
Vec2i abs_position(Object* obj)
{
// `Object::query_interface` returns nullptr if the interface is
// not provided by the entity. `Object` is an abstract base class
// inherited by all entities using this interface query system.
IPosition* position = obj->query_interface<IPosition>();
assert(position && "obj does not implement IPosition!");
const Vec2i xy = position->xy();
IParenting* parenting = obj->query_interface<IParenting>();
if (parenting && parenting->parent()->query_interface<IPosition>())
{
// If the entity implements IParenting and has a parent,
// return the sum of the parent position and the entity's
// local position.
return xy + abs_position(parenting->parent());
}
return xy;
}
... 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:
// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should have `position` and `parent` methods.
template <class Entity>
Vec2i abs_position(Entity& obj)
{
const Vec2i xy = obj.xy();
if (obj.parent())
{
// If the entity has a parent, return the sum of the parent
// position and the entity's local position.
return xy + abs_position(obj.parent());
}
return xy;
}
... por supuesto, en tal caso, el parent
método tendría que devolver el mismo Entity
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 ambos IPosition
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.