The Fundamentals of Object-Oriented Programming

[Previous] [Next]

Your first stop on the road to interface awareness must be an examination of the problems that interface-based programming was 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. The client usually creates the object by using the New operator followed by the class name. After creating the object, the client uses it by accessing an exposed set of properties and methods through a variable that is a class-based reference. Here's a simple example that uses 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 these members in order to honor the 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 aren't publicly visible. You can also change public method implementations as long as the calling syntax of the methods isn't 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 shouldn't 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 can't 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. If you do this, you can 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 as long as you can make one big assumption: that the class and the clients are compiled at the same time. Old client code will continue to run as before, even though it can't take advantage of the object's new functionality. New client code written after the class has been modified, however, 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.

What about a situation in which a class definition is compiled separately from the client code that uses it? This creates a potential versioning problem. Let's look at an example to illustrate the problem. Let's say you compile a class into one executable and compile some client code that uses it into another. After you put both of these executables into production, you decide to add a new method to the second version of the class. After that, you create a second version of the client that uses this new method. What will happen if you put the newly compiled client into production before the newly compiled class? The newer version of the client will attempt to execute a method that the older version of the class doesn't support.

This example should lead you to the following conclusion: When you're writing classes for a component-based system, adding methods to later versions of a class can be as bad as removing or changing methods. The public interface of a class must be held constant in order to provide compatibility in every situation.

Changing the Public Interface of a Class

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(ByRef Rolls As Integer) ' Implementation code goes here. End Sub ' Client hardcodes calling syntax. Dim Dog As CDog, Rolls As Integer Set Dog = New CDog Rolls = 20000 Dog.RollOver Rolls 

What if the requirements for a dog object weren't properly anticipated in the initial design? For instance, what if the required number of rolls exceeds the highest possible value for an Integer (about 32,000)? Perhaps your competitor has just released a dog component that supports rolling over 100,000 times. While you might be able to rewrite your implementation of RollOver to match or surpass your competitor's software, the bigger problem is with your method's signature. In order for your dog object to roll 100,000 times, you must change the parameter type to Long (long integer). This creates quite a design problem. The newer clients want to pass a 32-bit Long. But older clients such as the ones just shown 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 the method. 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 that the design of the class's public interface is full-featured and finalized before you write client code against it. But this isn't 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. In many cases, however, 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's expected to run for years in a rapidly changing business environment can't 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.

The use of class-based references results in a layer of dependencies between clients and classes. These dependencies create tight coupling between various pieces of software. As the author of a class, you want the freedom to modify and extend the functionality you expose in later versions. When you're authoring classes with public methods, however, it's easy to paint yourself into a corner. When you're months or years into a large project, you're faced with a no-win decision. Should you change the public interface defined by your class and force all clients to modify their code and recompile? Or should you freeze the functionality of your objects?

A Quick Primer on Implementation Inheritance

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 book won't address. Instead, we'll 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's reused is called the superclass. The class that benefits from the reuse is the subclass. Visual Basic doesn't currently support implementation inheritance (but I'm sure you've heard all the rumors), so I'll use a Java example to illustrate what implementation inheritance looks like. Examine the following Java class CDog:

 // Superclass 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 that each method has been defined with a valuable implementation. You can reuse the state and the behavior of the class by using implementation inheritance. CDog will be used as a superclass. A subclass that extends CDog will inherit both the class properties and the method implementations. The following Java code shows the syntax required to achieve implementation inheritance:

 // Subclass 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 subclass) extends CDog (the superclass), 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 methods such as FetchSlippers in CBeagle. You can also add new properties to the subclass definition.

You should use implementation inheritance only when a logical "is a" relationship exists between the subclass and the superclass. In this example, you can say, "A beagle is a dog." Figure 2-1 graphically depicts a relationship in which one class derives from another. 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 superclass. 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.

click to view at full size.

Figure 2-1 Implementation inheritance allows one class to reuse the state and the behavior of another.

Figure 2-2 is a graphic representation of what is known as an inheritance hierarchy. The hierarchy shows the relationships among 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 aren't uncommon in production code.

click to view at full size.

Figure 2-2 An inheritance hierarchy shows the relationships between the superclasses and the subclasses in an application.

When implementation inheritance is used correctly, it can be a powerful mechanism for code maintenance. When you improve the implementation of a method in a superclass, all of 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.

What is polymorphism?

So far, this chapter has explained how implementation inheritance offers the implicit reuse of method implementations, which results in greater maintainability through the elimination of duplicate code. Another powerful OOP feature provided by implementation inheritance is 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 superclass reference to connect to and invoke methods on subclass instances. Figure 2-3 shows how a client can use a CDog reference to communicate with three different types of objects. Each subclass that derives from CDog is type-compatible with a CDog reference. Therefore, a client can use a CDog reference when communicating with objects of type CBeagle, CTerrier, or CBoxer.

Figure 2-3 You can achieve polymorphism by using a superclass reference to communicate with subclass 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 subclass uses the definition of Bark that was supplied by CDog or if the subclass has overridden this method with its own implementation. The client simply invokes the method using the calling syntax defined in the CDog class. But if each subclass supplies its 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's 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 isn't determined until runtime. Polymorphism is based on the idea of dynamic binding as opposed to static binding. Dynamic binding provides a degree of controlled uncertainty that makes polymorphism very powerful.

Many developers have discovered the value of creating applications with plug-compatible objects. Even if thousands of lines of client code have been written to CDog's public interface, you can easily replace a CBeagle object with a CTerrier object or a 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.

Problems with implementation inheritance

So far, this chapter has explored the two biggest benefits of implementation inheritance: the implicit reuse of method implementations and polymorphism. It has not yet covered some of the potential problems with implementation inheritance. Unfortunately, implementation inheritance makes an application more susceptible to the kinds of dependency problems associated with class-based references because of the tight coupling between a subclass and its superclass.

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 levels. Properties and methods that are marked as protected are hidden from a client but are accessible from subclasses. Subclasses can see implementation details that have been hidden from the client. As you hardcode the names of protected properties and methods of a superclass into a subclass, 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 a tight coupling between the classes in the inheritance hierarchy. Once a subclass uses a protected property or method, you can't change the superclass's signature or remove it without breaking dependencies built into subclasses. This leads to fragility in applications with large inheritance hierarchies. Changing the classes at the top of the hierarchy often requires modifications to many subclasses. In some applications, changing a method signature or a 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 superclasses usually results in a system that can't evolve.

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 superclass scenario. You should know whether a class will be extended by subclasses. If you expect a class to be extended, it's as important to hide implementation details from subclasses as it is to hide them from clients.

This isn't to suggest that implementation inheritance isn't useful. It's powerful in appropriate development scenarios. It's 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 haven't understood the coupling problems that accompany white-box reuse. Over the past decade, the casual use of implementation inheritance has crippled the evolution of many large applications. Experienced developers who knew that implementation inheritance was most appropriate in small doses looked 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. This fueled the birth of interface-based programming and a development style known as object composition.



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