接口隔离原理:如果接口存在大量重叠该怎么办?


9

摘自敏捷软件开发,原理,模式和实践:Pearson新国际版

有时,由不同客户端组调用的方法会重叠。如果重叠很小,则组的接口应保持分离。公用功能应在所有重叠的接口中声明。服务器类将从每个接口继承通用功能,但仅实现一次。

鲍伯叔叔,谈论有微小重叠的情况。

如果出现大量重叠该怎么办?

说我们有

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};

如果UiInterface1和之间有大量重叠,该UiInterface2怎么办?


当我遇到一个高度重叠的接口时,我创建了一个父接口,该接口对通用方法进行分组,然后从该通用方法继承以创建专门化的接口。但!如果您不希望任何人不使用专业化就使用公共接口,那么实际上您需要进行代码复制,因为如果引入不同的公共接口,人们可以使用该接口。
安迪

这个问题对我来说有点模糊,可以根据情况提供许多不同的解决方案。为什么重叠会增加?
Arthur Havlicek

Answers:


1

铸件

几乎可以肯定,这将与被引用的书的方法完全相切,但是更好地符合ISP的一种方法是使用QueryInterfaceCOM样式的方法在代码库的一个中心区域包含转换思维。

在纯接口上下文中设计重叠接口的许多诱惑通常来自于希望使接口“自给自足”,而不是执行一种精确的,类似于狙击手的职责。

例如,设计如下客户端功能似乎很奇怪:

// 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从两者IPositionIParenting(希望有个更好的名字)。使用纯接口,它不应该像通常使用的那些整体式深层次方法(Qt,MFC等)那样严重地违反ISP,在这种情况下,由于过度违反ISP的那种水平,文档常常感到需要隐藏不相关的成员设计),所以一种务实的方法可能只是在这里和那里接受一些重叠。但是,这种COM样式的方法避免了为您将要使用的每种组合创建统一接口的需要。在这种情况下,完全消除了“自给自足”的顾虑,这通常消除了设计界面的最终诱惑,而这些界面的职责重叠,需要与SRP和ISP共同应对。


11

您必须根据具体情况作出判断。

首先,请记住SOLID原则就是……原则。他们不是规则。他们不是银弹。它们只是原则。这并不是要摆脱它们的重要性,您应该始终倾向于跟随它们。但是第二次它们带来了一定程度的疼痛,您应该抛弃它们直到需要它们。

考虑到这一点,请考虑一下为什么首先要分离出接口。接口的想法是说“如果此消耗代码需要在使用的类上实现一组方法,那么我需要在实现上设置一个契约:如果您使用此接口为我提供一个对象,那么我可以工作用它。”

ISP的目的是说:“如果我需要的合同只是现有接口的一个子集,则不应在将来可能传递给我的方法的任何类上强制使用现有接口。”

考虑以下代码:

public interface A
{
    void X();
    void Y();
}

public class Foo
{
     public void ConsumeX(A a)
     {
         a.X();
     }
}

现在我们遇到一种情况,如果要将新对象传递给ConsumeX,它必须实现X()和Y()以适合合同。

因此,我们现在应该更改代码,使其类似于下一个示例吗?

public interface A
{
    void X();
    void Y();
}

public interface B
{
    void X();
}

public class Foo
{
     public void ConsumeX(B b)
     {
         b.X();
     }
}

ISP建议我们应该这样做,所以我们应该倾向于该决定。但是,没有上下文,很难确定。我们可能会扩展A和B吗?它们可能会独立扩展吗?B是否有可能实现A不需要的方法?(如果没有,我们可以使A从B派生。)

这是您必须做出的判断。而且,如果您确实没有足够的信息来进行该调用,则应该选择最简单的选项,这很可能是第一个代码。

为什么?因为以后很容易改变主意。当您需要该新类时,只需创建一个新接口并在旧类中实现它们。


1
“首先,请记住,SOLID原则就是……原则。它们不是规则。它们不是万灵丹。它们只是原则。这并不是要摆脱其重要性,您应该始终精益求精跟随他们。但是第二次他们带来了一定程度的痛苦,您应该抛弃他们直到需要他们。” 这应该在每本设计模式/原理书的第一页上。它也应该每隔50页出现一次,以提醒您。
Christian Rodriguez
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.