Interfaces versus Base Classes: Object-oriented Architecture Revisited

Interfaces versus Base Classes: Object-oriented Architecture Revisited

Whether you have been using an object-oriented language such as Java or Delphi for many years now, or are just getting started with a newer object-oriented paradigm such as .NET, you have encountered two important aspects of modern, object-oriented programming: base classes and software interfaces. Both of these mechanisms serve to define the application programming interface, or API, of the classes that you implement in your code.

And although software interfaces and base classes serve similar purposes, they are different in a number of important ways. Unfortunately, it’s not always clear which mechanism is best suited for defining your API. It is not uncommon for class designers to overuse either base classes or interfaces at the expense of the other.

Consider the following. When Microsoft released the .NET 2.0 framework, ADO.NET, one of the more significant APIs in the .NET framework, was updated to rely more on base classes, instead of the software interfaces upon which the earlier framework relied. While these changes had little effect on the ADO.NET users, it had a positive impact on those developers responsible for creating custom data access classes.

This article reviews the roles of software interfaces and base classes in object-oriented development. It begins with a look at these mechanisms, including the benefits and limitations of each. It continues with rough guidelines that you can use to choose how to best implement your APIs, along with some examples that demonstrate why.

Base Classes

All classes, with the exception of the top class in a particular object hierarchy, descend from a parent, or ancestor class. In the .NET framework, this top class in Object, while in the Delphi Visual Component Library (VCL) it is TObject. In other words, with the exception of one class in each class hierarchy, every class has an ancestor class. (In languages that support multiple inheritance, a class may descend from two or more ancestors. Multiple inheritance introduces additional problems, which is why most of the newer languages avoid this feature.)

Ancestor classes impart a number of benefits to the classes that descend from them. Foremost of these, from the standpoint of this article, is inheritance. Inheritance means that a descendant class obtains, or inherits, the methods, fields, and properties of its ancestors. As a result, all descendant classes have some basic capabilities, even before their new methods, properties, or fields are added. For example, all objects in the .NET framework own a toString() method inherited from Object, and all objects in the Delphi VCL have a ClassName method.

When a class descends from an ancestor further down in the object hierarchy, such as System.Web.UI.WebControls (.NET) or TComponent (VCL), the number of inherited features can be substantial.

Not all aspects of what a class inherits are the same. Specifically, those public methods and properties that a class inherits from its ancestor(s) are necessarily part of the class’s API. While the private members that a class inherits may play important roles in how a class operates, it is the public members that can be called at runtime by other classes.

The public members shared by a class and its ancestors are important in another, crucial aspect, as far as object-oriented architectures are concerned. Specifically, it is these shared public members, or shared API, that make a class and the classes that descend from it interchangeable, from the standpoint of polymorphism.

In Delphi, any object that can be placed onto a form at design time must be a TComponent, which is to say that it must descend from the TComponent class

Consider this: In Delphi, any object that can be placed onto a form at design time must be a TComponent, which is to say that it must descend from the TComponent class. (Indeed, you cannot even register a class with the Tool Palette unless the class descends from TComponent.) This is because the designer understands TComponents, and treats all instances of TComponent the same (there is a healthy dose of runtime type information (RTTI) involved here, but the designer sees the world as being composed of TComponents, and that’s pretty much it).

A base class, then, is one whose interface is designed to be shared by it and its descendants. And there are two primary purposes served by this. First, the shared API permits the two or more classes that descend from a base class to be treated interchangeably, as far as their API is concerned.

The second benefit of a base class is one that was discussed earlier, which is that behaviors introduced in a base class are automatically present in descendant classes. In some cases, those behaviors are substantial. Developers who write their own classes know that one of the keys to efficient class declaration is to inherit as many of the features that the new class requires as possible.

Not all base classes introduce significant behaviors, however. Early in the history of object-oriented development software designers understood the importance of polymorphism, and sometimes designed base classes that contained nothing more than abstract methods (such classes are referred to as “pure abstract classes”). Abstract methods are those that define an API, but are not implemented in the class where they are introduced. Instead, abstract methods serve to provide all descendant classes with a “template” API, one that they will share with other descendants of the same abstract base class. The implementation details, however, are left to the individual descendants. (And note, a concrete class, that is, a class that can be instantiated, must have implementations of all inherited abstract methods.)

Interfaces

Pure abstract classes, which are intended to lend a common API to two or more descendant classes, eventually lead language designers to create a mechanism for defining an API abstracted from any implementation details. These definitions are called “interfaces.”

