GOTCHA 42 Runtime Type Identification can hurt extensibility


GOTCHA #42 Runtime Type Identification can hurt extensibility

In working with an inheritance hierarchy, how do you know which type an instance belongs to? Given a reference, you can use casting to convert it to the type you desire. However, the problem with casting is that if the conversion is not valid, it results in a runtime InvalidCastException. This is ugly and you must avoid it at all costs. What alternatives do you have?

.NET languages allow you to determine the type of an object at runtime. This feature is called Runtime Type Identification (RTTI). In C# you use the keyword is; in VB.NET you use TypeOf...Is. I will refer to these as RTTI operators.

What are the consequences of using RTTI operators extensively or arbitrarily? I will discuss the dark side of RTTI in a very simple example, shown in Example 6-1.

Example 6-1. Working with object hierarchy

C# (RTTI)

 //Animal.cs public class Animal {     public void Eat() { Console.WriteLine("Animal eating"); } } //Dog.cs public class Dog : Animal {     public void Bark() { Console.WriteLine("Dog barking"); } } //Cat.cs public class Cat : Animal {     public void Meow() { Console.WriteLine("Cat Meowing"); } } //Trainer.cs public class Trainer {     public void Train(Animal anAnimal)     {         anAnimal.Eat();         //Using casting         Dog doggie = (Dog) anAnimal;         doggie.Bark();         Cat aCat = (Cat) anAnimal;         aCat.Meow();         } } //Test.cs class Test {     [STAThread]     static void Main(string[] args)     {         Dog spencer = new Dog();         Cat snow = new Cat();         Trainer jimmy = new Trainer();         jimmy.Train(spencer);         jimmy.Train(snow);     } } 

VB.NET (RTTI)

 'Animal.vb Public Class Animal     Public Sub Eat()         Console.WriteLine("Animal eating")     End Sub End Class 'Dog.vb Public Class Dog     Inherits Animal     Public Sub Bark()         Console.WriteLine("Dog barking")     End Sub End Class 'Cat.vb Public Class Cat     Inherits Animal     Public Sub Meow()         Console.WriteLine("Cat Meowing")     End Sub End Class 'Trainer.vb Public Class Trainer     Public Sub Train(ByVal anAnimal As Animal)         anAnimal.Eat()         'Using Casting         Dim doggie As Dog = CType(anAnimal, Dog)         doggie.Bark()         Dim aCat As Cat = CType(anAnimal, Cat)         aCat.Meow()     End Sub End Class 'Test.vb Module Module1     Sub Main()         Dim spencer As New Dog         Dim snow As New Cat         Dim jimmy As New Trainer         jimmy.Train(spencer)         jimmy.Train(snow)     End Sub End Module 

In this example, the TRainer wants to train an Animal. In the process of training, she first feeds the animal by calling the Eat() method. In the next activity, she wants the animal to express itself. In this example, the animal may be either a Dog or a Cat, so you cast it to these types and call the Bark() and the Meow() methods. But this code, while flawless in compilation, throws an InvalidCastException at runtime, as shown in Figure 6-1.

Figure 6-1. Output from Example 6-1


The CLR does not like your casting a Dog object to a Cat. (It is only natural that dogs do not like to be treated as catsdo not try that at home.) The worst you can do at this point is to surround the casting code with try/catch statements, suppress the exception in the catch and claim that you have taken care of the situation. This is undesirable for a couple of reasons. For one thing, using exceptions in situations like this is expensive. For another, you have not properly handled the condition where the given Animal is not a type you expect. Casting is ugly.

Let's consider the use of RTTI. In the code in Example 6-2, I show only the changes to the TRainer class (the only class I have changed).

Example 6-2. Using RTTI

C# (RTTI)

 //Trainer.cs public class Trainer {     public void Train(Animal anAnimal)     {         anAnimal.Eat();         //Using RTTI         if (anAnimal is Dog) {             Dog doggie = (Dog) anAnimal;             doggie.Bark(); }         else if (anAnimal is Cat) {             Cat aCat = (Cat) anAnimal;             aCat.Meow();         }     } } 

VB.NET (RTTI)

 'Trainer.vb Public Class Trainer     Public Sub Train(ByVal anAnimal As Animal)         anAnimal.Eat()         'Using RTTI         If TypeOf anAnimal Is Dog Then             Dim doggie As Dog = CType(anAnimal, Dog)             doggie.Bark()         ElseIf TypeOf anAnimal Is Cat Then             Dim aCat As Cat = CType(anAnimal, Cat)             aCat.Meow()         End If     End Sub End Class 

In the train() method you check to see if, at run time, the given reference points to an instance of Dog. If so, then you perform the cast. Similarly, you check to see if the reference points to an object of Cat and make the cast only if it is. You will not trigger an exception in this case. Figure 6-2 shows the output from the modified program.

Figure 6-2. Output after the code change in Example 6-2


  In C#,     Dog doggie = anAnimal as Dog;     if (doggie != null)     {         doggie.Bark();     }  is equivalent to     if (anAnimal is Dog)     {         Dog doggie = (Dog) anAnimal;         doggie.Bark();     } 

Both the as and is operators represent the use of RTTI. However, the as operator can only be used with reference types.


