Fundição
Isso quase certamente será uma tangente completa à abordagem do livro citado, mas uma maneira de se adaptar melhor ao ISP é adotar uma mentalidade de elenco em uma área central da sua base de código usando uma QueryInterface
abordagem no estilo COM.
Muitas das tentações de projetar interfaces sobrepostas em um contexto puro de interface geralmente vêm do desejo de tornar as interfaces "auto-suficientes" mais do que executar uma responsabilidade precisa, semelhante a um atirador de elite.
Por exemplo, pode parecer estranho projetar funções 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;
}
... além de muito feio / perigoso, já que estamos perdendo a responsabilidade de fazer a conversão propensa a erros no código do cliente usando essas interfaces e / ou passando o mesmo objeto como argumento várias vezes para vários parâmetros do mesmo função. Por isso, muitas vezes acabamos querendo projetar uma interface mais diluída que consolide as preocupações de IParenting
e IPosition
em um só lugar, como IGuiElement
algo que se torna suscetível de se sobrepor às preocupações de interfaces ortogonais que também serão tentadas a ter mais funções-membro para a mesma razão de "auto-suficiência".
Responsabilidades de Mistura vs. Elenco
Ao projetar interfaces com uma responsabilidade ultra-singular e totalmente destilada, a tentação geralmente é aceitar algumas downcasting ou consolidar interfaces para cumprir várias responsabilidades (e, portanto, seguir o ISP e o SRP).
Usando uma abordagem no estilo COM (apenas a QueryInterface
parte), adotamos a abordagem de downcasting, mas consolidamos o casting em um local central na base de código e podemos fazer algo mais como isto:
// 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;
}
... é claro, espero que com invólucros com segurança de tipo e tudo o que você possa construir centralmente para obter algo mais seguro do que ponteiros brutos.
Com isso, a tentação de projetar interfaces sobrepostas é frequentemente atenuada ao mínimo absoluto. Ele permite que você crie interfaces com responsabilidades muito singulares (às vezes apenas uma função de membro) que você pode misturar e combinar tudo o que quiser sem se preocupar com o ISP e obter a flexibilidade de digitar pseudo-pato em tempo de execução em C ++ (embora, é claro, com a compensação das penalidades de tempo de execução para consultar objetos para ver se eles suportam uma interface específica). A parte do tempo de execução pode ser importante em, por exemplo, uma configuração com um kit de desenvolvimento de software em que as funções não terão as informações em tempo de compilação dos plug-ins com antecedência que implementam essas interfaces.
Modelos
Se os modelos são uma possibilidade (temos as informações necessárias em tempo de compilação com antecedência, que não são perdidas no momento em que obtemos um objeto, por exemplo), podemos simplesmente fazer isso:
// 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;
}
... é claro que, nesse caso, o parent
método teria que retornar o mesmo Entity
tipo; nesse caso, provavelmente queremos evitar interfaces diretas (já que muitas vezes eles querem perder informações de tipo em favor do trabalho com ponteiros de base).
Sistema Entidade-Componente
Se você começar a adotar a abordagem no estilo COM mais do ponto de vista da flexibilidade ou do desempenho, geralmente acabará com um sistema de componente de entidade semelhante ao que os mecanismos de jogo aplicam no setor. Nesse ponto, você estará completamente perpendicular a muitas abordagens orientadas a objetos, mas o ECS pode ser aplicável ao design da GUI (um lugar que eu contemplei usar o ECS fora de um foco orientado a cena, mas o considerei tarde demais depois). adotando uma abordagem no estilo COM para tentar lá).
Observe que esta solução no estilo COM está completamente disponível no que diz respeito aos designs dos kits de ferramentas da GUI, e o ECS seria ainda mais, portanto, não é algo que será apoiado por muitos recursos. No entanto, isso definitivamente permitirá que você atenue as tentações de projetar interfaces com responsabilidades sobrepostas a um mínimo absoluto, muitas vezes tornando-a uma preocupação.
Abordagem pragmática
A alternativa, é claro, é relaxar um pouco a guarda ou projetar interfaces em um nível granular e, em seguida, começar a herdá-las para criar interfaces mais grosseiras que você usa, como as IPositionPlusParenting
derivadas de ambos IPosition
eIParenting
(espero que com um nome melhor que isso). Com interfaces puras, ele não deve violar o ISP tanto quanto as abordagens hierárquicas profundas monolíticas comumente aplicadas (Qt, MFC etc.), onde a documentação geralmente sente a necessidade de ocultar membros irrelevantes, devido ao nível excessivo de violação do ISP com esses tipos. projetos), portanto, uma abordagem pragmática pode simplesmente aceitar alguma sobreposição aqui e ali. No entanto, esse tipo de abordagem no estilo COM evita a necessidade de criar interfaces consolidadas para cada combinação que você usará. A preocupação com a "auto-suficiência" é completamente eliminada nesses casos, e isso frequentemente elimina a fonte definitiva de tentação para projetar interfaces com responsabilidades sobrepostas que desejam lutar com o SRP e o ISP.