Ted Pattison
Microsoft Corporation
January 1999
Note This article has been adapted from the Microsoft Press book by Ted Pattison titled Programming Distributed Applications with COM & Microsoft Visual Basic (ISBN# 1-57231-961-5).
Summary: Provides Microsoft® Visual Basic® programmers with an understanding of interface-based programming. (19 printed pages) Covers:
Introduction
Classes, Objects, and Clients
Implementation Inheritance
What Is Polymorphism?
Problems Associated with Implementation Inheritance
Using Interfaces with Visual Basic
Why Use Interfaces?
Designing Applications with User-defined Interfaces
Summary
Getting a grasp of interface-based programming is really tough. To gain an appreciation for this style of programming, you must leave behind old habits and intuitions about writing code and look back at the evolution of object-oriented programming (OOP) and computer science over the past decade—a Darwinian saga of how the interface has made modern software designs more fit for survival. For software to survive in the ever-changing jungle of the production environment, it must have three distinct characteristics: reusability, maintainability, and extensibility. This article will provide a general overview of interface-based programming and examine these characteristics.
The Component Object Model (COM) is founded on the idea of interface-based programming; COM would have no meaning without the concept of the interface. If you are a Visual Basic programmer who is trying to understand how COM really works, you should spend the time to learn how and why interfaces are so valuable in object-oriented software design. An understanding of interface-based programming will make you a much stronger COM programmer. This, in turn, will prepare you for creating COM-based applications for distributed environments such as Microsoft Transaction Server (MTS).
Interface-based programming exists outside the world of COM. It is a programming discipline that is based on the separation of the public interface from implementation. It was pioneered in languages such as C++ and Smalltalk by software engineers who discovered that using distinct interfaces could make their software, especially large applications, easier to maintain and extend. The creators of Java saw the elegance of interface-based programming and consequently built support for it directly into their language.
Interfaces solve many problems associated with code reuse in object-oriented programming. This document will discuss what some of these problems are. In particular, when you program in a style consistent with classic OOP, a client can build inflexible dependencies on a class definition. These dependencies can make it difficult to maintain or extend the class without breaking the client. It becomes tedious or impossible to improve the code for an object over time. Certain problems are also associated with a popular OOP language feature known as implementation inheritance. This powerful but often misused feature is vulnerable to similar dependency problems, which compromise an application's maintainability and extensibility. Even though Visual Basic does not support implementation inheritance, this document will discuss its strengths and limitations in order to address some of the problems that interface-base programming was created to solve.
Visual Basic version 5.0 added support for defining and implementing user-defined interfaces. This article will show you how to use interfaces in a Visual Basic application. After discussing the basics of using interfaces, it will demonstrate how to achieve polymorphism and run-time type inspection, which make interface-based programming so powerful.
Your first stop on the road to interface awareness must be an examination of the problems that interface-based programming is meant to solve. Many of these problems have to do with the relationship between a class and the clients that use it. Think about the following questions: What is the relationship between a client and a class definition? What must a client know about a class in order to benefit from using it? What dependencies are created in the client when a programmer writes code using a class's methods and properties?
In an object-oriented paradigm, a client typically instantiates an object from a class. It usually creates an object by using the New operator followed by the class name. After creating an object, the client uses it by accessing an exposed set of properties and methods through a variable that is a class-based reference. Here is a simple example using a variable based on a class type to access an object's public members:
Dim Dog As CDog
Set Dog = New CDog
' access a property
Dog.Name = "Snoopy"
' invoke a method
Dog.Bark
In this example, a class-based reference makes it possible to instantiate and communicate with a dog object. The communication between the client and the object takes place through a set of publicly accessible properties and methods that are known as an object's public interface. The class author must use the public interface to expose the object's functionality to the client. This is what makes an object useful. Note that the method names and property names from the public interface are hard-coded into the client. Future versions of the class must continue to supply the same members in order to honor these dependencies built into the client.
One benefit of using classes is that they allow you to reuse code. Once a class has been written, you can use it in many different places in an application. Classes thus let you reduce or eliminate redundant code in an application. They also facilitate code maintenance. You can modify or remove any properties or methods that are not publicly visible. You can also change public method implementations as long as the calling syntax of the methods is not altered. When the implementation of a method in a class is improved, any client that uses the class will seamlessly benefit from the changes.
When you modify an existing class definition, you should not change the calling syntax for accessing any public method or property because of the risk of breaking the dependencies that client code has built on the original class definition. As long as you hold the public interface constant, you can make any modifications to improve your class without breaking any client code.
To rephrase the key point of the last paragraph: Once you publish a property or a method signature in a class's public interface, you cannot change or remove it. This means that you must properly design the public interface at the beginning of a project and use discipline as the class evolves. This lets you improve and extend object code without having to rewrite any client code. You can maintain a class by fixing bugs and improving method implementations.
The rules for maintaining existing members of the public interface are cut and dried, but what flexibility do you have when you add new functionality to a class? What can you do to safely extend an object in later versions? It's easy and safe to add new public methods and properties to a class. Old clients continue to run as before even though they cannot take advantage of the object's new functionality. However, new clients written after the class has been modified can take advantage of any members added to the public interface. This means you can improve an object safely over time in the production environment.
Problems arise in class design when you change the signature of a public method in a way that breaks an existing client. This commonly happens when you discover that the initial class design was inadequate. For instance, imagine a method that provides the behavior for a dog rolling over. The following RollOver() method is defined with a 16-bit integer parameter to allow the client to request a specific number of rolls in each invocation:
' method defined in CDog class
Public Sub RollOver(Rolls As Integer)
' implementation
End Sub
' client hard-codes calling syntax
Dim Dog As CDog, Rolls As Integer
Set Dog = New CDog
Rolls = 20000
Dog.RollOver Rolls
What if the requirements of a dog object change were not anticipated properly in the initial design? For instance, what if the required number of rolls exceeds the highest possible value for an integer (about 32 KB)? What if a client wants to invoke the method with a value of 50,000? To accommodate a larger value, you must change the parameter type to a long integer. This creates quite a design problem. The newer clients want a 32-bit integer, but older clients, such as the one just shown, already have a dependency on the 16-bit integer.
You have only two options. One is to modify the method signature and then rewrite all the client code that calls it. The other is to leave things as they are and deal with the limitations of the original design. As you can see, poor class design results in either broken clients or nonextensible objects.
The intuitive solution to this problem is to make sure the design of the class's public interface is full-featured and finalized before you write client code against it. But this is not always possible, even for the most experienced class designer. If a class models a real-world entity that never changes, an experienced designer can create a robust, long-lasting design. However, in many cases it's impossible to predict how external changes will affect the requirements for an object's public interface. A designer who is creating classes for an application that is expected to run for years in a rapidly changing business environment cannot possibly predict what is needed. If the business model is constantly changing, your classes must change with it. Therein lies the need for extensible objects.
Software designers have found that the use of class-based references results in a layer of dependencies between clients and classes. You can lessen the impact of these dependencies on maintainability and extensibility through disciplined design and by anticipating future requirements. Don't define a method or property in the public interface unless you are prepared to live with it forever. Most experienced designers make all data properties private and provide access to an object's state through public methods. This prevents any client dependencies to the actual data layout of the class. Be conscientious about which methods you mark as public. Any member you mark as private can be changed or removed as the class implementation evolves. Of course, you have to make some members public or the class will be useless. Designing with class-based references always involves these tradeoffs.
Many of the features of OOP are meant to give programmers higher levels of code reuse. Languages such as C++, Smalltalk, and Java offer a popular feature known as implementation inheritance, which offers one of many possible ways to achieve code reuse in an object-oriented paradigm. Some people argue that a language must offer implementation inheritance to be considered a real object-oriented language. This has led to a heated debate in both the software industry and the academic community—a debate that this article will not address. Instead, we will focus on the benefits and problems associated with this powerful feature.
In implementation inheritance, one class is defined to reuse the code of another class. The class that is reused is called the super class. The class that benefits from the reuse is the sub class. Visual Basic does not presently support implementation inheritance, so I will use a Java example to illustrate what implementation inheritance looks like. Examine the following Java class CDog:
// super class
class CDog
{
// DOG STATE
public String Name;
// DOG BEHAVIOR
public void Bark()
{/* method implementation */}
public void RollOver(int Rolls)
{/* method implementation */}
}
The class CDog contains a property and two methods. Assume each method has been defined with a valuable implementation. You can reuse the state and behavior of the class by using implementation inheritance. CDog will be used as a super class. A sub class that extends CDog will inherit both the class properties and method implementations. The following Java code shows the syntax required to achieve implementation inheritance:
// sub class
class CBeagle extends CDog
{
// BEAGLE STATE
// Name property is inherited
// A color property is added
Public String Color;
// BEAGLE BEHAVIOR
// implementation of RollOver() is inherited
// implementation of Bark() is overridden
public void Bark()
{/* CBeagle-specific implementation */}
// CBeagle extends CDog by adding a new method
public void FetchSlippers()
{/* CBeagle-specific implementation */}
}
When CBeagle (the sub class) extends CDog (the super class), it inherits all of the existing properties and method implementations. This means that CBeagle can reuse all of the state and behavior defined in CDog. You can then extend CDog by overriding existing methods, such as Bark(), and adding additional methods, such as FetchSlippers(), in CBeagle. You can also add new properties to the sub class definition.
Figure 1. Implementation inheritance allows one class to reuse the state and behavior of another. The sub class inherits the properties and method implementations of the super class and extends it by overriding methods and adding additional properties and methods.
You should use implementation inheritance only when a logical "IS A" relationship exists between the sub class and the super class. In this example, you can say "a beagle is a dog," as shown in Figure 1. As long as the "IS A" requirement is met, implementation inheritance is useful for achieving code reuse. Implementation inheritance can be especially valuable when an application contains many classes that must exhibit a common behavior. The commonality of several classes can be hoisted to a super class. For example, once the CDog class has been written it can be extended by CBeagle, CTerrier, CBoxer, and any other class that "IS A" dog. Code written to define state and behavior in the CDog class can be reused in many other classes.
Figure 2 shows a graphic representation of what is known as an inheritance hierarchy. The hierarchy shows the relationships between the various classes in the application. This hierarchy is simple; you can create others that are far more complex. Imagine a hierarchy in which CScottie extends Cterrier, which extends Cdog, which extends Cmammal, which extends CAnimal. As you can imagine, inheritance hierarchies can become large and complex. Hierarchies containing five or more levels are not uncommon in production code.
Figure 2. An inheritance hierarchy shows the relationships between the super classes and sub classes in an application.
When implementation inheritance is used correctly, it can also be a powerful mechanism for code maintenance. When you improve the implementation of a method in a super class, all the classes down the inheritance hierarchy automatically benefit from the changes. A bug fix to the CAnimal class can potentially improve hundreds of other classes. As the inheritance hierarchy becomes larger and more complex, modifications to classes at the top can have a significant impact on many classes below. This implies that a single modification can affect the behavior of many distinct object types.
So far, this article has explained how implementation inheritance offers the implicit reuse of method implementations, which results in greater maintainability through the elimination of duplicate code. There is another powerful OOP feature provided by implementation inheritance known as polymorphism. This is arguably the most important concept in object-oriented programming. Polymorphism allows a client to treat different objects in the same way even if they were created from different classes and exhibit different behaviors.
You can use implementation inheritance to achieve polymorphism in languages such as C++ and Java. For instance, you can use a super class reference to connect to and invoke methods on sub class instances. Figure 3 shows how a client can use a CDog reference to communicate with three different types of objects. Each sub class that derives from CDog is type-compatible with a CDog reference. Therefore, a client can use a CDog reference when communicating objects of type CBeagle, CRetriever, or CBoxer.
Figure 3. You can achieve polymorphism by using a super class reference to communicate with sub class instances. A client can use a CDog reference to communicate with any CDog-compatible object.
A client can be sure that any class that extends the CDog class provides an implementation of the Bark() method. The client doesn't care if the sub class uses the definition of Bark() that was supplied by CDog or if the sub class has overridden this method with its own implementation. The client simply invokes the method using the calling syntax defined in the CDog class. However, if the sub classes each supply their own implementation of Bark(), each object type can respond in its own unique way to the same request. Examine the following Java code:
// method accepts any CDog-compatible object
Public void MakeDogBark(CDog Dog)
{
// different objects can respond differently
Dog.Bark()
}
If this method is invoked using a CBeagle object, it might have very different results than if it is invoked using a CTerrier object. The client code knows which method to call, but it has no idea how the Bark() method will be carried out. The calling syntax is well defined at compile time, but the actual method implementation is not determined until run time. Polymorphism is based on the idea of dynamic binding of implementation code, as opposed to static binding. Dynamic binding provides a degree of controlled uncertainty that makes polymorphism so powerful. You can create applications based on plug-compatible objects. If thousands of lines of client code have been written to the public interface of the CDog class, you can easily replace a CBeagle object with a CTerrier object or CBoxer object. Such a change has little or no impact on client code, because client code has dependencies on the CDog class but not on any of the classes that extend it.
So far, this article has discussed the two biggest benefits of implementation inheritance: the implicit reuse of method implementations and polymorphism. It has not yet discussed some of the potential problems with implementation inheritance. Unfortunately, implementation inheritance makes an application more susceptible to the same type of dependency problems associated with class-based references because of the tight coupling between a sub class and its super class.
With the proper use of encapsulation, you can hide implementation details from clients. This allows you to freely change the implementation details of the class without breaking client code. The problem with implementation inheritance is that it breaks the encapsulation of nonpublic members. Languages that offer implementation inheritance provide a protected level of visibility in addition to public and private. Properties and methods that are marked as protected are hidden from a client but are accessible from sub classes. Sub classes therefore have access to implementation details that have been hidden from the client. As you hard-code the names of protected properties and methods of a super class into a sub class, another layer of inflexible dependencies is created.
Implementation inheritance is an example of a development style known as white-box reuse. Applications that are built on white-box reuse often experience tight coupling between the classes in the inheritance hierarchy. Once a sub class uses a protected property or method, you cannot change the super class's signature or remove it without breaking dependencies built into sub classes. This leads to fragility in applications with large inheritance hierarchies. Changing the classes at the top of the hierarchy often requires modifications to many sub classes. In some applications, changing a method signature or property type at the top of the hierarchy can result in breaking tens or hundreds of classes down the inheritance chain. On the other hand, freezing the public and protected interfaces of key super classes usually results in a system that is cannot evolve.
As in the case of simple class design, you must carefully consider whether to give a property or a method protected visibility. Proper design using implementation inheritance requires a high level of expertise and discipline to prevent what is known as the fragile super class scenario. You should know whether a class will be extended by other sub classes. If you expect a class to be extended, it is as important to encapsulate implementation details from sub classes as it is to encapsulate them from clients.
This is not to suggest that implementation inheritance is not useful. It is powerful in appropriate development scenarios. It is best used in smaller, controlled situations. Creating a large inheritance hierarchy that can evolve along with the requirements of an application is beyond the reach of all but the most experienced object-oriented designers.
When C++ and Smalltalk were first introduced, the OOP evangelists oversold implementation inheritance as a cure-all technique to achieve code reuse. As a result, this feature has been abused by designers who have not understood the coupling problems that come along with white-box reuse. Over the past decade, the casual usage of implementation inheritance has crippled the evolution of many large systems. Experienced developers who knew that implementation inheritance was most appropriate in small doses continued to look for more flexible ways to achieve reuse on a large scale. In particular, they looked for ways to achieve reuse without compromising extensibility in larger systems. They need an object-oriented reuse mechanism that could scale to accommodate larger, evolving designs. This fueled the birth of interface-based programming and a development style known as object composition.
Object composition offers another way to achieve reuse without the tendency toward tight coupling. Object composition is based on black-box reuse, in which implementation details of a class are never revealed to the client. Clients know only about an available set of requests (the what). Objects never expose internal details of the response (the how).
Black-box reuse is based on formal separation of interface and implementation. This means the interface becomes a first-class citizen. An interface is an independent data type that is defined on its own. This is an evolution of classic OOP, in which a public interface is defined within the context of a class definition.
At this point, you are probably thinking that this is all pretty vague. You're asking yourself, "What exactly is an interface?" Unfortunately, it's hard to provide a concise definition that conveys the key concepts of an entirely new way to write software. An interface can be described in many ways. You can get up to speed pretty quickly on the syntax for defining, implementing, and using interfaces. However, the implications that interfaces have on software design are much harder for the average programmer to embrace. Learning how to design with interfaces usually takes months or years.
At its most basic level, an interface is a set of public method signatures. It defines the calling syntax for a set of logically related client requests. However, while an interface defines method signatures, it cannot include any implementation or data properties. By providing a layer of indirection, an interface decouples a class from the clients that use it. This means an interface must be implemented by one or more classes in order to be useful. Once an interface has been implemented by a class, a client can create an object from the class and communicate with it through an interface reference.
You can use an interface to create an object reference but not the object itself. This makes sense because an object requires data properties and method implementations that cannot be supplied by an interface. Because it is not a creatable entity, an interface is an abstract data type. Objects can be instantiated only from creatable classes known as a concrete data types.
From a design standpoint, an interface is a contract. A class that implements an interface guarantees the objects it serves up will support a certain type of behavior. More specifically, a class must supply an implementation for each method defined by the interface. When communicating with an object through an interface reference, a client can be sure the object will supply a reasonable response to each method defined in the interface.
More than one class can implement the same interface. An interface defines the exact calling syntax and the loose semantics for each method. These loose semantics give each class author some freedom in determining the appropriate object behavior for each method. For instance, if the IDog interface defines a method named Bark(), different class authors can supply different responses to the same request, as long as each somehow reinforces the concept of a dog barking. The CBeagle class can implement Bark() in a different way than either CTerrier or CBoxer. This means that interfaces provide the opportunity for polymorphism. Interfaces are like implementation inheritance in the sense that they let you build applications composed of plug-compatible objects. However, interfaces provide plug-compatibility without the risk of the tight coupling that can occur with implementation inheritance and white-box reuse.
Inheritance is an objected-oriented concept that models an "IS A" relationship between two entities. So far, this article has used the term implementation inheritance instead of the more generic term inheritance because extending a super class with a sub class is only one way to leverage an "IS A" relationship. When a class implements an interface, it also takes advantage of an "IS A" relationship. For instance, if a class CBeagle implements the interface IDog, it is correct to say that a beagle "IS A" dog. You can use a CBeagle object in any situation in which an IDog-compatible object is required.
Interface-based programming is founded on a second form of inheritance known as interface inheritance. This means that inheritance does not require the reuse of method implementations. Instead, the only true requirement for inheritance is that a sub class instance be compatible with the base type that is being inherited. The base type that is inherited can be either a class or a user-defined interface. In either situation, you can use the base-type references to communicate with objects of many different types. This allows both forms of inheritance to achieve polymorphism.
Both forms of inheritance offer polymorphism, yet they differ greatly when it comes to their use of encapsulation. Implementation inheritance is based on white-box reuse. It allows a sub class to know intimate details of the classes it extends. This allows a sub class to experience implicit reuse of a super class's method implementation and data properties. Implementation inheritance is far more powerful than interface inheritance in terms of reusing state and behavior. However, this reuse comes with a cost. The loss of encapsulation in white-box reuse limits its scalability in large designs.
As the term black-box reuse suggests, interface inheritance enforces the concepts of encapsulation. Strict adherence to the encapsulation of implementation details within classes allows for more scalable application designs. Interface-based programming solves many of the problems associated with white-box reuse. However, to appreciate this style of programming, you must accept the fact that the benefits are greater than the costs. This is a struggle for many programmers.
When a class implements an interface, it takes on the obligation to provide set methods. Sub class authors must write additional code whenever they decide to implement an interface. When you compare this to implementation inheritance, it seems like much more work. When you inherit from a class most of your work is already done, but when you inherit from an interface your work has just begun. At first glance, implementation inheritance looks and smells like a cheeseburger, while interface inheritance looks like a bowl of steamed broccoli. You have to get beyond the desire to have the cheeseburger to reach a higher level of interface awareness. The key advantage of interface inheritance over implementation inheritance is that it is not vulnerable to the tight coupling that compromises the extensibility of an application.
Visual Basic 5.0 was the first version of the product to support user-defined interfaces. You can achieve the benefits of interface-based programming with a Visual Basic project by following these three required steps:
As you can see, the basic steps for adding interfaces to your applications are pretty easy. Using interfaces also lets you add polymorphism to your application designs. We will use a simple example to demonstrate the Visual Basic syntax required to complete these steps.
You define a custom interface in Visual Basic by using a regular class module. It would be better if the Visual Basic integrated development environment (IDE) were to provide a separate editor for defining interfaces, but unfortunately an editor dedicated to creating interfaces is not currently available. You use the class module editor to create both interface definitions and classes.
To define a new interface, you simply add a new class module to an existing project. Then you give it an appropriate name. If you are creating an interface to express the behavior of a dog, a suitable name could be IDog or itfDog. These are the two most common naming conventions among Visual Basic developers. If you are working in a Visual Basic project that is either an ActiveX® DLL or an ActiveX EXE, you should also set the class module's instancing property to PublicNotCreatable. This setting makes sense because the interface will represent an abstract data type. In a Standard EXE project, class modules don't have an instancing property.
You define your interface by creating the calling syntax for a set of public methods. Don't include an implementation for any of the methods in your interface. You need only define the signatures, nothing more. In essence, you define how the client calls these methods, not what will happen. Here's an example of the IDog interface defined in a Visual Basic class module:
' (class module IDog.cls)
' IDog expresses behavior of a dog object
Public Property Get Name() As String
End Property
Public Property Let Name(ByVal Value As String)
End Property
Public Sub Bark()
End Sub
Public Sub RollOver(ByVal Rolls As Integer)
End Sub
One of the first things you notice when declaring an interface in Visual Basic is the presence of End Sub, End Function, or End Property after each method signature. This doesn't really makes sense in an interface definition. The keyword End usually signifies the end of a method implementation. This is an idiosyncrasy of the Visual Basic IDE and an unfortunate side effect of using the Visual Basic class module for defining both classes and interfaces. Perhaps a future version of Visual Basic will provide a module type dedicated to defining interfaces that will not require End Sub, End Function, or End Property, but for now you just have to grin and bear it.
Another important point is that this interface can use logical properties in addition to methods. This is reasonable when you consider that a logical property is actually a set of methods, not a data property. The client can use the logical property Name, defined in the preceding interface, just like a regular data property, but it must be implemented in terms of a Property Let/Property Get method pair.
Stop and think about this: Why can't an interface contain data members? Because an interface, unlike a class, is never used to create objects. Its mission is to encapsulate a class's implementation details. The data layout of an object is among the most important details to encapsulate within a class definition. If an interface were to contain actual data members, the client would build dependencies on them. You know by this point that dependencies are bad.
Even though interfaces cannot contain data properties, Visual Basic still lets you define a data property in the class module you are using to define an interface, like this:
Public Name As String
However, when you define a data property in a class module, Visual Basic transparently redefines the data property as a logical property when you use it as an interface definition. This is simply a convenience that Visual Basic provides when you create interfaces. The Name property just defined still requires Property Let and Property Get in any class that implements the interface. It is also important to note that implementing an interface has no effect on the data layout for a class definition. Any class that implements this interface should include a private data property for the physical storage of the dog's name.
After you create the interface definition, the next step is to create a concrete class that implements it. Add a second class module to your project and give it an appropriate name. For instance, you can create a concrete class CBeagle that implements the IDog interface. You must use the keyword Implements at the top of a class module. This is what the statement looks like:
Implements Idog
Once a class module contains this line, every method and logical property in the interface must have an associated implementation in the class module. This requirement will be checked by Visual Basic's compiler. You cannot compile your code without supplying every implementation. For instance, implementing the Bark() method in the IDog interface requires this definition:
Private Sub IDog_Bark()
' implementation
End Sub
Visual Basic's mapping of interfaces requires that each method implementation use the name of the interface followed by an underscore and the method name. Visual Basic uses this proprietary syntax to create an entry point into an object when a particular interface is used. The Visual Basic compiler requires you to supply a similar implementation for each method and logical property in the interface. This guarantees that objects created from the class provide an entry point for each interface member.
Fortunately, the Visual Basic IDE makes it easy to create the procedure stubs for the method implementations if you use the keyword Implements at the top of the class module. The class module's editor window has a wizard bar that includes two drop-down combo boxes. If you select the name of the interface in the left-hand combo box, you can quickly generate the skeletons for the method implementations by selecting the method names in the right-hand combo box, as shown in Figure 4.
Figure 4. The wizard bar makes it easy to create the procedure skeletons when implementing a user-defined interface.
Implements IDog
Private Name As String
Private Property Let IDog_Name(ByVal Value As String)
Name = Value
End Property
Private Property Get IDog_Name() As String
IDog_Name = Name
End Property
Private Sub IDog_Bark()
' implementation
End Sub
Private Sub IDog_RollOver(ByVal Rolls As Integer)
' implementation
End Sub
This code sample shows a partial implementation of the CBeagle class that implements the IDog interface. The wizard bar generates method implementations that are marked as Private. This means that these method implementations are not available to clients that use a CBeagle reference. They are available only to clients that use an IDog reference. The preceding code also demonstrates how the CBeagle class can implement the logical Name property by defining a private data property and implementing the Property Let and Property Get methods.
Now that you have created an interface and a class that implements it, you can use the interface to communicate with an object. For instance, a client can communicate with a CBeagle object through an IDog reference. You can use the IDog reference to invoke any method that the interface exposes. Here is a simple example:
Dim Dog As IDog
Set Dog = New CBeagle
' access object through interface reference
Dog.Name = "Spot"
Dog.Bark
Dog.RollOver 12
Once the client is connected to the object through the interface reference, it can invoke methods and access logical properties. The Visual Basic IDE provides the same Microsoft IntelliSense®, type-checking, and debugging that is available when you use class-based references. Note that you cannot use an interface after the New operator. An interface is not a creatable type. You must use a concrete class such as CBeagle to create an object when you use the New operator.
When Visual Basic programmers learn how to use interfaces in an application, they often wonder, "Why would I ever want to do that?" or "Why should I care?" Programming with class-based references seems far more natural compared to the additional complexity required with user-defined interfaces. The previous example would have been far easier if the client code programmed against public methods and properties of the CBeagle class instead of the IDog interface. User-defined interfaces seem like extra work without any tangible benefits.
There are several significant reasons why a Visual Basic/COM programmer should care about interfaces. The first reason is that interfaces are the foundation of COM. In COM, clients cannot use class-based references. Instead, they must access COM objects through interface references. As it turns out, Visual Basic can do a pretty good job of hiding the complexities of this requirement. When you use a class-based reference, Visual Basic generates a default COM interface for the class behind the scenes. This means that you create COM-based applications with Visual Basic without ever having to deal with user-defined interfaces explicitly. However, if you embrace interface-based programming, you will become a much stronger COM programmer.
Another reason why you should care about interfaces is that they can offer power and flexibility in software designs. Using user-defined interfaces in Visual Basic becomes valuable when you don't have a one-to-one mapping between a class and a public interface. There are two common scenarios. In one scenario, you create an interface and implement it in multiple classes. In the other scenario, you implement multiple interfaces in a single class. Both techniques offer advantages over application designs in which clients are restricted to using references based on concrete classes. While interface-based designs often require more complexity, the sky is the limit when it comes to what you can do with them.
Consider a case in which many classes implement the same interface. For example, assume the classes CBeagle, CTerrier, and CBoxer all implement the interface IDog. An application can maintain a collection of IDog-compatible objects using the following code:
Dim Dog1 As IDog, Dog2 As IDog, Dog3 As IDog
' create and initialize dogs
Set Dog1 = New CBeagle
Dog1.Name = "Mo"
Set Dog2 = New CTerrier
Dog2.Name = "Larry"
Set Dog3 = New CBoxer
Dog3.Name = "Curly"
' add dogs to a collection
Dim Dogs As New Collection
Dogs.Add Dog1
Dogs.Add Dog2
Dogs.Add Dog3
The application can achieve polymorphic behavior by treating all of the IDog-compatible objects in the same manner. The following code demonstrates enumerating through the collection and invoking the Bark() method on each object:
Dim Dog As IDog
For Each Dog In Dogs
Dog.Bark
Next dog
As this application evolves, this collection can be modified to hold any mix of IDog-compatible objects, including objects created from CBeagle, CTerrier, CBoxer, and any other future class that is written to implement the IDog interface. The For Each loop in the previous example is written in terms of the IDog interface and has no dependencies on any concrete class. You do not have to modify the loop when you introduce new concrete class types into the application.
Another powerful design technique is to have a single class implement multiple interfaces. If you do this, you will have objects that support multiple interfaces and, therefore, multiple behaviors. When used together with run-time type inspection, this becomes very powerful. Assume the sample application adds another interface, IWonderDog, with the following method:
Sub FetchSlippers()
End Sub
Assume the CBeagle class implements IWonderDog but the CTerrier class does not. A client can inspect an object at run time and ask if it supports a specific interface. If the object does support the interface, the client can call upon its functionality. If the object does not support the interface, the client can degrade gracefully. The following code demonstrates using the Visual Basic TypeOf syntax to test for IWonderDog support:
Dim Dog1 As IDog, Dog2 As IDog
Set Dog1 = New CBeagle
Set Dog2 = New CTerrier
If TypeOf Dog1 Is IWonderDog Then
Dim WonderDog1 As IWonderDog
Set WonderDog1 = Dog1
WonderDog1.FetchSlippers
End If
If TypeOf Dog2 Is IWonderDog Then
Dim WonderDog2 As IWonderDog
Set WonderDog2 = Dog2
WonderDog2.FetchSlippers
End If
When the client queries the CBeagle object, it finds that it is IWonderDog-compatible. In other words, the object supports the IWonderDog interface. The client can then create an IWonderDog reference and assign the CBeagle object to it by casting the IDog reference with the Set statement. Once the client has an IWonderDog reference, it can successfully call FetchSlippers(). It's important to note that there are two references but only one object. When you have multiple interfaces, code in the client becomes more complex because it takes several references to a single object to get at all the functionality.
When the CTerrier object is queried for IWonderDog compatibility, the client discovers the interface is not supported. This allows the client to degrade gracefully. Client code can enumerate through a collection of IDog-compatible objects and safely call FetchSlippers() on each object that supports the IWonderDog interface, like this:
Dim Dog As IDog, WonderDog As IWonderDog
For Each Dog In Dogs
If TypeOf Dog Is IWonderDog Then
Set WonderDog = Dog
WonderDog.FetchSlippers
End If
Next dog
As you can imagine, this ability to determine the functionality of an object at run time is very useful when you evolve an application. If a later version of the CBoxer class implements the IWonderDog interface, the For Each loop shown in the preceding code can take advantage of that without being rewritten. Client code can anticipate supported functionality in future versions of the object.
The preceding example showed how to use an object that supports more than one interface. You can also employ user-defined interfaces to safely extend the behavior of an object when an existing set of method signatures has become too limiting. For instance, the IDog interface defines the RollOver() method as follows:
Public Sub RollOver(ByVal Rolls As Integer)
End Sub
If you need to extend the functionality of dog objects in the application so clients can pass larger integer values, you can create a second interface named IDog2. Assume the IDog2 interface defines the same members as Idog, with the exception of the RollOver() method, which is defined like this:
Public Sub RollOver(ByVal Rolls As Long)
End Sub
A new client can test to see if an IDog object supports the new behavior. If the new behavior is not supported, the client can simply fall back on the older behavior. Here's an example of how this works:
Sub ExerciseDog(Dog As IDog)
If TypeOf Dog Is IDog2 Then
' use new behavior if supported
Dim Dog2 As IDog2, lRolls As Long
Set Dog2 = Dog
lRolls = 50000
Dog2.RollOver lRolls
Else
' use to older behavior if necessary
Dim iRolls As Integer
iRolls = 20000
Dog.RollOver iRolls
End If
End Sub
The key observation to make about this versioning scheme is that you can introduce new clients and new objects into an application without breaking older clients and older objects. A new object can accommodate older clients by continuing to support the interfaces from earlier versions. New clients deal with older objects by using the older interface when required. In a world without interfaces, extending objects often requires modifying all the clients. Modifying clients often requires modifying all the objects. The versioning scheme made possible by interface-based programming allows you to make small changes to an application with little or no impact on code that is already in production.
This document has presented a simple application to demonstrate the core concepts of interface-based programming. How can you apply these principles in a real-world application? If you are designing a large application that uses customer objects, you can create a user-defined interface, ICustomer, and start writing lots of client code against the interface instead of to a concrete CCustomer class. If you create several classes that implement the ICustomer interface, you can achieve the plug-and-play benefits of polymorphism. Different types of customer objects exhibit different behavior, but they are all controlled through the same interface.
From a versioning standpoint, this design lets you evolve the behavior of various customer objects by introducing new interfaces into the application. Interfaces such as ICustomer2, ICustomer3, and ICustomer4 let you safely extend the behavior of customer objects. The best part about this approach is that you can revise clients and objects independently. Older clients and objects can use earlier interfaces, while newer clients and objects can communicate through newer interfaces. All of this is made possible through the run-time type inspection of interface support.
Microsoft Transaction Server provides yet another motivation for employing user-defined interfaces in an application design. MTS has a declarative security model based on roles. An MTS administrator assigns Microsoft Windows NT® user accounts and group accounts to MTS roles. The administrator can then configure the access permissions that each role has to an MTS component at both the class level and the interface level. You might consider creating an ICustomerRead and an ICustomerWrite interface to provide a greater degree of granularity. Once you implement these two interfaces in the CCustomer, you can easily configure security to allow one set of users to access objects through both interfaces, while limiting another set of users to read-only access. User-defined interfaces make the MTS security model far more powerful.
The industry has adopted interface-based programming because of the limitations of other common techniques, such as the use of class-based references and implementation inheritance. User-defined interfaces bring a new level of complexity to both application design and programming, but their value is easy to measure in large applications. In a Darwinian sense, interface-based programming makes software more fit for survival. Interfaces make your code easier to reuse, maintain, and extend.
COM is based on interface-based programming through and through. COM requires a formal separation of interface and implementation—that is, it requires that clients communicate with objects exclusively through interface references. This ensures that clients never build dependencies on the classes that serve up objects. This allows COM programmers to revise their object code without worrying about breaking client code. COM clients can get run-time type information from objects. A COM client can always query an object and ask if it supports a specific interface. If the requested interface is not supported, the client can discover this and degrade gracefully. This lets programmers revise components and applications independently. Older clients and older objects can work in harmony with newer clients and newer objects. Herein lies the key to versioning in COM.