23

使用接口或抽象类分离定义和实现的更好方法是什么?

实际上,我不喜欢将引用计数对象与其他对象混合。我想这在维护大型项目时可能会成为一场噩梦。

但有时我需要从 2 个或更多类/接口派生一个类。

你的经验是什么?

4

6 回答 6

25

理解这一点的关键是要认识到它不仅仅是定义实现。这是关于描述同一个名词的不同方式:

  • 类继承回答了这个问题:“这是什么类型的对象?”
  • 接口实现回答了这个问题:“我能用这个对象做什么?”

假设您正在为厨房建模。(提前为以下食物类比道歉,我刚吃完午饭回来......)你有三种基本类型的器皿——叉子、刀子和勺子。这些都属于器物类别,因此我们将对其进行建模(我省略了一些无聊的东西,例如支持字段):

type
    TMaterial = (mtPlastic, mtSteel, mtSilver);

    TUtensil = class
    public
        function GetWeight : Integer; virtual; abstract;
        procedure Wash; virtual; // Yes, it's self-cleaning
    published
        property Material : TMaterial read FMaterial write FMaterial;
    end;

这一切都描述了任何器具共有的数据和功能——它是由什么组成的,它的重量(取决于具体的类型)等等。但是你会注意到抽象类并没有真正任何事情。ATForkTKnife没有更多的共同点可以放在基类中。技术上你可以Cut用 a TFork,但 aTSpoon可能有点牵强,那么如何体现只有一些器具才能做某些事情呢?

好吧,我们可以开始扩展层次结构,但它会变得混乱:

type
    TSharpUtensil = class
    public
        procedure Cut(food : TFood); virtual; abstract;
    end;

这照顾了尖锐的,但如果我们想这样分组呢?

type
    TLiftingUtensil = class
    public
        procedure Lift(food : TFood); virtual; abstract;
    end;

TFork并且TKnife两者都适合TSharpUtensil,但TKnife举起一块鸡就很糟糕。我们最终要么不得不选择这些层次结构中的一个,要么只是将所有这些功能推到通用结构中TUtensil,并且派生类干脆拒绝实现没有意义的方法。在设计方面,这不是我们想要陷入困境的情况。

当然,真正的问题是我们使用继承来描述一个对象的作用,而不是它什么。对于前者,我们有接口。我们可以清理这个设计很多:

type
    IPointy = interface
        procedure Pierce(food : TFood);
    end;

    IScoop = interface
        procedure Scoop(food : TFood);
    end;

现在我们可以梳理一下具体类型的作用:

type
    TFork = class(TUtensil, IPointy, IScoop)
        ...
    end;

    TKnife = class(TUtensil, IPointy)
        ...
    end;

    TSpoon = class(TUtensil, IScoop)
        ...
    end;

    TSkewer = class(TStick, IPointy)
        ...
    end;

    TShovel = class(TGardenTool, IScoop)
        ...
    end;

我想每个人都明白这一点。重点(没有双关语)是我们对整个过程有非常细粒度的控制,我们不必做出任何权衡。我们在这里同时使用继承接口,选择并不相互排斥,只是我们只在抽象类中包含所有派生类型真正通用的功能。

您是否选择使用抽象类或下游的一个或多个接口实际上取决于您需要使用它做什么:

type
    TDishwasher = class
        procedure Wash(utensils : Array of TUtensil);
    end;

这是有道理的,因为洗碗机里只有餐具,至少在我们非常有限的厨房里,不包括餐具或杯子等奢侈品。并且可能不会去那里TSkewerTShovel即使他们在技术上可以参与进食过程。

另一方面:

type
    THungryMan = class
        procedure EatChicken(food : TFood; utensil : TUtensil);
    end;

这可能不太好。他不能只吃一个TKnife(嗯,不容易)。并且同时要求 aTForkTKnife也没有意义;如果是鸡翅呢?

这更有意义:

type
    THungryMan = class
        procedure EatPudding(food : TFood; scoop : IScoop);
    end;

现在我们可以给他TFork,TSpoonTShovel, 他很高兴,但不是TKnife,它仍然是一个器具,但在这里并没有真正的帮助。

您还会注意到第二个版本对类层次结构的变化不太敏感。如果我们决定改为TFork继承 from TWeapon,只要它仍然实现,我们的人仍然很高兴IScoop


我也在这里掩盖了引用计数问题,我认为@Deltics 说得最好;仅仅因为你拥有它AddRef并不意味着你需要对它做同样的事情TInterfacedObject。接口引用计数是一种附带的功能,它在您需要它的时候是一个有用的工具,但是如果您要将接口与类语义混合(而且经常是这样),它并不总是使将引用计数功能用作内存管理的一种形式是有意义的。

