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 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 superclass. The class that benefits from the reuse is the subclass. Visual Basic doesn't currently support implementation inheritance, so I will 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," as shown in Figure 2-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 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 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, CRetriever, 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. However, 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 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 as opposed to static binding. Dynamic binding provides a degree of controlled uncertainty that makes polymorphism extremely powerful. You can create applications based on plug-compatible objects. 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 Associated 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. Properties and methods that are marked as protected are hidden from a client but are accessible from subclasses. Subclasses therefore have access to 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 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.

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 superclass scenario. You should know whether a class will be extended by other subclasses. If you expect a class to be extended, it's as important to encapsulate implementation details from subclasses as it is to encapsulate 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 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. This fueled the birth of interface-based programming and a development style known as object composition.



Programming Distributed Applications With Com & Microsoft Visual Basic 6.0
Programming Distributed Applications with Com and Microsoft Visual Basic 6.0 (Programming/Visual Basic)
ISBN: 1572319615
EAN: 2147483647
Year: 1998
Pages: 72
Authors: Ted Pattison

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