抽象与接口 - Delphi 中分离定义和实现
使用接口或抽象类来分离定义和实现的更好方法是什么?
实际上我不喜欢将引用计数对象与其他对象混合。我想这在维护大型项目时可能会成为一场噩梦。
但有时我需要从 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
理解这一点的关键是要认识到这不仅仅是定义与实现的问题。这是关于描述同一名词的不同方式:
假设您正在为厨房建模。 (提前为以下食物类比道歉,我刚吃完午饭回来……)你有三种基本的器具——叉子、刀子和勺子。这些都属于器具类别,因此我们将对其进行建模(我省略了一些无聊的东西,例如支持字段):
这一切都描述了任何器具通用的数据和功能 - 它是由什么制成的但您会注意到抽象类实际上并没有做任何事情。
TFork
和TKnife
实际上并没有太多可以放入基类中的共同点。从技术上讲,您可以使用TFork
来Cut
,但是TSpoon
可能会有点牵强,那么如何反映只有 some< /em> 器皿可以做某些事情吗?好吧,我们可以开始扩展层次结构,但它会变得混乱:
这会照顾到尖锐的层次结构,但如果我们想以这种方式分组怎么办?
TFork
和TKnife
都可以放在TSharpUtensil
下,但是TKnife
很难举起一块鸡肉。我们最终要么必须选择这些层次结构之一,要么只是将所有这些功能推入通用的 TUtensil 中,并且派生类只是拒绝实现没有意义的方法。从设计角度来看,我们不想陷入这种情况。当然,真正的问题是我们使用继承来描述对象做什么,而不是它是。对于前者,我们有接口。我们可以对这个设计进行很多清理:
现在我们可以理清具体类型的作用:
我想每个人都明白了。重点(没有双关语)是我们对整个过程有非常细粒度的控制,而且我们不需要做出任何权衡。我们在这里使用继承和接口,这些选择并不是相互排斥的,只是我们只在抽象类中包含对所有派生来说真正非常通用的功能类型。
您是否选择使用抽象类或一个或多个下游接口实际上取决于您需要用它做什么:
这是有道理的,因为只有餐具才能放入洗碗机,至少在我们非常有限的厨房中是这样。不包括盘子或杯子等奢侈品。
TSkewer
和TShovel
可能不会进入那里,即使它们在技术上可以参与进食过程。另一方面:
这可能不太好。他不能只用
TKnife
吃饭(嗯,不容易)。同时需要TFork
和TKnife
也没有意义;如果是鸡翅怎么办?这更有意义:
现在我们可以给他
TFork
、TSpoon
或TShovel
,他很高兴,但不是 < 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:
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):
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
andTKnife
don't really have much more in common that you could put in the base class. You can technicallyCut
with aTFork
, but aTSpoon
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:
That takes care of the sharp ones, but what if we want to group this way instead?
TFork
andTKnife
would both fit underTSharpUtensil
, butTKnife
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 generalTUtensil
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:
Now we can sort out what the concrete types do:
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:
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
andTShovel
probably don't go in there, even though they can technically participate in the eating process.On the other hand:
This might not be so good. He can't eat with just a
TKnife
(well, not easily). And requiring both aTFork
andTKnife
doesn't make sense either; what if it's a chicken wing?This makes a lot more sense:
Now we can give him either the
TFork
,TSpoon
, orTShovel
, and he's happy, but not theTKnife
, 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 fromTWeapon
instead, our man's still happy as long as it still implementsIScoop
.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 thatTInterfacedObject
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 useTInterfacedObject
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!
我怀疑这是一个“更好的方法”的问题 - 他们只是有不同的用例。
如果您没有类层次结构,并且您不想构建一个类层次结构,并且强制不相关的类进入同一层次结构甚至没有意义 - 但您想要无论如何都要平等地对待某些类,而不必知道类的具体名称 ->
接口是最佳选择(例如,如果您必须从这些类派生(前提是它们是类=),请考虑 Java Comparable 或 Iterateable ,它们将毫无用处。
如果您有一个合理的类层次结构,您可以使用抽象类为该层次结构的所有类提供统一的访问点,这样做的好处是您甚至可以实现默认行为 等等。
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.
您可以拥有没有引用计数的接口。编译器为所有接口添加对 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.
在 Delphi 中,可以使用三种方法将定义与实现分开。
每个单元都有一个分隔,您可以将 publuc 类放在接口部分,并将其实现放在实现部分。代码仍然驻留在同一个单元中,但至少代码的“用户”只需要读取接口,而不需要读取实现的内部内容。
在类中使用虚拟或动态声明的函数时,您可以覆盖这些函数
子类。这是大多数类库使用的方式。查看 TStream 及其派生类,例如 THandleStream、TFileStream 等。
当您需要与仅类派生不同的层次结构时,可以使用接口。接口始终派生自 IInterface,IInterface 被建模为基于 COM 的 IUnknown:您可以获得随之推送的引用计数和查询类型信息。
对于 3:
- 如果您从 TInterfacedObject 派生,引用计数确实会处理对象的生命周期,但这不是本质。
- 例如,TComponent 也实现了 IInterface,但没有引用计数。这伴随着一个大警告:在销毁对象之前确保将接口引用设置为 nil。编译器仍将向您的界面插入 decref 调用,该调用看起来仍然有效,但实际上并非如此。第二:人们不会期望这种行为。
在 2 和 3 之间进行选择有时是相当主观的。我倾向于使用以下内容:
In Delphi there are three ways to separate the definition from the implementation.
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.
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.
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:
根据我处理超大型项目的经验,这两种模型不仅运行良好,甚至可以毫无问题地共存。接口相对于类继承的优势在于,您可以将特定接口添加到并非源自共同祖先的多个类,或者至少不会将代码引入到层次结构中,否则您可能会冒在代码中引入新错误的风险已经被证明有效。
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.
我不喜欢 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.