An interface is a formal declaration of an API, entirely distinct from any class declaration. In most languages, interfaces consist of method declarations, and many also permit property declarations (though in some languages, a property is nothing more than a specific pattern of public get and set methods).

Interfaces are designed to be implemented by classes. When a class is declared to implement an interface, there is an explicit commitment (or contract) that the class will support the API defined by the interface. Importantly, all classes that implement the interface can be treated interchangeably, or polymorphically, with respect to the interface.

Delphi’s Code Completion feature of Code Insight (similar to Visual Studio’s Intellisense) understands this commitment, and displays unfulfilled interface methods in an implementing class’s declaration. This is shown in Figure 1, where Code Insight displays the as yet unimplemented method of the IUsageLogger interface in the TCustomDataSet class declaration. (By default, not yet implemented methods of an interface appear in Code Completion in red text, although this is not visible in the following figure since LogUsage is currently selected.)

Fig. 1: Delphi’s Code Insight provides Code Completion for interfaces that must be implemented

Interfaces, like classes, are declared in a hierarchy. Specifically, an interface can inherit from an existing interface, and most frameworks contain only one base interface.

While an interface is similar in some respects to a base class, there are three significant differences. First, an interface is not a class declaration, and cannot be used to create an instance of anything.

Second, while an interface can declare methods (and properties), it never defines method implementation. Classes that implement an interface inherit no behavior from the interface. In fact, a class that implements an interface is required to provide implementations of all methods declared in the interface (as well as any ancestor interfaces of the interface being implemented).

Third, and probably the most important difference, is that an interface is not bound in any way to the object hierarchy. In other words, an interface can be used to make two or more classes interchangeable, programmatically, without respect to their class ancestry. In other words, you can use interfaces to treat very different classes in a similar fashion. By comparison, without interfaces, classes can only be treated polymorphically to the extent to which they descend from a common ancestor from whom they inherit the API.

The Problem

The problem is relatively simple. Since you can define your API using either base classes or interfaces, which should you choose? Each has well known advantages (base classes introduce behaviors while interfaces are de-coupled from the class hierarchy), and each has limitations (base classes are class bound, and interfaces introduce no behavior). So, what’s the problem?

Since you can define your API using either base classes or interfaces, which should you choose?

The problem is that it’s not always obvious which is better. In some cases, either mechanism is suitable for defining your API. Furthermore, it is not uncommon for developers who are new to interfaces to overuse them. It’s nothing to be embarrassed about; I’ve made the mistake myself.

After seeing too many examples where a wrong choice was made, I am offering the following guidelines for selecting between base classes and interfaces. These guidelines, of course, are rather general, and do not apply in all situations. Therefore, as a general reminder, I acknowledge these offered opinions are just that, and the specifics of your particular API may not lend themselves to the distinctions that I am making here.

Choosing Between Base Classes and Interfaces

In most cases, choosing between a base class and an interface as the basis for your API can be determined by answering the following two questions:

  1. Are there predictable behaviors that deserve or need to be defined as part of your API?
  2. To what extend can or should your API be class-bound?

We will start by considering those instances where a base class better serves your purposes. Next we will consider those situations where an interface is typically optimal. Finally, we will take a look at instances where a combination of both a base class and an interface are best.

APIs and Inherited Behavior

Base classes are preferable when some or all of the basic behaviors of the API can be predicted, and inherited behavior is a possibility. While this might sound obvious, it is not always clear that inherited behavior is the way to go. Such was the case in ADO.NET 1.1.

Base classes are preferable when some or all of the basic behaviors of the API can be predicted, and inherited behavior is a possibility

With ADO.NET, Microsoft wanted to ensure that other database vendors could develop custom .NET data providers, assuring that the API was “open.” In hindsight, the fact that any first class .NET language can presumably descend from a class created by another .NET class made the primary reliance on interfaces unnecessary. Upon reconsideration, some of the behaviors of ADO.NET data providers could be predicted and inherited. As a result, ADO.NET 2.0 introduces many base classes that provide for some of the basic behaviors required by most all .NET data providers.

Along a similar theme, base classes are preferable when descendants can benefit from ancestor implementation. Specifically, while both base classes and interfaces define an API, if descending from an ancestor class relieves the descendants of having to implement basic behaviors, base classes are preferable to interfaces.

Consider the base class shown in Listing 1, TBaseObject, which is a real-world ancestor class of a hierarchy of data access objects used in ASP.NET applications. This base class introduces two public properties, Changed and ValidationMessage, and one public function, IsValid. This base class also implements two protected methods, BuildValidationMessage and DoValidate.

