Separating the Interface from the Implementation

[Previous] [Next]

Interface-based programming offers another way to achieve reuse without the tendency toward tight coupling. Interface-based programming is based on black-box reuse, in which encapsulation is never compromised. Clients know only about the names and the calling syntax for an available set of requests. Clients never know about the implementation details behind the objects they're using.

Black-box reuse is achieved through a 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 scope of a class definition.

At this point, you're 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. But 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. While an interface defines method signatures, however, 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.

The Two Faces of Inheritance

Inheritance is an objected-oriented concept based on the "is a" relationship between entities. So far, I've 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. In general, 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 extended. The base type that's extended can be a class or a user-defined interface. In either situation, you can use a base-type reference 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 superclass 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. But this reuse comes at a cost. The loss of encapsulation in white-box reuse limits the scalability of applications based on implementation inheritance.

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. But 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 method implementations. 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.

Using Interfaces with Visual Basic

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 three required steps:

  1. Define an interface.
  2. Implement the interface in one or more creatable classes.
  3. Use an interface reference in a client to communicate with objects.

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 look at a simple example of the Visual Basic syntax required to complete these steps.

You can create user-defined interfaces in Visual Basic by using standard class modules. It would be better if the Visual Basic integrated development environment (IDE) provided 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 interface definitions as well as 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 adjustable 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. Objects are created from a class that encapsulates the 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 

But when you define a property in an interface, Visual Basic transparently defines it as a logical property. This is simply a convenience that Visual Basic provides when you create user-defined interfaces. The Name property defined in the preceding line of code 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. You 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 is 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 skeletons 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 

click to view at full size.

Figure 2-4 The wizard bar makes it easy to create the procedure skeletons for implementing a user-defined interface.

The wizard bar generates method signatures 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've 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 ' IDog is abstract. Set Dog = New CBeagle ' CBeagle is concrete. ' 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 features, type checking, and debugging that are available when you use class-based references. Note that you shouldn'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.

Why Design with User-Defined Interfaces?

When Visual Basic programmers see what's required to utilize user-defined interfaces in an application for the first time, 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 been written 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 programmer building a distributed application 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 the next chapter, Visual Basic does a pretty good job of hiding the complexities of this requirement. When you use a class-based reference, Visual Basic generates a default COM-style 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. If you embrace interface-based programming, however, you'll 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. Two scenarios are common. 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 iterates through the collection and invokes 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's 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 any of the code inside 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 runtime type inspection, this strategy becomes very powerful. Assume that the sample application adds another interface, IWonderDog, with the following method:

 Sub FetchSlippers() 

Let's say you have two classes, CBeagle and CTerrier, that both implement IDog. Assume that the CBeagle class implements IWonderDog but that the CTerrier class doesn't. A client can inspect objects created from these classes at runtime and ask them whether they support a specific interface. If an object supports the interface, the client can call upon its functionality. If an object doesn't support the interface, the client can degrade gracefully. The following code uses the Visual Basic TypeOf keyword 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 object 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. Note that two references refer to the same object. When you're dealing with classes that implement 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, which allows the client to degrade gracefully. Client code can iterate 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 runtime 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.

Extending a Class Definition

The example above shows 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(ByVal 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 classes into an application without breaking older clients and older classes. A new class can accommodate older clients by continuing to support the interfaces from earlier versions. New clients deal with older classes by using the older interface when required. In a world without interfaces, extending classes often requires modifying all the clients. Modifying clients often requires modifying classes. The versioning scheme made possible by interface-based programming allows you to make changes to an application with little or no impact on code that's already in production.

Using Interfaces in Your Application Designs

This chapter 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're designing a large application that uses customer data from many different DBMSs, you often find that it's necessary to write separate code for each one. You can create a user-defined interface ICustomerDataManager and start writing lots of client code in business logic components against the interface instead of a concrete CCustomerDataManager class. This will make it much easier to port your application across several DBMSs. If you create several classes that implement the ICustomerDataManager interface (perhaps one class for accessing an Oracle DBMS and another for SQL Server), you can achieve the plug-and-play benefits of polymorphism. Different types of objects exhibit different behavior, but they're all controlled through the same interface.

From a versioning standpoint, this design lets you improve the behavior of various classes by introducing new interfaces into the application. Interfaces such as ICustomerDataManager2, ICustomerDataManager3, and ICustomerDataManager4 let you safely extend the behavior of your objects over time. The best part about this approach is that you can revise clients and classes independently. Class authors have much more freedom to modify and extend the functionality behind their objects without breaking existing clients. 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 runtime type inspection of interface support.

Interfaces and COM

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 fact, this is one of the only areas in which Microsoft and Sun agree.) In a Darwinian sense, interface-based programming makes software more fit for survival. Interfaces remove the dependencies that couple various pieces of software. And this act of decoupling makes your code easier to reuse, maintain, and extend.

The next chapter presents the fundamentals of COM. As you'll see, COM is based on the following core concepts of interface-based programming:

  • 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 undesirable dependencies on components (classes), which in turn allows component authors to revise their class code without worrying about breaking client code.
  • COM clients query objects for runtime type information A client can always ask an object whether it supports a specific interface. If the requested interface isn't supported, the client can discover this and degrade gracefully. This lets programmers revise components and client applications independently. Older components and older clients can work in harmony with newer components and newer clients. Herein lies the key to versioning in COM.

Summary

This chapter showed the use of interfaces in a single application. The entire application was written in a single language, and all the source code was sent to a compiler at the same time. COM, on the other hand, must work across binary component boundaries. Moreover, clients and components can be written in different languages and can run in different processes on different computers. COM must solve many problems at the physical level to achieve the benefits of interface-based programming in a distributed environment.



Programming Distributed Applications with COM+ and Microsoft Visual Basic 6.0
Programming Distributed Applications with Com and Microsoft Visual Basic 6.0 (Programming/Visual Basic)
ISBN: 1572319615
EAN: 2147483647
Year: 2000
Pages: 70
Authors: Ted Pattison

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net