Export (0) Print
Expand All

When to Use Inheritance

Inheritance is a useful programming concept, but it is easy to use inappropriately. Often interfaces do the job better. This topic and When to Use Interfaces help you understand when each approach should be used.

Inheritance is a good choice when:

  • Your inheritance hierarchy represents an is-a relationship and not a has-a relationship.
  • You can reuse code from the base classes.
  • You need to apply the same class and methods to different data types.
  • The class hierarchy is reasonably shallow, and other developers are not likely to add many more levels.
  • You want to make global changes to derived classes by changing a base class.

These considerations are discussed in order below.

Inheritance and "Is a" Relationships

Two ways to show class relationships in object-oriented programming are "is a" and "has a" relationships. In an "is a" relationship, the derived class is clearly a kind of the base class. For example, a class named PremierCustomer represents an "is a" relationship with a base class named Customer because a premier customer is a customer. However, a class named CustomerReferral represents a "has a" relationship with the Customer class because a customer referral has a customer, but a customer referral is not a kind of customer.

Objects in an inheritance hierarchy should have an "is a" relationship with their base class because they inherit the fields, properties, methods, and events defined in the base class. Classes that represent a "has a" relationship with other classes are not suited to inheritance hierarchies because they may inherit inappropriate properties and methods. For example, if the CustomerReferral class were derived from the Customer class discussed previously, it might inherit properties that make no sense, such as ShippingPrefs and LastOrderPlaced. "Has a" relationships such as this should be represented using unrelated classes or interfaces. The following illustration shows examples of both "is a" and "has a" relationships.

Base Classes and Code Reuse

Another reason to use inheritance is the advantage of code reuse. Well-designed classes can be debugged once and used over and over as a basis for new classes.

A common example of effective code reuse is in connection with libraries that manage data structures. Suppose, for example, that you have a large business application that manages several kinds of in-memory lists. One is an in-memory copy of your customer database, read in from a database at the beginning of the session for speed. The data structure might look something like the following:

Class CustomerInfo
   Public PreviousCustomer As CustomerInfo
   Public NextCustomer As CustomerInfo
   Public ID As Integer
   Public FullName As String
   
   Function InsertCustomer As CustomerInfo
      ' Add code to add a CustomerInfo item to the list.
   End Function
 
    Function DeleteCustomer As CustomerInfo
      ' Add code to remove a CustomerInfo item from the list.
    End Function

   Function GetNextCustomer As CustomerInfo
      ' Add code to get the next CustomerInfo item from the list.
   End Function

   Function GetPrevCustomer As CustomerInfo
        'Add code to get the previous CustomerInfo item from the list.
   End Function
End Class

Your application may also have a similar list of products the user has added to a shopping cart list, as shown in the following code fragment:

Class ShoppingCartItem
   Public PreviousItem As ShoppingCartItem
   Public NextItem As ShoppingCartItem 
   Public ProductCode As Integer
   Function GetNextItem As ShoppingCartItem
      ' Add code to get the next ShoppingCartItem from the list.
   End Function
End Class

You can see a pattern here: two lists behave the same way (insertions, deletions, and retrievals) but operate on different data types. Maintaining two code bases to perform essentially the same functions is asking for trouble. The most efficient solution is to factor out the list management into its own class, and then inherit from that class for different data types:

Class ListItem
   Public PreviousItem As ListItem
   Public NextItem As ListItem
   Function GetNextItem() As ListItem
      ' Add code to get the next item in the list.
   End Function
   Function InsertNextItem As ListItem
      ' Add code to add a item to the list.
   End Function
 
    Function DeleteNextItem As ListItem
      ' Add code to remove a item from the list.
    End Function

   Function GetPrevItem As ListItem
        'Add code to get the previous item from the list.
   End Function

End Class

The ListItem class needs only to be debugged once. Then you can build classes that use it without ever having to think about list management again. For example:

Class CustomerInfo
   Inherits ListItem
   Public ID As Integer
   Public FullName As String
End Class
Class ShoppingCartItem
   Inherits ListItem
   Public ProductCode As Integer
End Class

