代码之家  ›  专栏  ›  技术社区  ›  markus_ja

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

  •  22
  • markus_ja  · 技术社区  · 15 年前

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

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

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

    你有什么经验?

    6 回复  |  直到 15 年前
        1
  •  24
  •   Aaronaught    15 年前

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

    • 类继承 回答问题: “这是什么东西?”
    • 接口实现 回答问题: “我能用这个东西做什么?”

    假设你在做一个厨房的模型。(为下面的食物类比道歉,我刚从午餐回来…)你有三种基本的餐具-叉子、刀和汤匙。这些都适合 器具 类别,因此我们将对其建模(我省略了一些无聊的东西,如支持字段):

    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 没有更多的共同点可以放在基类中。技术上你可以 Cut 用一个 三角叉 但A TSpoon 可能是一个延伸,所以如何反映这样一个事实 一些 器皿能做某些事情吗?

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

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

    这需要处理尖锐的问题,但是如果我们想以这种方式分组呢?

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

    三角叉 t刀 两个都适合 TSharpUtensil 但是 举起一只鸡真是太差劲了。我们要么不得不选择这些层次结构中的一个,要么将所有这些功能推送到 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;
    

    这可能不太好。他不能只吃 (嗯,不容易)。同时需要 三角叉 也没道理,如果是鸡翅呢?

    这更有意义:

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

    现在我们可以给他 三角叉 , 特斯庞 铁铲 他很开心,但不是 这仍然是一个器具,但在这里没有真正的帮助。

    您还将注意到第二个版本对类层次结构中的更改不太敏感。如果我们决定改变 三角叉 继承 TWeapon 相反,只要我们的人还活着,他还是很快乐的。 IScoop .


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

    事实上,我会这么说的 大多数时候,您可能不希望引用计数语义 . 是的,我说了。我一直觉得整个引用计数只是为了帮助支持OLE自动化等等。( IDispatch )除非你有充分的理由想要自动销毁你的界面,否则忘记它,不要使用 TinterfacedObject公司 完全。你可以随时在需要的时候更改它-这就是使用界面的意义!从高层设计的角度考虑接口,而不是从内存/生命周期管理的角度考虑。


    所以这个故事的寓意是:

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

    • 当对象 属于同一个家庭 你想让他们 共享共同功能 从公共基类继承。

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

        2
  •  8
  •   Leo user370469    15 年前

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

    • 如果你 没有类层次结构 ,并且您不想构建一个类,甚至将不相关的类强制到相同的层次结构中都没有意义——但是您希望在不必知道类的特定名称的情况下,以任何方式将某些类视为相等的类->

      接口是前进的道路(想想javas 可比的 可迭代的 例如,如果您必须从这些类派生(前提是它们是classes=),那么它们将是完全无用的。

    • 如果你 有一个合理的阶级等级制度 您可以使用抽象类为这个层次结构的所有类提供统一的访问点,这样做的好处是您甚至可以实现默认行为等。
        3
  •  5
  •   Deltics    15 年前

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

    如果您从TinterfacedObject派生,则对象生存期确实将被引用计数,但是如果您从Tobject派生自己的类并实现IUnknown,而不实际计算引用,也不在实现Release时释放“self”,则您将得到一个支持接口但具有显式管理的Lifeti的基类。我是正常的。

    由于编译器自动生成了对addRef()和release()的调用,因此仍然需要小心处理这些接口引用,但这与小心处理常规tobject的“悬空引用”没有太大区别。

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

        4
  •  3
  •   Ritsaert Hornstra    15 年前

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

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

    2. 在类中使用虚拟或动态声明函数时,可以重写 子类。这是大多数类库使用的方法。看看tstream,它是派生类,如thandlestream、tfilestream等。

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

    3: -如果您从TinterfacedObject派生,引用计数实际上会考虑对象的生命周期,但这不是真实性。 -例如,tcomponent也实现了IInterface,但没有引用计数。这会带来一个大警告:在销毁对象之前,请确保您的接口引用设置为nil。Comiler仍将向您的接口插入递减调用,该调用看起来仍然有效,但不有效。第二:人们不会期望这种行为。

    在2和3之间选择有时是非常主观的。我倾向于使用以下方法:

    • 如果可能,使用虚拟和动态并重写派生类中的那些。
    • 使用接口时:创建一个接受对Intercae实例的引用作为变量的基类,并尽可能保持接口的简单性;对于每个方面,尝试创建一个单独的Intercae变量。当没有指定接口时,请尝试将默认实现放置到位。
    • 如果上面的限制太大:开始使用TinterfacedObject-S,并真正注意可能的周期,因此内存泄漏。
        5
  •  1
  •   skamradt    15 年前

    在我处理超大项目的经验中,这两个模型不仅工作得很好,甚至可以毫无问题地共存。与类继承相比,接口的优势在于,您可以将特定的接口添加到不从公共祖先继承的多个类中,或者至少在不将代码引入层次结构的情况下,可能会在已经证明有效的代码中引入新的错误。

        6
  •  1
  •   Warren P    15 年前

    我不喜欢使用COM接口,除非有人已经制作了一个。也许这是因为我对COM和类型库的不信任。我甚至把接口“伪造”成带有回调插件的类,而不是使用接口。我想知道是否有其他人感受到了我的痛苦,并且避免了像瘟疫一样使用界面?

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

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

    推荐文章