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

mar*_*_ja 22 delphi oop interface abstract-base-class

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

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

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

你有什么经历?

Aar*_*ght 24

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

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

假设你在为厨房做模特.(对于下面的食物类比事先道歉,我刚从午餐回来......)你有三种基本类型的器具 - 叉子,刀子和勺子.这些都符合器具类别,因此我们将对其进行建模(我省略了一些无聊的东西,如支撑场):

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;
Run Code Online (Sandbox Code Playgroud)

这一切都描述了任何器具共有的数据和功能 - 它的构成,重量(取决于具体类型)等等.但是你会发现抽象类并没有真正任何事情.A TForkTKnife没有太多可以放在基类中的共同点.你可以在技术上Cut用a TFork,但TSpoon可能是一个延伸,所以如何反映只有一些器具可以做某些事情的事实?

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

type
    TSharpUtensil = class
    public
        procedure Cut(food : TFood); virtual; abstract;
    end;
Run Code Online (Sandbox Code Playgroud)

这会照顾到尖锐的,但如果我们想以这种方式分组呢?

type
    TLiftingUtensil = class
    public
        procedure Lift(food : TFood); virtual; abstract;
    end;
Run Code Online (Sandbox Code Playgroud)

TFork并且TKnife两者都适合TSharpUtensil,但是TKnife提起一块鸡是非常糟糕的.我们最终要么必须选择其中一个层次结构,要么只是将所有这些功能推送到通用中TUtensil,并且派生类只是拒绝实现无意义的方法.从设计的角度来看,这不是我们想要陷入困境的情况.

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

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

    IScoop = interface
        procedure Scoop(food : TFood);
    end;
Run Code Online (Sandbox Code Playgroud)

现在我们可以弄清楚具体类型的作用:

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;
Run Code Online (Sandbox Code Playgroud)

我想每个人都有这个主意.关键点(没有双关语意思)是我们对整个过程有非常细粒度的控制,我们不必做出任何权衡.我们在这里使用继承接口,选择不是互斥的,只是我们只在抽象类中包含真正对所有派生类型都很常见的功能.

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

type
    TDishwasher = class
        procedure Wash(utensils : Array of TUtensil);
    end;
Run Code Online (Sandbox Code Playgroud)

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

另一方面:

type
    THungryMan = class
        procedure EatChicken(food : TFood; utensil : TUtensil);
    end;
Run Code Online (Sandbox Code Playgroud)

这可能不太好.他不能只吃一个TKnife(好吧,不容易).而同时需要一个TForkTKnife没有意义要么; 如果是鸡翅呢?

这更有意义:

type
    THungryMan = class
        procedure EatPudding(food : TFood; scoop : IScoop);
    end;
Run Code Online (Sandbox Code Playgroud)

现在,我们可以给他要么TFork,TSpoon或者TShovel,他是幸福的,但不是TKnife,这仍然是一个具,但并没有真正帮助在这里.

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


我对这里的引用计数问题也略有不同,我认为@Deltics说得最好; 只是因为你有这AddRef并不意味着你需要用它来做同样的事情TInterfacedObject.接口引用计数是一种偶然的特性,对于那些需要它的时候它是一个有用的工具,但是如果你要将接口与类语义混合(通常你是),它并不总是如此将引用计数功能用作内存管理的一种形式.

事实上,我甚至可以说大多数时候,你可能不希望引用计数语义.是的,我说,我说.我总觉得整个ref-counting的事情只是为了帮助支持OLE自动化等等(IDispatch).除非你有充分的理由想要自动破坏你的界面,否则就忘了它,不要使用它TInterfacedObject.您可以随时根据需要进行更改 - 这是使用界面的重点!从高级设计的角度考虑接口,而不是从内存/生命周期管理的角度考虑.


所以故事的寓意是:

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

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

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


Leo*_*Leo 8

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

  • 如果你没有类层次结构,并且你不想构建一个层次结构,并且将不相关的类强制到同一个层次结构中甚至没有意义 - 但是你想要对某些类进行相同的处理而不必知道类的具体名称 - >

    接口是可行的方法(例如,考虑Javas ComparableIterateable,如果您必须从这些类派生(假设它们是classes =),它们将完全没用.

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


Del*_*ics 5

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

如果从TInterfacedObject派生,对象生存期确实会被引用计数,但是如果从TObject派生自己的类并实现IUnknown而不实际计算引用并且在Release的实现中没有释放"self"那么你将得到一个支持的基类接口但正常管理生命周期.

由于自动生成对编译器注入的AddRef()和Release()的调用,您仍然需要小心这些接口引用,但这与对常规TObject的"悬空引用"的小心并没有多大区别.

这是我过去在复杂和大型项目中成功使用的东西,甚至包括支持接口的ref计数和非ref计数对象.