getto
Questo sarà quasi sicuramente una tangente completa all'approccio del libro citato, ma un modo per conformarsi meglio all'ISP è quello di abbracciare una mentalità di casting in un'area centrale della base di codice usando un QueryInterface
approccio in stile COM.
Molte delle tentazioni di progettare interfacce sovrapposte in un contesto di pura interfaccia spesso derivano dal desiderio di rendere le interfacce "autosufficienti" più che eseguire una precisa responsabilità simile a un cecchino.
Ad esempio, potrebbe sembrare strano progettare funzioni client come questa:
// 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;
}
... oltre che abbastanza brutto / pericoloso, dato che stiamo perdendo la responsabilità di eseguire il casting soggetto a errori sul codice client utilizzando queste interfacce e / o passando lo stesso oggetto di un argomento più volte a più parametri dello stesso funzione. Quindi finiamo spesso per voler progettare un'interfaccia più diluita che consolidi le preoccupazioni di IParenting
e IPosition
in un posto, simile IGuiElement
o qualcosa del genere che diventa quindi suscettibile di sovrapporsi con le preoccupazioni delle interfacce ortogonali che saranno anche tentate di avere più funzioni membro per la stessa ragione dell '"autosufficienza".
Mescolare le responsabilità contro il casting
Quando si progettano interfacce con una responsabilità totalmente distinta e ultra-singolare, la tentazione sarà spesso quella di accettare alcune operazioni di downcasting o di consolidare interfacce per adempiere a responsabilità multiple (e quindi calpestare sia l'ISP che l'SRP).
Usando un approccio in stile COM (solo la QueryInterface
parte), giochiamo all'approccio di downcasting ma consolidiamo il casting in un posto centrale nella base di codice e possiamo fare qualcosa di più simile a questo:
// 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;
}
... ovviamente si spera con involucri sicuri e tutto ciò che puoi costruire centralmente per ottenere qualcosa di più sicuro dei puntatori non elaborati.
Con ciò, la tentazione di progettare interfacce sovrapposte viene spesso mitigata al minimo assoluto. Ti consente di progettare interfacce con responsabilità molto singolari (a volte solo una funzione membro all'interno) che puoi mescolare e abbinare tutto ciò che ti piace senza preoccuparti dell'ISP e ottenere la flessibilità della digitazione pseudo-anatra in fase di esecuzione in C ++ (anche se ovviamente con il compromesso delle penalità di runtime per interrogare gli oggetti per vedere se supportano una particolare interfaccia). La parte di runtime può essere importante, ad esempio, in un'impostazione con un kit di sviluppo software in cui le funzioni non avranno in anticipo le informazioni di compilazione dei plug-in che implementano queste interfacce.
Modelli
Se i template sono una possibilità (abbiamo in anticipo le informazioni necessarie in fase di compilazione che non si perdono nel momento in cui otteniamo un oggetto, ad esempio), allora possiamo semplicemente farlo:
// 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;
}
... ovviamente in tal caso, il parent
metodo dovrebbe restituire lo stesso Entity
tipo, nel qual caso probabilmente vorremmo evitare le interfacce in modo definitivo (poiché spesso vorranno perdere le informazioni sul tipo a favore del lavoro con i puntatori di base).
Sistema Entity-Component
Se inizi a perseguire ulteriormente l'approccio in stile COM da un punto di vista della flessibilità o delle prestazioni, spesso finirai con un sistema di componenti entità simile a quello che i motori di gioco applicano nel settore. A quel punto andrai completamente perpendicolare a molti approcci orientati agli oggetti, ma l'ECS potrebbe essere applicabile al design della GUI (un posto in cui ho contemplato l'uso dell'ECS al di fuori di un focus orientato alla scena, ma lo ho considerato troppo tardi dopo accontentarsi di un approccio in stile COM per provare lì).
Si noti che questa soluzione in stile COM è completamente disponibile per quanto riguarda la progettazione di toolkit GUI e ECS sarebbe ancora di più, quindi non è qualcosa che sarà supportato da molte risorse. Tuttavia, ti consentirà sicuramente di mitigare le tentazioni di progettare interfacce che hanno responsabilità sovrapposte al minimo assoluto, rendendolo spesso non preoccupante.
Approccio pragmatico
L'alternativa, ovviamente, è rilassare un po 'la guardia o progettare interfacce a livello granulare e quindi iniziare a ereditarle per creare interfacce più grossolane che usi, come quelle IPositionPlusParenting
che derivano da entrambi IPosition
eIParenting
(si spera con un nome migliore di quello). Con interfacce pure, non dovrebbe violare l'ISP tanto quanto quegli approcci monolitici di profonda gerarchia comunemente applicati (Qt, MFC, ecc.) In cui la documentazione spesso sente la necessità di nascondere membri irrilevanti dato l'eccessivo livello di violazione dell'ISP con questi tipi di disegni), quindi un approccio pragmatico potrebbe semplicemente accettare alcune sovrapposizioni qua e là. Tuttavia, questo tipo di approccio in stile COM evita la necessità di creare interfacce consolidate per ogni combinazione che userete mai. La preoccupazione di "autosufficienza" è completamente eliminata in questi casi, e ciò spesso eliminerà la fonte ultima di tentazione di progettare interfacce che hanno responsabilità sovrapposte che vogliono combattere sia con SRP che con ISP.