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. Objects never expose internal details of the response.
Black-box reuse is based on formal separation of interface and implementation. This means that 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 ramifications of interfaces for 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 can't 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 that 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 isn't a creatable entity, an interface is an abstract data type. Objects can be instantiated only from creatable classes known as concrete data types.
From a design standpoint, an interface is a contract. A class that implements an interface guarantees that 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 that 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. The 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 way that's different from either CTerrier or CBoxer. This means that interfaces provide the opportunity for polymorphism. Interfaces are like implementation inheritance in that they let you build applications composed of plug-compatible objects. But 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 chapter has used the term implementation inheritance instead of the more generic term inheritance because extending a superclass with a subclass 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 doesn't require the reuse of method implementations. Instead, the only true requirement for inheritance is that a subclass instance be compatible with the base type that's being inherited. The base type that's inherited can be 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 implementation inheritance and interface inheritance offer polymorphism, but they differ greatly when it comes to their use of encapsulation. Implementation inheritance is based on white-box reuse. It allows a subclass to know intimate details of the classes it extends. This allows a subclass to experience implicit reuse of a superclass'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 problems associated with white-box reuse. However, to appreciate this style of programming, you must accept the idea 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 a set of methods. Subclass authors must write additional code whenever they decide to implement an interface. When you compare this with 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 interface inheritance isn't 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'll 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 IDE were to provide a separate editor for defining interfaces, but unfortunately an editor dedicated to creating interfaces isn't 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're creating an interface to express the behavior of a dog, a suitable name might be IDog or itfDog. These are the two most common naming conventions among Visual Basic developers. If you're working in a Visual Basic project that's 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:
' Interface 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(ByRef 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 makes no sense. The keyword End usually signifies the end of a method implementation. This is a confusing 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 won't 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 interface above 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 can't contain data properties, Visual Basic still lets you define a property in an interface, like this:
Public Name As String
However, when you define a data property in an interface, Visual Basic transparently redefines the data property as a logical property. This is simply a convenience that Visual Basic provides when you create interfaces. The Name property defined above still requires Property Let and Property Get in any class that implements the interface. Also 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 can't 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 code goes here. End Sub
Visual Basic's mapping of interfaces requires each method implementation to 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 will 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 combo box, you can quickly generate the skeletons for the method implementations by selecting the method names in the right combo box. An example of using the wizard bar is shown in Figure 2-4. Here's a partial implementation of the CBeagle class that implements the IDog 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 code goes here. End Sub Private Sub IDog_RollOver(ByRef Rolls As Integer) ' Implementation code goes here. End Sub
Figure 2-4. The wizard bar makes it easy to create the procedure stubs for implementing a user-defined interface.
The wizard bar generates method implementations that are marked as private. This means that these method implementations aren't available to clients that use a CBeagle reference. They're available only to clients that use an IDog reference. The code above 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's 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 IntelliSense, type checking, and debugging that are available when you use class-based references. Note that you can't use an interface after the New operator. An interface isn't 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 with the additional complexity required with user-defined interfaces. The previous example would have been far easier if the client code had programmed against a 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 can't use class-based references. Instead, they must access COM objects through interface references. As you'll see in later chapters, 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 can work in 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 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 that 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 the 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 don't 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'll have objects that support multiple interfaces and therefore multiple behaviors. When used together with run-time type inspection, this becomes very powerful. Assume that the sample application adds another interface, IWonderDog, with the following method:
Sub FetchSlippers() End Sub
Assume that the CBeagle class implements IWonderDog but that the CTerrier class doesn't. A client can inspect an object at run time and ask whether it supports a specific interface. If the object does support the interface, the client can call upon its functionality. If the object doesn't 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's 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. 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 that the interface isn't supported. This condition 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 improve an application. If a later version of the CBoxer class implements the IWonderDog interface, the For Each loop shown above can take advantage of that without being rewritten. Client code can anticipate supported functionality in future versions of the object.
The example above 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(ByRef Rolls As Integer) End Sub
If you need to extend the functionality of dog objects in the application so that clients can pass larger integer values, you can create a second interface named IDog2. Assume that the IDog2 interface defines the same members as IDog with the exception of the RollOver method, which is defined like this:
Public Sub RollOver(ByRef Rolls As Long) End Sub
A new client can test to see whether an IDog object supports the new behavior. If the new behavior isn't 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 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's already in production.