铸件
几乎可以肯定,这将与被引用的书的方法完全相切,但是更好地符合ISP的一种方法是使用QueryInterface
COM样式的方法在代码库的一个中心区域包含转换思维。
在纯接口上下文中设计重叠接口的许多诱惑通常来自于希望使接口“自给自足”,而不是执行一种精确的,类似于狙击手的职责。
例如,设计如下客户端功能似乎很奇怪:
// 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;
}
...以及非常丑陋/危险,因为我们正在漏掉使用这些接口对客户端代码进行容易出错的转换的责任,并且/或者将同一对象作为参数多次传递给同一参数的多个参数功能。因此,我们最终往往希望设计出更加稀释接口,整合的关注IParenting
,并IPosition
在一个地方,像IGuiElement
或类似的东西,然后变得易于使用的,将同样受到诱惑,有更多的成员函数正交接口的关注重叠同样的“自给自足”的理由。
混合责任与铸造
当设计具有完全精炼的,超奇异的职责的接口时,诱惑通常是要么接受一些贬低或合并接口以履行多个职责(并因此承担ISP和SRP)。
通过使用COM样式的方法(只是QueryInterface
一部分),我们采用了向下转换的方法,但是将强制转换整合到了代码库的一个中心位置,并且可以执行以下操作:
// 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;
}
……当然希望使用类型安全的包装器,并且可以集中构建所有这些内容,以获得比原始指针更安全的东西。
这样,设计重叠接口的诱惑通常会减少到绝对的最低限度。它允许您设计职责非常单一的界面(有时仅包含一个成员函数),您可以混合和匹配自己喜欢的所有内容而不必担心ISP,并在C ++的运行时获得伪鸭类型的灵活性(当然可以权衡运行时惩罚以查询对象以查看它们是否支持特定接口)。例如,在具有软件开发套件的设置中,运行时部分可能很重要,在该设置中,功能不会预先包含实现这些接口的插件的编译时信息。
范本
如果有可能使用模板(我们事先有必要的编译时信息,但在获得对象之前不会丢失信息),那么我们可以简单地做到这一点:
// 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;
}
...当然,在这种情况下,该parent
方法将必须返回相同的Entity
类型,在这种情况下,我们可能希望避免直接使用接口(因为它们经常会丢失类型信息,而倾向于使用基指针)。
实体组件系统
如果您从灵活性或性能的角度出发进一步追求COM风格的方法,则通常会得到一个类似于游戏引擎在行业中应用的实体组件系统。到那时,您将完全垂直于许多面向对象的方法,但是ECS可能适用于GUI设计(我曾考虑过在一个面向场景的焦点之外使用ECS的地方,但考虑到此后为时已晚)尝试使用COM风格的方法尝试一下)。
请注意,就GUI工具包设计而言,这种COM样式的解决方案是完全可用的,而ECS甚至更多,因此它不会得到大量资源的支持。但是,它绝对可以使您减轻设计界面的诱惑,因为这些界面的职责重叠到了最低限度,这常常使它成为无关紧要的问题。
务实的方法
当然,另一种选择是放宽您的防护,或在粒度级别上设计接口,然后开始继承它们以创建您使用的更粗略的接口,例如IPositionPlusParenting
从两者IPosition
和IParenting
(希望有个更好的名字)。使用纯接口,它不应该像通常使用的那些整体式深层次方法(Qt,MFC等)那样严重地违反ISP,在这种情况下,由于过度违反ISP的那种水平,文档常常感到需要隐藏不相关的成员设计),所以一种务实的方法可能只是在这里和那里接受一些重叠。但是,这种COM样式的方法避免了为您将要使用的每种组合创建统一接口的需要。在这种情况下,完全消除了“自给自足”的顾虑,这通常消除了设计界面的最终诱惑,而这些界面的职责重叠,需要与SRP和ISP共同应对。