Interface It!

By: Jimmy Tharpe

Abstract: A quick guide to the ins and outs of interfaces in Delphi. By Jimmy Tharpe.

Programming is an art, and abstraction is one of the great methods of artistic creation.

An interface defines an abstraction - the essential qualities that an object must have. When an object implements an interface, it must implement the entire interface. Variables are not defined as part of the interface -- and that is just as  it should be. Part of being abstract is that you can define what is required but not how it is required. The implementation of an interface is none of the interface's business. Variables are containers, not abstractions.

Imagine yourself in a field of flowers. What is a flower? A flower is an abstract concept; if an object meets the requirements of "flower" it is a flower even if you call it a rose. "Rose" simply refers to a specific implementation of "flower." Defining an interface is like describing a flower -- it describes what must be, the essential characteristics that define flowerness. You do not define the stem, you say that there must be a stem.

One Destination, Many Paths

That's what an interface is. Now let's talk about what they're good for.

Because interfaces define essential qualities but not implementation, they are used to arrive at one destination that may have many paths. It is as simple as that. There is only one definition of "flower" but there are many types of flowers.

Of course, we are programmers, not botanists.

Let's use a real-world example: versioning. Wouldn't it be useful to have a common interface for versioning? This interface could be used to access Windows version information, component versions...we could even implement it as a Web Service used for checking if updates exist. All your program would need to know is the interface -- none of the implementation. Now wouldn't that be nice?

Using Interfaces

Interfaces are defined much like classes, however the keyword "interface" is used instead of "class." Another difference is that all declarations have the same visibility, so the keywords private, protected, public, and published aren't allowed when declaring an interface.

An interface may include definitions for functions, procedures, and properties. (It's easy to think of Interfaces as just a collection of related methods.) When defining a property, you must specify read and write methods: Remember, interfaces cannot have variables.

Let's define an IVersionInfo interface:

type
  IVersionInfo = interface(IInterface)
  ['{D4BB99BE-3CC6-4C8B-A883-AE9ADE837F51}']
    function GetMajorVersion: integer;
    procedure SetMajorVersion(Value: integer);
    property MajorVersion: integer
      read GetMajorVersion write SetMajorVersion;

    function GetMinorVersion: integer;
    procedure SetMinorVersion(Value: integer);
    property MinorVersion: integer
      read GetMinorVersion write SetMinorVersion;

    function GetRelease: integer;
    procedure SetRelease(Value: integer);
    property Release: integer
      read GetRelease write SetRelease;

    function GetBuild: integer;
    procedure SetBuild(Value: integer);
    property Build: integer
      read GetBuild write SetBuild;
  end;

You may want to expand this interface later to include things such as a Company Name, File Description, and so on. For now, let's keep it simple. There is already plenty to discuss.

Notice that we have inherited from IInterface. This is not necessary; I did it to illustrate the difference between IInterface and IUnknown. IInterface is like TObject -- it is the base all descendants descend from. IUnknown and IInterface are really the same thing (IUnknown = IInterface;), but by using IUnknown you are implying that your interface has something to do with Microsoft's COM technology. On the other hand, working with IInterface makes no such implication. IInterface is a regular, cross-platform interface.

IInterface is new in Delphi 6. In Delphi 5 you must use IUnknown. 

The next thing to notice is this line:

['{D4BB99BE-3CC6-4C8B-A883-AE9ADE837F51}']

That line defines the interfaces GUID (Globally Unique Identifier). To generate a GUID, just press Ctrl+Shift+G in the Delphi IDE. GUIDs are used to identify an interface and they must be used when using Supports(), "as", QueryInterface(), or GetInterface() methods. (See Delphi/Kylix help for more detail.) Though GUIDs are not always required, it is best to always include them. It's not like you can use them up.

Finally, take a look at the definition if IInterface:

IInterface = interface
  ['{00000000-0000-0000-C000-000000000046}']
  function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
  function _AddRef: Integer; stdcall;
  function _Release: Integer; stdcall;
end;

Notice that it contains three functions: QueryInterface, _AddRef, and _Release. According to the Delphi Help, "QueryInterface checks whether the object (or record) that implements this interface supports the interface specified by IID." If QueryInterface is successful, it sets the Obj parameter to point to an instance of the given interface, calls the _AddRef method of the interface, and returns zero. Otherwise the result is non-zero (for example, E_NoInterface).

_AddRef and _Release are used for reference counting. As long as a reference to an interface is kept, the object is kept in memory. Thus they are used to manage an object's lifetime. Assuming that reference counting is implemented (it doesn't have to be), if the reference count drops to zero, the object is freed. _AddRef is used to add a reference, and _Release is used to subtract a reference.

