4.7 Substitution

only for RuBoard

4.7 Substitution

One of polymorphism's best features is that it allows you to use a derived class where a base class is expected. It sounds crazy, but it's true. And because of that, it also allows you to write some amazingly adaptable code. Look at the following fragment:

 Dim pmt As Payment
pmt = New CreditCard( )
pmt.Authorize(3.33) 

Guess what method is called as a result of running this code? If you guessed CreditCard.Authorize , you are right.

To use a real-world context as an example, some larger online retailers allow the use of multiple payments when placing an order. Theoretically, each payment could be different: a credit card, a gift certificate, and maybe some kind of Internet-based cash. In a situation like this, you could create a very adaptable solution by writing the following code:

 'Pseudo-code
Dim p As Payment
Dim payments( ) As Payment
   
payments(0) = New GiftCertificate(30.00)
payments(1) = New NuttyCash(15.00)
payments(2) = New CreditCard("1234")
   
For Each p In payments
    p.Bill( )
Next p 

An array of the base type, Payment , was declared. The array serves as a container that can hold any object derived from the base Payment type. When the Bill method is called for each element, the appropriate version of the method is called. If the payment is a credit card, then CreditCard.Bill is called. If it is a gift certificate, then GiftCertificate.Bill is called, and so on.

You can also declare methods that expect a base class like this:

 Private Function ProcessPayment(ByVal p As Payment, _
                                ByVal amount As Double)
  If p.Authorize(amount) Then
    p.Bill( )
  Else
    'Notify customer service
  End If
End Function 

Each major function Authorize , Bill , and Credit is overridable. Overridable functions are often called virtual methods , indicating that the most derived implementation is called. It doesn't matter that each object is accessed through its base type. What matters is the actual type of aliased object.

If you made your base classes abstract, you should write methods that expect the base class and not a specific derivative. Writing your methods in this way will allow you to grow your applications over time without creating dependencies. Consider the following code fragment:

 Select Case ClassName
   
  Case "CreditCard"
  'Do cc billing
   
  Case "GiftCertificate"
  'Do gift cert. billing
   
  Case "WireTransfer"
  'Do wire transfer billing
   
  Case "Check"
  'Do check billing
   
End Select 

When you find yourself writing code in which you look at the actual type of an object like this, step back for a moment and reconsider your course of action. Polymorphism was designed as an alternative to this type of coding.

Why is this code fragment problematic ? The function to which the code fragment belongs cannot be considered closed . This means that there is no way to add a new kind of payment to the system without revisiting the code shown in the fragment. When you add the new type, you have to add a new case to handle it. Adding this new case is a violation of a very important principle in object-oriented programming called the open -closed principle . This principle is responsible for many of the practices that programmers today consider "good." The fact that class members should always be private and global variables should be avoided comes from this principle. Honestly, people don't just make this stuff up.

4.7.1 Open-Closed Principle

If you develop an application that will last through more than one version, you can be sure of one thing: change. And if a single change introduces more changes like a domino effect throughout your application, you probably have what is known in the industry as bad code . Designing software that is stable and impervious to change is pivotal to the open-closed principle. This principle comes from Bertrand Meyer, demigod of OOP and creator of the Eiffel programming language. It goes something like this:

Software entities should be open for extension, but closed for modification.

Here, "entities" can mean a couple of things. A class is the obvious first choice, but it can also be applied to an entire module (a source file containing numerous related classes) or just a single method. When an entity is "open for extension," new behavior can be added as a result of changing requirements, but the entity itself cannot change. It is "closed for modification." Doesn't this concept seem like a contradiction?

Applications conforming to the open-closed principle are changed by adding new code, not by changing code that already works. Abstraction and polymorphism are key in implementing this principle. Abstract base classes allow you to define a specific set of behaviors that can be implemented in infinite ways through derived classes. As you have seen, methods can be written to expect these abstract base classes instead of concrete types. These methods and the modules they reside in can be considered closed because they rely on an abstraction that will not change.

Consider a class whose member variables are made public. If the member variables change, then every method that depends on those variables needs to be changed, too. Thus, no method that depends on those variables can be thought of as closed. Of course, you should expect that member functions of a class need to change (internally) in respect to its member data. But you should also expect a derived class to be closed to changes of those variables. This is encapsulation, after all.

Using abstraction and polymorphism is the way to successfully write code that adheres to the open-closed principle. These two mechanisms are enforced through inheritance. By using inheritance, you can derive classes that conform to the contract presented by the base class. Specifically, the abstract base class says that you must override certain functions to participate in this hierarchy or relationship.

Are there any rules or pitfalls to be aware of when using inheritance like this? Is it enough to say that Class B is-a Class A and be done with it? Or will you need to look at other considerations? Rest assured, devious traps lie in wait for those who use inheritance in a reckless and cavalier manner. However, what you need to watch out for is not always obvious.

4.7.2 Liskov Substitution Principle

If you want a good grasp of proper inheritance, the Liskov Substitution Principle (LSP) is a good place to start. Basically, it says:

Methods that use references to base classes must be able to use objects of derived classes without knowing it.

In other words, a derived class must behave like a base class; you should be able to swap the two classes in any given situation. Sounds easy, right? Well, let's see what happens when this principle is violated.

Because no book on OOP should omit an example involving a geometric shape of some kind, here is a token example of an application that uses rectangles. However, the charade ends here. This isn't a CAD program or a non-Windows GUI framework. This example is a rectangle application, pure and simple. The primary class used in this application is the Rectangle , shown in Example 4-9.

