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.