抽象与接口 - Delphi 中分离定义和实现

发布于 2024-08-22 13:36:38 字数 134 浏览 6 评论 0原文

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

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

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

你的经验是什么?

What is the better approach for separating definition and implementation, using interfaces or abstract classes?

I actually I don't like mixing reference counted objects with other objects. I imagine that this can become a nightmare when maintaining large projects.

But sometimes I would need to derive a class from 2 or more classes/interfaces.

What is your experience?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

栖竹 2024-08-29 13:36:38

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

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

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

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;

这一切都描述了任何器具通用的数据和功能 - 它是由什么制成的但您会注意到抽象类实际上并没有做任何事情。 TForkTKnife 实际上并没有太多可以放入基类中的共同点。从技术上讲,您可以使用 TForkCut,但是 TSpoon 可能会有点牵强,那么如何反映只有 some< /em> 器皿可以做某些事情吗?

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

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

这会照顾到尖锐的层次结构,但如果我们想以这种方式分组怎么办?

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

TForkTKnife 都可以放在 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 吃饭(嗯,不容易)。同时需要 TForkTKnife 也没有意义;如果是鸡翅怎么办?

这更有意义:

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

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

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


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

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


所以这个故事的寓意是:

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

  • 当对象属于同一族并且您希望它们共享共同的功能时,请从共同的基类继承。

  • 如果两种情况都适用,则两者都使用!

The key to understanding this is to realize that it's about more than just definition vs. implementation. It's about different ways of describing the same noun:

  • Class inheritance answers the question: "What kind of object is this?"
  • Interface implementation answers the question: "What can I do with this object?"

Let's say you're modeling a kitchen. (Apologies in advance for the following food analogies, I just got back from lunch...) You have three basic types of utensils - forks, knives and spoons. These all fit under the utensil category, so we'll model that (I'm omitting some of the boring stuff like backing fields):

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;

This all describes data and functionality common to any utensil - what it's made of, what it weighs (which depends on the concrete type), etc. But you'll notice that the abstract class doesn't really do anything. A TFork and TKnife don't really have much more in common that you could put in the base class. You can technically Cut with a TFork, but a TSpoon might be a stretch, so how to reflect the fact that only some utensils can do certain things?

Well, we can start extending the hierarchy, but it gets messy:

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

That takes care of the sharp ones, but what if we want to group this way instead?

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

TFork and TKnife would both fit under TSharpUtensil, but TKnife is pretty lousy for lifting up a piece of chicken. We end up either having to choose one of these hierarchies, or just shove all of this functionality into the general TUtensil and have derived classes simply refuse to implement the methods that make no sense. Design-wise, it's not a situation we want to find ourselves stuck in.

Of course the real problem with this is that we're using inheritance to describe what an object does, not what it is. For the former, we have interfaces. We can clean up this design a lot:

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

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

Now we can sort out what the concrete types do:

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;

I think everybody gets the idea. The point (no pun intended) is that we have very fine-grained control over the whole process, and we don't have to make any tradeoffs. We're using both inheritance and interfaces here, the choices are not mutually exclusive, it's just that we only include functionality in the abstract class that's really, truly common to all derived types.

Whether or not you choose to use the abstract class or one or more of the interfaces downstream really depends on what you need to do with it:

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

This makes sense, because only utensils go in the dishwasher, at least in our very limited kitchen which does not include such luxuries as dishes or cups. The TSkewer and TShovel probably don't go in there, even though they can technically participate in the eating process.

On the other hand:

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

This might not be so good. He can't eat with just a TKnife (well, not easily). And requiring both a TFork and TKnife doesn't make sense either; what if it's a chicken wing?

This makes a lot more sense:

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

Now we can give him either the TFork, TSpoon, or TShovel, and he's happy, but not the TKnife, which is still a utensil but doesn't really help out here.

You'll also notice that the second version is less sensitive to changes in the class hierarchy. If we decide to change TFork to inherit from TWeapon instead, our man's still happy as long as it still implements IScoop.


I've also sort of glossed over the reference-counting issue here, and I think @Deltics said it best; just because you have that AddRef doesn't mean you need to do the same thing with it that TInterfacedObject does. Interface reference-counting is sort of an incidental feature, it's a helpful tool for those times when you need it, but if you're going to be mixing interface with class semantics (and very often you are), it doesn't always make sense to use the reference-counting feature as a form of memory-management.

In fact, I'd go so far as to say that most of the time, you probably don't want the reference counting semantics. Yes, there, I said it. I always felt that the whole ref-counting thing was just to help support OLE automation and such (IDispatch). Unless you have a good reason to want the automatic destruction of your interface, just forget about it, don't use TInterfacedObject at all. You can always change it when you need it - that's the point of using an interface! Think about interfaces from a high-level design point of view, not from the perspective of memory/lifetime management.


So the moral of the story is:

  • When you require an object to support some particular functionality, try to use an interface.

  • When objects are of the same family and you want them to share common features, inherit from a common base class.

  • And if both situations apply, then use both!