Implementing Interfaces

At this point, you should know what interfaces are, what they are used for, and a little bit about reference counting. Now it is time for implementation. Let's continue with IVersionInfo and create a descendent of TComponent called TVersionComponent that implements IVersionInfo.

To implement an interface, you simply add it to the class() statement. Here is the initial code:

type
  TVersionComponent = class(TComponent, {for D5: IUnknown,} IVersionInfo)
  end;

Try to compile this. What happens? It doesn't compile!

That is because the methods of IVersionInfo have not been added. If we add all the Get and Set methods to the protected section of the component and flesh them out, it will compile. Notice that we do not have to declare the properties; that has been done for us by the interface.

To create a component go to File | New | Other | Component and follow the dialog.

Here are the required methods:

type
  TVersionComponent = class(TComponent, IVersionInfo)
  private
    FMajorVersion,
    FMinorVersion,
    FRelease,
    FBuild: integer;
  protected
    {IVersionInfo}
    function GetMajorVersion: integer;
    procedure SetMajorVersion(Value: integer);
    function GetMinorVersion: integer;
    procedure SetMinorVersion(Value: integer);
    function GetRelease: integer;
    procedure SetRelease(Value: integer);
    function GetBuild: integer;
    procedure SetBuild(Value: integer);
  end;

In this case I am using private variables to store and retrieve the version information. It doesn't have to be this way. We can implement the IVersionInfo methods any way we want. We could make API calls, Web Service requests, whatever. The implementation is up to us.

Delphi 6 introduces support for interfaces in TComponent by implementing IInterface. If you are using Delphi 5, you must add additional support for IInterface by implementing the QueryInterface, _AddRef, and _Release functions. Just return -1 in _AddRef and _Release. For QueryInterface, insert the following code:

if GetInterface(IID, Obj) then
  Result := S_OK else
  Result := E_NOINTERFACE

If you flesh out the methods introduced so far, then install the component, you may wonder where the properties declared in IVersionInfo went to. The answer is that they are a part of the IVersionInfo interface, not a part of the component. To use these properties, you can declare them in the component class declaration, or you can use the "as" keyword to type-cast the component. Here's an example of type-casting with interfaces:

procedure TForm1.FormCreate(Sender: TObject);
  var
    VI: IVersionInfo;
begin
  VI := (VersionComponent1 as IVersionInfo);
  Caption := IntToStr(VI.MajorVersion) + '.' +
             IntToStr(VI.MinorVersion) + '.' +
             IntToStr(VI.Build);
end;

Of course, we could replace each instance of the VI object with "(VersionComponent1 as IVersionInfo)," but that wouldn't be as pretty.

One final note about reference counting. Take a look at this code:

var
  VI: IVersionInfo;
begin
  VI := TVersionComponent.Create(Self);
  Caption := IntToStr(VI.MajorVersion) + '.' +
             IntToStr(VI.MinorVersion) + '.' +
             IntToStr(VI.Build);
end;

TComponent (in Delphi 6) does not implement reference counting, so this code will cause a memory leak. However, had we implemented reference counting (or if we had descended from TInterfacedObject which implements reference counting for us), this code would not cause a memory leak. 

This is where reference counting and Delphi compiler magic come in to play. When you assign VI to a new instance of a TVersionComponent, Delphi (invisibly) inserts a call to _AddRef (normally incrementing the reference count by one). When the VI object goes out of scope, Delphi inserts a call to _Release ( the reference count gets decremented to zero). If reference counting is implemented, when the reference count gets set to zero, the object is freed.

Go Forth

Now go forth and create your programs. Use and understand interfaces, for I shall return and show you how to implement IVersionInfo to take advantage of the Windows API, Web Services, and more!

Jimmy would like to thank Alessandro Federici and Azret "da man" Botash for their vast knowledge and insight into the world of interfaces.


Server Response from: ETNASC03