使用接口还是抽象类更好的方法来分离定义和实现?
我个人不喜欢将引用计数对象与其他对象混合在一起。我想象在维护大型项目时,这可能会成为噩梦。
但是有时我需要从两个或多个类/接口派生一个类。
你有什么经验吗?
使用接口还是抽象类更好的方法来分离定义和实现?
我个人不喜欢将引用计数对象与其他对象混合在一起。我想象在维护大型项目时,这可能会成为噩梦。
但是有时我需要从两个或多个类/接口派生一个类。
你有什么经验吗?
理解这一点的关键是要认识到,它不仅涉及到定义与实现之间的区别。它是有关于用不同的方式描述同一名称的:
假设你正在建立一个厨房模型。(为以下食物类比表示歉意,我刚从午餐回来...)你有三种基本的餐具 - 叉子、刀和勺子。 它们都属于餐具类别,所以我们会对其进行建模(省略了一些无聊的东西,如支持字段):
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;
这些都描述了任何餐具都共有的数据和功能,例如它是由什么材料制成的,它的重量(这取决于具体的类型)等等。但是您会注意到抽象类实际上并没有做任何事情。一个TFork和一个TKnife没有太多共同之处可以放在基类中。您可以使用TFork技术上进行Cut操作,但使用TSpoon可能有点勉强,那么如何反映只有一些餐具可以执行特定任务呢?
好吧,我们可以开始扩展层次结构,但这会变得混乱:
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;
这很有道理,因为只有餐具才会放在洗碗机里,至少在我们非常简陋的厨房里是这样,没有像盘子或杯子这样的奢侈品。即使 TSkewer
和 TShovel
在技术上可以参与进食过程,它们可能也不适合放在洗碗机里。
另一方面:
type
THungryMan = class
procedure EatChicken(food : TFood; utensil : TUtensil);
end;
这可能不是很好。他不能只用一把 TKnife
来吃饭(至少不容易)。同时要求使用 TFork
和 TKnife
也没有意义;如果只是鸡翅膀呢?
这样做更加合理:
type
THungryMan = class
procedure EatPudding(food : TFood; scoop : IScoop);
end;
现在我们可以给他TFork
、TSpoon
或者TShovel
,他会很高兴,但是不要给他TKnife
,虽然它也是餐具,但在这里并没有什么用处。
你也会注意到,第二个版本对类层次结构的变化更加不敏感。如果我们决定将TFork
改为继承自TWeapon
,只要它仍然实现了IScoop
,我们的人还是很高兴的。
在这里,我也有点简略地概述了引用计数问题,我认为@Deltics说得最好。只是因为你有那个AddRef
并不意味着你需要像TInterfacedObject
一样使用它。接口引用计数是一种附带功能,它是一个有用的工具,当你需要它的时候,但如果你要混合接口和类语义(很多情况下都是这样),使用引用计数特性作为内存管理形式并不总是合理的。
实际上,我甚至可以说,大多数时间,你可能不想要引用计数语义。是的,在这里,我说了。我一直觉得整个引用计数的事情只是为了支持OLE自动化之类的(IDispatch
)。除非你有一个很好的理由想要自动销毁你的接口,否则就不要使用TInterfacedObject
。当你需要时,你总是可以改变它——这就是使用接口的目的!从高层次设计的角度考虑接口,而不是从内存/生存期管理的角度考虑。
所以故事的寓意是:
当你需要一个对象来支持某些特定功能时,请尝试使用接口。
当对象属于同一族,并且你希望它们共享公共特性时,请继承自一个公共基类。
如果两种情况都适用,则同时使用!
根据我的经验,在极大型项目中,这两种模式都能够很好地运作,甚至可以在不产生任何问题的情况下共存。接口比类继承具有更多优势,因为您可以将特定接口添加到多个不从共同祖先继承的类中,或者至少不会在已经被证明可用的代码层次结构中引入新错误的风险。
我非常讨厌COM接口,除非别人已经创建了一个,否则我永远不会使用它们。也许这是因为我不信任COM和类型库的东西。我甚至会将接口伪装成带有回调插件的类,而不是使用接口。我想知道是否还有其他人像我一样感到痛苦,并避免使用接口,就好像它们是一种瘟疫一样。
我知道有些人会认为我避免使用接口是一种弱点。但我认为所有使用接口的Delphi代码都有一种“代码气味”。我喜欢使用委托和任何其他机制,将我的代码分成几个部分,并尝试尽可能多地使用类,永远不使用接口。我并不是说这是好的,我只是说我有我的理由,我有一个规则(有时可能是错误的,对于某些人来说总是错误的):我避免使用接口。
IUnknown
的接口。由于 Delphi 没有多重继承(这很可能是一件好事),因此您的TBase
是无用的。您已经可以拥有一个抽象基类了。 - mghie