事实上,我什至会说,大多数时候,您可能不想要引用计数语义。是的,在那里,我说过。我一直觉得整个引用计数只是为了帮助支持 OLE 自动化等 ( IDispatch)。除非您有充分的理由想要自动销毁您的界面,否则就别管它了,根本不要使用TInterfacedObject。您可以在需要时随时更改它——这就是使用界面的意义所在!从高级设计的角度考虑接口,而不是从内存/生命周期管理的角度考虑。


所以这个故事的寓意是:

  • 当您需要一个对象来支持某些特定功能时,请尝试使用接口。

  • 当对象属于同一家族并且您希望它们共享共同特征时,请从公共基类继承。

  • 如果这两种情况都适用,那么同时使用!

于 2010-02-17T19:43:28.023 回答
8

我怀疑这是一个“更好的方法”的问题——他们只是有不同的用例

  • 如果您没有类层次结构,并且您不想构建一个,并且将不相关的类强制进入相同的层次结构甚至没有意义 - 但是您无论如何都希望将某些类平等对待而不必知道类的具体名称 ->

    接口是要走的路(例如考虑Java 的ComparableIterateable,如果您必须从这些类派生(假设它们是类 =),它们将完全没用。

  • 如果你有一个合理的类层次结构,你可以使用抽象类来为这个层次结构的所有类提供一个统一的访问点,你甚至可以实现默认行为等。

于 2010-02-17T08:36:03.920 回答
5

您可以拥有没有引用计数的接口。编译器为所有接口添加对 AddRef 和 Release 的调用,但这些对象的生命周期管理方面完全取决于 IUnknown 的实现。

如果您从 TInterfacedObject 派生,则对象生命周期确实会被引用计数,但是如果您从 TObject 派生自己的类并实现 IUnknown 而不实际计算引用并且在 Release 的实现中没有释放“自我”,那么您将获得一个支持的基类接口,但像往常一样具有显式管理的生命周期。

由于编译器会自动生成对 AddRef() 和 Release() 的调用,因此您仍然需要小心这些接口引用,但这与小心对常规 TObject 的“悬空引用”实际上并没有太大区别。

这是我过去在复杂的大型项目中成功使用的东西,甚至混合了支持接口的引用计数和非引用计数对象。

于 2010-02-17T09:15:23.993 回答
3

在 Delphi 中,有三种方法可以将定义与实现分开。

  1. 您在每个单元中都有一个分隔,您可以将 publuc 类放在接口部分中,并将其实现放在实现部分中。代码仍然驻留在同一个单元中,但至少代码的“用户”只需要阅读接口而不是实现的核心。

  2. 在您的类中使用虚拟或动态声明的函数时,您可以覆盖子类中的那些。这是大多数类库使用的方式。查看 TStream 及其派生类,如 THandleStream、TFileStream 等。

  3. 当您需要与仅类派生不同的层次结构时,您可以使用接口。接口总是从 IInterface 派生的,它被建模为基于 COM 的 IUnknown:你会得到引用计数和查询类型信息。

对于 3: - 如果您从 TInterfacedObject 派生,则引用计数确实会照顾您的对象的生命周期,但这不是。- 例如,TComponent 也实现了 IInterface,但没有引用计数。这带有一个大警告:确保在销毁对象之前将接口引用设置为 nil。编译器仍会向您的界面插入 decref 调用,该调用看起来仍然有效但不是。第二:人们不会期望这种行为。

在 2 和 3 之间选择有时是相当主观的。我倾向于使用以下内容:

  • 如果可能,请使用 virtual 和 Dynamic 并覆盖派生类中的那些。
  • 使用接口时:创建一个接受对接口实例的引用作为变量的基类,并使接口尽可能简单;对于每个方面都尝试创建一个单独的 intercae 变量。当没有指定接口时,尝试使用默认实现。
  • 如果以上限制太多:开始使用 TInterfacedObject-s 并真正注意可能的循环,从而导致内存泄漏。
于 2010-02-17T08:14:11.547 回答
2

根据我处理大型项目的经验,这两种模型不仅运行良好,甚至可以毫无问题地共存。接口比类继承的优势在于,您可以将特定接口添加到多个不是来自共同祖先的类中,或者至少不将代码引入到层次结构中,以免您冒着在代码中引入新错误的风险已经证明有效。

于 2010-02-17T17:17:46.167 回答
1

我不喜欢 COM 接口,以至于我从不使用它们,除非其他人已经生产了它们。也许这来自我对 COM 和类型库的不信任。我什至将接口“伪造”为带有回调插件的类,而不是使用接口。我想知道是否有其他人感受到我的痛苦,并避免使用接口,就好像它们是瘟疫一样?

我知道有些人会认为我避免接口是一个弱点。但我认为所有使用 Interfaces 的 Delphi 代码都有一种“代码气味”。

我喜欢使用委托和任何其他我可以使用的机制,将我的代码分成多个部分,并尝试尽我所能使用类,并且从不使用接口。我并不是说这很好,我只是说我有我的理由,而且我有一个规则(这有时可能是错误的,而对于某些人来说总是错误的):我避免使用接口。

于 2010-02-17T18:51:03.067 回答