Is this better than using casting? Well, at least the exceptions go away. But what happens if you add another type of Animal to your system in the future, say a Horse? When an instance of Horse is sent to the TRain() method, it invokes the Eat() method, but not any of the other methods on Horse, for example Neighs(). If you ask the horse if it had a good time at the trainer, it will probably say, "The trainer fed me, then he asked if I was a dog and I said no. He then asked if I was a cat and I said no. Then he just walked away. He is not an equal opportunity trainer."

The code that uses RTTI in this manner is not extensible. It fails the Open-Closed Principle (OCP), which states that a software module must be open for extension but closed for modification. That is, you should be able to accommodate changes in the requirements by adding small new modules of code, not by changing existing code (see Gotcha #23, "Copy Constructor hampers exensibility").

While RTTI is better than casting, it is still bad. It is better to rely on polymorphism. You should abstract the methods of the derived class into the base class. In this example, the Bark() of Dog and the Meow() of Cat can be abstracted as, say, MakeNoise() in Animal. However, Animal doesn't know how to implement that method, so it's marked as abstract/MustOverride. This alerts derived classes that they are responsible for implementing it. The code in Example 6-3 shows these changes.

Example 6-3. Relying on abstraction and polymorphism

C# (RTTI)

 //Animal.cs public abstract class Animal {     public void Eat()     {         Console.WriteLine("Animal eating");     }     public abstract void MakeNoise(); } //Dog.cs public class Dog : Animal {     public void Bark()     {         Console.WriteLine("Dog barking");     }     public override void MakeNoise()     {         Bark();     } } //Cat.cs public class Cat : Animal {     public void Meow()     {         Console.WriteLine("Cat Meowing");     }     public override void MakeNoise()     {         Meow();     } } //Trainer.cs public class Trainer {     public void Train(Animal anAnimal)     {         anAnimal.Eat();         //Using Abstraction and Polymorphism         anAnimal.MakeNoise();     } } 

VB.NET (RTTI)

 'Animal.vb Public MustInherit Class Animal     Public Sub Eat()         Console.WriteLine("Animal eating")     End Sub     Public MustOverride Sub MakeNoise() End Class 'Dog.vb Public Class Dog     Inherits Animal     Public Sub Bark()         Console.WriteLine("Dog barking")     End Sub     Public Overrides Sub MakeNoise()         Bark()     End Sub End Class 'Cat.vb Public Class Cat     Inherits Animal     Public Sub Meow()         Console.WriteLine("Cat Meowing")     End Sub     Public Overrides Sub MakeNoise()         Meow()     End Sub End Class 'Trainer.vb Public Class Trainer     Public Sub Train(ByVal anAnimal As Animal)         anAnimal.Eat()         'Using Abstraction and Polymorphism         anAnimal.MakeNoise()     End Sub End Class 

Now the TRainer class relies on the polymorphic behavior of the MakeNoise() method. This code is far superior to using RTTI, as it's more extensible. It makes it easier to add different domestic animals who might seek education from this trainer.

Not all uses of RTTI are bad, though. For example, consider the case where an Animal has a Play() method and a Dog decides that it only likes playing with other Dogs. This code looks like Example 6-4.

Example 6-4. OK use of RTTI

C# (RTTI)

 //Animal.cs public abstract class Animal {         public void Eat()         {             Console.WriteLine("Animal eating");         }         public abstract void MakeNoise();         public abstract bool Play(Animal other); } //Dog.cs public class Dog : Animal {         public void Bark()         {             Console.WriteLine("Dog barking");         }         public override void MakeNoise()         {             Bark();         }         public override bool Play(Animal other) {     if (other is Dog)         return true;     else         return false; } } 

VB.NET (RTTI)

 'Animal.vb Public MustInherit Class Animal     Public Sub Eat()         Console.WriteLine("Animal eating")     End Sub     Public MustOverride Sub MakeNoise()     Public MustOverride Function Play(ByVal other As Animal) As Boolean End Class 'Dog.vb Public Class Dog     Inherits Animal     Public Sub Bark()         Console.WriteLine("Dog barking")     End Sub     Public Overrides Sub MakeNoise()         Bark()     End Sub     Public Overrides Function Play(ByVal other As Animal) As Boolean         If TypeOf other Is Dog Then     Return True         Else     Return False         End If     End Function End Class 

The use of RTTI in the Play() method is benign. It checks to see if Other refers to an instance of its own type. At this point, there is no extensibility issue if a Dog never wants to play with any animals other than Dogs. Of course, if the Dog changes its mind, you'll have to externalize the rule (maybe in a configuration file), or apply a solution like the Visitor pattern. (Refer to [Freeman04, Gamma95] for more details on patterns.) Those would eliminate the use of RTTI here as well.

IN A NUTSHELL

Use RTTI sparingly. Do not use it if you are checking against multiple types. Use it only if you do not violate the Open-Closed Principle.

SEE ALSO

Gotcha #23, "Copy Constructor hampers exensibility," Gotcha #43, "Using new/shadows causes "hideous hiding"," Gotcha #44, "Compilers are lenient toward forgotten override/overrides," Gotcha #45, "Compilers lean toward hiding virtual methods," Gotcha #46, "Exception handling can break polymorphism," and Gotcha #47, "Signature mismatches can lead to method hiding."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

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