Example 4-9. The amazing Rectangle class
 Public Class Rectangle
   
  Private myWidth As Double
  Private myHeight As Double
   
  Public Property Width( ) As Double
    Get
      Return myWidth
    End Get
    Set(ByVal Value As Double)
      myWidth = Value
    End Set
  End Property
   
  Public Property Height( ) As Double
    Get
      Return myHeight
    End Get
    Set(ByVal Value As Double)
      myHeight = Value
    End Set
  End Property
   
  Public ReadOnly Property Area( ) As Double
    Get
      Return myWidth * myHeight
    End Get
  End Property
   
  Public Overrides Function ToString( ) As String
    Dim s As String
    s = String.Format("Width: {0}, Height: {1}", _
        myWidth.ToString( ), myHeight.ToString( ))
    Return s
  End Function
   
End Class 

OK, so the rectangle application works well. It handles those rectangles like nobody's business, when suddenly, a highly intelligent person from marketing drops in and says that the application needs to work with squares. Incredible as it sounds, some people want squares, too.

Well, everyone knows that inheritance should represent an is-a relationship, and for the most part, a square is a rectangle. After all, it has four sides, right? You should be able to derive it from Rectangle like this:

 Public Class Square : Inherits Rectangle
   
End Class 

However, just using the is-a relationship as a yardstick for proper inheritance can lead to some subtle problems. First, a Square doesn't need to be described in terms of two sides. Thus, an extra member variable takes up 8 bytes of space. Remember that this isn't a CAD application in which there could be thousands of squares lying around. But even if you are not concerned with memory, there is another problem.

Square inherits the Width and Height properties from Rectangle . This inheritance is inappropriate because squares aren't usually described according to width and height. Again, a square is defined by the fact that both adjacent sides are the same. It is possible to override Width and Height so that when one is set, the other is set to the same value. In this way, the sides of the square would never be invariant.

The problem is that these properties are not Overridable . You might say that this problem is an oversight to be blamed on the writer of the base class, but is it really? After all, Width and Height are primitive operations in respect to a rectangle. Blaming this problem on an oversight is like saying, "We'd better give someone the ability to change the way multiplication works for integers!"

For the sake of argument, assume that the Width and Height properties are virtual. In that case, as the code in Example 4-10 shows, you can override them so that when one is set, the other is set to the same value. In this way, the sides of the Square are never discordant.

Example 4-10. The Square class
 Public Class Square : Inherits Rectangle
   
  Public Overrides Property Width( ) As Double
    Get
      Return MyBase.Width
    End Get
    Set(ByVal Value As Double)
      MyBase.Width = Value
      MyBase.Height = Value
    End Set
  End Property
   
  Public Overrides Property Height( ) As Double
    Get
      Return MyBase.Height
    End Get
    Set(ByVal Value As Double)
      MyBase.Width = Value
      MyBase.Height = Value
    End Set
  End Property
   
End Class 

When one side of the square is set, the other is set to match. Everything should work, right? Well, not exactly. In a fish tank, the assumptions made here seem to hold water, but what about the assumptions a user might make? Ahhhh, so many times, the user is forgotten! Look at the following code fragment to see the story unfold:

 #Const Debug = True
   
Imports System
Imports System.Diagnostics
   
Public Class NotSomeCADProgram
   
  Public Sub Test(ByVal r As Rectangle)
    r.Width = 5
    r.Height = 10
    Debug.Assert(r.Area = 50)
  End Sub
   
End Class 

What happens when a Square is passed to the Test method? It's going to blow up for sure. The area will end up being 100 . Is the person who wrote that method in error? It does seem reasonable to assume that changing the width of a rectangle would not alter its height. After all, a rectangle is described by two sides, and each is independent of the other.

A square might be a rectangle, but a Square is definitely not a Rectangle . Admittedly, the Square class did look valid until it was actually used. However, when viewed in terms of an actual user, the idea that a Square is-a Rectangle falls apart. Sure, they might share similar properties, but the behavior of a Square is different from that of a Rectangle ; is-a is more about behavior than anything else.

4.7.3 Shadowing

Overriding a method hides the method that has the same name and signature in the base class. However, another form of method-hiding hides by name alone. It is called shadowing .

Shadowing is very different from overriding. You are free to shadow any method in a base class that you choose; no permission is needed. Also, shadowed methods are not virtual. For example, look at this code fragment:

 Imports System
   
Public Class A
  Public Sub Foo( )
    Console.WriteLine("A.Foo")
  End Sub
End Class
   
Public Class B : Inherits A
  Public Shadows Sub Foo( )
    Console.WriteLine("B.Foo")
  End Sub
End Class
   
Public Class Test
  Public Shared Sub Main( )
    Dim myA As A = New B( )
    myA.Foo( )
  End Sub
End Class 

If you reference an instance of B through the base class A , A.Foo is still called as a result. With Overrides , you would expect B.Foo to be called. If you want to verify this process for yourself, add an Overridable modifier to A.Foo and replace Shadows with Overrides in B.Foo .

Shadowing hides every member in the base class. If your base class has 15 overloaded Foo methods and a derived class shadows it, all 15 methods will be hidden. The only usable version of Foo is the one in the derived class, unless you access the method through a base class reference (which is not very realistic in most situations).

Interestingly, shadowing solves the problem presented in Example 4-10. If the Width and Height properties were shadowed and an instance of Square were passed to the Test method, the assertion would have held because the base class version of the properties would have been called. However, the only reason this is mentioned is to remind those of you who may have noticed the fact that this is not a valid solution in this particular case.

only for RuBoard