愚人国度 2024-08-29 13:36:38

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

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

    接口是最佳选择(例如,如果您必须从这些类派生(前提是它们是类=),请考虑 Java ComparableIterateable ,它们将毫无用处。

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

I doubt that this is a question of "better approach" - they just have different use cases.

  • If you don't have a class hierarchy, and you don't want to build one, and it doesn't even make sense to force unrelated classes into the same hierarchy - but you want to treat some classes equal anyways without having to know the specific name of the class ->

    Interfaces are the way to go (think about Javas Comparable or Iterateable for instance, if you would have to derive from those classes (provided that they were classes =), they would be totally useless.

  • If you have a reasonable class hiearchy, you can use abstract classes to provide a uniform access point to all classes of this hierarchy, with the benefit that you even can implement default behaviour and such.

执笔绘流年 2024-08-29 13:36:38

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

如果您从 TInterfacedObject 派生,则对象生命周期确实会进行引用计数,但如果您从 TObject 派生自己的类并实现 IUnknown,而没有实际计数引用,也没有在 Release 的实现中释放“self”,那么您将获得一个支持以下功能的基类:接口,但具有正常的显式管理生命周期。

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

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

You can have interfaces without reference counting. The compiler adds calls to AddRef and Release for all interfaces but the lifetime management aspect of those objects is entirely down to the implementation of IUnknown.

If you derive from TInterfacedObject the object lifetime will indeed be reference counted, but if you derive your own class from TObject and implement IUnknown without actually counting references and without freeing "self" in the implementation of Release then you will get a base class that supports interfaces but has an explicitly managed lifetime as normal.

You still need to be careful with those interface references due to the automatically generated calls to AddRef() and Release() injected by the compiler, but this is really not much different from being careful with "dangling references" to regular TObject's.

It is something that I have successfully used in sophisticated and large projects in the past, even mixing ref counted and non-ref counted objects supporting interfaces.

只有影子陪我不离不弃 2024-08-29 13:36:38

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

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

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

  3. 当您需要与仅类派生不同的层次结构时,可以使用接口。接口始终派生自 IInterface,IInterface 被建模为基于 COM 的 IUnknown:您可以获得随之推送的引用计数和查询类型信息。

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

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

  • 如果可能,使用 virtual 和 Dynamic 并覆盖派生类中的那些。
  • 使用接口时:创建一个基类,接受对接口实例的引用作为变量,并使接口尽可能简单;对于每个方面,尝试创建一个单独的 intercae 变量。当未指定接口时,尝试使用默认实现。
  • 如果上述内容太有限:开始使用 TInterfacedObject-s 并真正注意可能的循环以及内存泄漏。

In Delphi there are three ways to separate the definition from the implementation.

  1. You have a separation in each unit where you can place the publuc classes in the interface section and it's implementation in the implementation section. The code still resides in the same unit but at least a "user" of your code only needs to read the interface and not the guts of the implementation.

  2. When using virtual or dynamic-ally declared functions in your class you can override those in
    subclasses. This is the way for most class libraries to use. Look at TStream and it's derived classes like THandleStream, TFileStream etc.

  3. You can use interfaces when you need a different hierarchy than only the class derivation. Interfaces are always derived from IInterface which is modelled to a COM based IUnknown: you get reference counting and querying type info pushed along with it.

For 3:
- If you derive from TInterfacedObject the reference counting indeed takes care of the lifetime of your objects but this is not ness.
- TComponent for example also implements the IInterface but WITHOUT reference counting. This comes with a BIG warning: make sure that your interface references are set to nil before destroying your object. The comiler will still insert decref calls to your interface which looks still valid but isn't. Second: people will not expect this behaviour.

Choosing betweeen 2 and 3 is sometimes quite subjective. I tend to use the following:

  • If possible, use virtual and Dynamic and override those in derived classes.
  • When working with interfaces: make a base class that accepts the reference to the interfcae instance as a variable and keep your interfaces as simple as possible; for every aspect try to create a separate intercae variable. Try to have a default implementation in place when no interface is specified.
  • If the above is too limiting: start using TInterfacedObject-s and really look out for the possible cycles and hence memory leaks.
甜尕妞 2024-08-29 13:36:38

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

In my experience with extremely large projects, both models not only work well, they even can co-exist without any problems. Interfaces have the advantage over class inheritance in that you can add a specific interface to multiple classes that don't descend from a common ancestor, or at least without not introducing code so far back into the hierarchy that you risk introducing new bugs in code that has already been proved working.

表情可笑 2024-08-29 13:36:38

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

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

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

I dislike COM Interfaces to the point of never ever ever using them except when someone else has produced one. Maybe this came from my distrust of COM and Type Library stuff. I have even "faked" interfaces as classes with callback plug-ins rather than use interfaces. I wonder if anyone else has felt my pain, and avoided the use of Interfaces as if they were a plague?

I know some people will consider my avoidance of interfaces a weakness. But I think that all Delphi code using Interfaces has a kind of "code smell".

I like to use delegates and any other mechanism I can, to separate my code into sections, and try do do everything I can with classes, and never ever use interfaces. I'm not saying that's good, I'm just saying that I have my reasons, and I have a rule (that may be sometimes wrong, and for some people always wrong): I avoid Interfaces.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文