开发者

Abstract vs. Interface - separating definition and implemention in Delphi

开发者 https://www.devze.com 2022-12-20 21:28 出处:网络
What is the better approach for separating definition and implementation, using interfaces or abstract clas开发者_JS百科ses?

What is the better approach for separating definition and implementation, using interfaces or abstract clas开发者_JS百科ses?

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?


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!


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.


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.


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.


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.


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消