Although inheritance-based code reuse is powerful tool, it also has associated risks. Even the best-designed systems sometimes change in ways the designers could not foresee. Changes to an existing class hierarchy can sometime have unintended consequences; some examples are discussed in "The Fragile Base Class Problem," in Class Design Changes After Deployment.

Interchangeable Derived Classes

Derived classes in a class hierarchy can sometimes be used interchangeably with their base class, a process called inheritance-based polymorphism. This approach combines the best features of interface-based polymorphism with the option of reusing or overriding code from a base class.

An example where this can be useful is in a drawing package. For example, consider the following code fragment, which does not use inheritance:

Sub Draw(ByVal Shape As DrawingShape, ByVal X As Integer, _

      ByVal Y As Integer, ByVal Size As Integer)

   Select Case Shape.Type

      Case shpCircle

      ' Circle drawing code here.

      Case shpLine

      ' Line drawing code here.

   End Select

End Sub

This approach poses some problems. If someone decides to add an ellipse option later, it will be necessary to alter the source code; it is possible that your target users will not even have access to your source code. A more subtle problem is that drawing an ellipse requires another parameter (ellipses have both a major and a minor diameter) that would be irrelevant to the line case. If someone then wants to add a polyline (multiple connected lines), then another parameter would be added, and it would be irrelevant to the other cases.

Inheritance solves most of these problems. Well-designed base classes leave the implementation of specific methods up to the derived classes, so that any kind of shape can be accommodated. Other developers can implement methods in derived classes by using the documentation for the base class. Other class items (such as the x- and y-coordinates) can be built into the base class because all descendants use them. For example, Draw could be a MustOverride method:

MustInherit Class Shape
   Public X As Integer
   Public Y As Integer
   MustOverride Sub Draw()
End Class

Then you could add to that class as appropriate for different shapes. For example, a Line class might only need a Length field:

Class Line
   Inherits Shape
   Public Length As Integer
   Overrides Sub Draw()
       ' Place code here to implement Draw for this shape.
   End Sub
End Class

This approach is useful because other developers, who do not have access to your source code, can extend your base class with new derived classes as needed. For example, a class named Rectangle could be derived from the Line class:

Class Rectangle
   Inherits Line
   Public Width As Integer
   Overrides Sub Draw()
      ' Place code here to implement Draw for the Rectangle shape.
   End Sub
End Class

This example shows how you can move from general-purpose classes to very specific classes by adding implementation details at each level.

At this point it might be good to reevaluate if the derived class truly represents a "is a" relationship, or have we inadvertently used a "has a" relationship. If the new rectangle class is just composed of lines, then inheritance is not the best choice. However, if the new rectangle is a line with a width property, then the "is a" relationship is maintained.

Shallow Class Hierarchies

Inheritance is best suited for relatively shallow class hierarchies. Excessively deep and complex class hierarchies can be difficult to develop. The decision to use a class hierarchy involves weighing the benefits of using a class hierarchy against complexity. As a general rule, you should limit hierarchies to six levels or fewer. However, the maximum depth for any particular class hierarchy depends on a number of factors, including the amount of complexity at each level.

Global Changes to Derived Classes Through the Base Class

One of the most powerful features of inheritance is the ability to make changes in a base class that propagate to derived classes. When used carefully, you can update the implementation of a single method, and dozens, or even hundreds of derived classes can use the new code. However, this can be a dangerous practice because such changes may cause problems with inherited classes designed by other people. Care must be taken to ensure that the new base class is compatible with classes that use the original. You should specifically avoid changing the name or type of base class members.

Suppose, for example, that you design a base class with a field of type Integer to store zip code information, and other developers have created derived classes that use the inherited zip code field. Suppose further that your zip code field stores 5 digits, and the post office has expanded zip codes with a hyphen and 4 more digits. In a worst-case scenario, you could modify the field in the base class to store a 10-character string, but other developers would need to change and recompile the derived classes to use the new size and data type.

The safest way to change a base class is to simply add new members. For example, you could add a new field to store the additional 4 digits in the zip code example discussed previously. That way, client applications can be updated to use the new field without breaking existing applications. The ability to extend base classes in an inheritance hierarchy is an important benefit that does not exist with interfaces.

See Also

When to Use Interfaces | Class Design Changes After Deployment

Show:
© 2014 Microsoft