type
  TBaseObject = class(TObject)
  protected
    FValidationMessage: String;
    FIsValid: Boolean;
    FChanged: Boolean;
    procedure BuildValidationMessage(Value: String);
      virtual;
    procedure DoValidate(ValidateTest: Boolean;
      FailureMessage: String);
  published
  public
    function IsValid: Boolean; virtual;
    property ValidationMessage: String
      read FValidationMessage;
    property Changed: Boolean read FChanged;
  end;

While the details of the implementations of these methods is beyond this discussion of this article, the essential point is that any class that inherits from TBaseObject (and all classes in the previously mentioned ASP.NET data layer hierarchy do), inherits a collection of behaviors and capabilities that are consistent with validation of the data stored in the object in question. While an interface can define the API, without the TBaseObject ancestor, the actual behaviors represented by the API would need to be introduced by implementing classes.

APIs with No Inheritance

When the opposite is true, when no behaviors of the implementing class can be predicted, an interface is adequate. Since the API is all that is known, attempting to introduce the API with a base class unnecessarily binds the descendants to a portion of the class hierarchy, rather than letting the implementing class be dictated by the needs of the API.

Here is a real-world example in which an interface defines IAppDataManager.

type
  IAppDataManager = interface
    function get_ApplicationData: String;
    function get_Changed: Boolean;
    procedure Reset;
    procedure Save;
    property Changed: Boolean read get_Changed;
    property ApplicationData: String read GetAppData;
  end;

This interface defines a coherent collection of features (associated with retrieving, saving, and detecting changes to application data), but in this case, there is no behavior to inherit. Specifically, some implementations return ApplicationData by reading from a database, others read the ApplicationData from XML files, while others use socket connections.

Importantly, there is one thing, and one thing only, shared by these implementations: the API. It is the only feature that makes the implementing classes compatible.

In this framework, there are a group of classes referred to as application sessions, and they need to read ApplicationData. Each of these classes have a property of the type IAppDataManager, and this property can be assigned any class that implements this interface, after which, the application session can read ApplicationData. An application session doesn’t care how the implementing class reads ApplicationData, nor is it interested in the implementing class’s ancestor. They just need the data.

APIs and Cross-Platform Development

Unlike the preceding example, where an interface is preferable, when the source language simply cannot inherit the API from a descendant class, an interface is the only mechanism for defining the API. Two examples of this that come to mind immediately are CORBA (the common object request broker architecture) and COM (the component object model).

One feature that is common to both CORBA and COM is that they are language independent

One feature that is common to both CORBA and COM is that they are language independent. And not just in the .NET way. CORBA and COM can be implemented by languages that have no knowledge of each other’s object model, making inheritance impossible (or maybe unreasonable). Since inheritance is not an option, the introduction of behavior is out.

Nonetheless, the use of an interface to define the API is sufficient to provide for polymorphism. For example, any class that implements the IDispatch interface can be used as an automation server, at least within the Windows COM framework.

APIs Defined Using Both Interfaces and Base Classes

As noted earlier, interfaces and base classes are not mutually exclusive. Your API may be defined by both interfaces and base classes.

There are several instances where this might be the case. First, there are situations where an interface is required. For example, there may be a method that returns a value of an interface type. This means that any object that can be returned by this method must implement the specified interface or one that descends from the specified interface.

The interesting thing about this example is that the methods and properties used by the code that calls the method may have been introduced in an ancestor object, one that was not specifically declared to implement the interface.

In a situation like this one, the class may inherit the core behaviors, but at the same time implement the interface required by the method that returns the object. These features might be coincidental or not, but that is not the point. The implementing class can be treated polymorphically both with respect to its class as well as its interface. So long as the class’s design is not forced, you get the best of both worlds.

A second situation is when a class inherits both behaviors and interface implementations, but the interface is of no use to you. For example, most Delphi developers are unaware that the TComponent class implements the IInterface (equivalent to IUnknown) interface, and probably do not care anyway.

In these cases, you use the class because its methods are those that you need, or you need to treat the class interchangeably with one of its ancestors. The interface is irrelevant, but there is nothing gained by decoupling the interface from the class, which is to mean that you are better off living with the interface even though you don’t need it. (And, if you need the polymorphism introduced by the ancestor class, such as TComponent, you really don’t have a choice anyway.)

Summary

Both base classes and interfaces permit you to treat different classes polymorphically, which is a powerful and essential capability in object-oriented languages. They do this through different means, however, and with different benefits and limitations. By considering these differences you can better choose which to use when defining your application programming interfaces.

Geef feedback:

CAPTCHA image
Vul de bovenstaande code hieronder in
Verzend Commentaar