GOTCHA 23 Copy Constructor hampers exensibility


GOTCHA #23 Copy Constructor hampers exensibility

You use classes to model concepts in an object-oriented system and create instances of your classes throughout an application. You may be interested in making a copy of an object at runtime. How do you make such a copy? In C++, you don't have to do anything special; C++ gives you a default copy constructor, a constructor that takes an instance of the class as its parameter. But this is a mixed blessing (or is it a curse?). The default C++ copy constructor makes what is called a shallow copy; i.e., the contents of the source object are bit-wise copied into the other object. Deep copy is when not only the contents of an object are copied, but also the contents of the objects that this object refers to. A deep copy does not copy just one object; it copies a tree of objects. Whether you need a shallow copy or a deep copy depends on the relationship between the object and its contents. For instance, consider Example 3-7.

Example 3-7. A class with different relationships with its contents

C# (CopyingObjects)

 public class Person {     private int age;     private Brain theBrain;     private City cityOfResidence; } 

VB.NET (CopyingObjects)

 Public Class Person     Private age as Integer     Private theBrain as Brain     Private cityOfResidence as City End Class 

If you make a copy of a Person object, you most likely want the new person to have a separate Brain, but may want to refer to (share) the City of the other person. From the object modeling point of view, the person aggregates the Brain but associates with the City. Generally you want to deep-copy the aggregated object, but you may want to shallow-copy the associated object, or just set it to null/Nothing. At the code level, you use a reference to represent both aggregation and association. There is a semantic mismatch between the object model and how it is expressed in the language. There is no way for the compiler or the runtime to figure out whether an object is being associated or aggregated. You have to implement the logic to properly copy an object. Without it, any effort to do so is just a guess, and probably not correct.

This is the problem with the C++ approach. Unfortunately, C++ decided to err on the side of shallow copy. Instead of saying, "Hum, I have no idea how to make a copy so I won't even try," C++ decided, "Hum, I have no idea how to make a copy so I'll make a shallow copy."

.NET decided to err on the side of caution. It says "I can't possibly make a copy of an object without the programmer clearly specifying the intent." So .NET doesn't provide a default copy constructor.

Thus if you want to make a copy of an object, you just write your own copy constructor, right? Let's explore this further in Example 3-8.

Example 3-8. Writing a copy constructor

C# (CopyingObjects)

 //Brain.cs using System; namespace Copy {     public class Brain     {         public Brain() {}          public Brain(Brain another)         {             //Code to properly copy Brain can go here         }         public override string ToString()         {             return GetType().Name + ":" + GetHashCode();         }     } } //Person.cs using System; namespace Copy {     public class Person     {         private int theAge;         private Brain theBrain;         public Person(int age, Brain aBrain)         {             theAge = age;             theBrain = aBrain;         }          public Person(Person another)         {             theAge = another.theAge;              theBrain = new Brain(another.theBrain);         }         public override string ToString()         {             return "This is person with age " +                        theAge + " and " +                        theBrain;         }     } } //Test.cs using System; namespace Copy {     class Test     {         [STAThread]         static void Main(string[] args)         {             Person sam = new Person(1, new Brain());              Person bob = new Person(sam);             // You rely on the copy constructor of Brain             //to make a good deep copy             Console.WriteLine(sam);             Console.WriteLine(bob);         }     } } 

VB.NET (CopyingObjects)

 'Brain.vb Public Class Brain     Public Sub New()     End Sub      Public Sub New(ByVal another As Brain)         ' Code to properly copy Brain can go here     End Sub     Public Overrides Function ToString() As String         Return Me.GetType().Name & ":" & GetHashCode()     End Function End Class 'Person.vb Public Class Person     Private theAge As Integer     Private theBrain As Brain     Public Sub New(ByVal age As Integer, ByVal aBrain As Brain)         theAge = age         theBrain = aBrain     End Sub      Public Sub New(ByVal another As Person)         theAge = another.theAge          theBrain = New Brain(another.theBrain)         ' You rely on the copy constructor of Brain         ' to make a good deep copy     End Sub     Public Overrides Function ToString() As String         Return "This is person with age " & _             theAge & " and " & _             theBrain.ToString()     End Function End Class 'Test.vbModule Test     Sub Main()         Dim sam As New Person(1, New Brain)          Dim bob As Person = New Person(sam)         Console.WriteLine(sam)         Console.WriteLine(bob)     End Sub End Module 

This example has a Person class with theAge and theBrain as its members. Person has a constructor and a copy constructor. The Main() method in Test copies Person sam to Person bob. The output is shown in Figure 3-6.

Figure 3-6. Output from Example 3-8


When it prints the first Person (sam), the age is 1 and the Brain's hash code value is 1. When it prints the second Person (bob), which was copied from the instance sam, the age is 1 but the Brain's hash code is 2.

Generally speaking you should not use the hash code to determine identity. Even if the hash code values are the same, it does not mean the objects are identical. Here, however, since the hash code is different, you can infer that the objects are different. In reality you might use something like a GUID in each object to determine its uniqueness, or you could test the references to the Brain of the two objects to confirm that they are different. (The issues of dealing with the hash code and determining the identity of objects can get complicated. For good discussions on these topics refer to "Common Object Operations," "Equals vs. ==," and "Hashcode," in the section "on the web" in the Appendix.)


So, in Example 3-8 we created a copy of the Person with his own Brain. Have you solved the problem of properly copying the object? Not really, because the Person's copy constructor depends on the Brain class. It specifically creates an instance of Brain. What if you have a class that derives from Brain, as shown in Example 3-9?

Example 3-9. Incorrect copying

C# (CopyingObjects)

 //SmarterBrain.cs using System; namespace Copy {     public class SmarterBrain : Brain     {         public SmarterBrain()         {         }         public SmarterBrain(SmarterBrain another) : base(another)         {         }     } } //Test.cs using System; namespace Copy {     class Test     {         [STAThread]         static void Main(string[] args)         {             Person sam = new Person(1, new SmarterBrain());             Person bob = new Person(sam);             Console.WriteLine(sam);             Console.WriteLine(bob);         }     } } 

VB.NET (CopyingObjects)

 'SmarterBrain.vb  Public Class SmarterBrain     Inherits Brain     Public Sub New()    End Sub     Public Sub New(ByVal another As SmarterBrain)         MyBase.New(another)     End Sub End Class 'Test.vb Module Test     Sub Main()         Dim sam As New Person(1, New SmarterBrain)         Dim bob As Person = New Person(sam)         Console.WriteLine(sam)         Console.WriteLine(bob)     End Sub End Module 

SmarterBrain inherits from Brain. In the Main() method of Test you create an instance of SmarterBrain and send it to the Person object. The output after this enhancement is shown in Figure 3-7.

Figure 3-7. Output after change in Example 3-9


While the first Person instance (sam) has an instance of SmarterBrain, the copied instance (bob) is left with just a regular plain vanilla Brain. What went wrong? The Person's copy constructor is asking a new instance of Brain to be created regardless of the actual object referred to by theBrain. How about the fix in Example 3-10?

Example 3-10. A fix?

C# (CopyingObjects)

         public Person(Person another)         {             theAge = another.theAge;              if(another.theBrain is SmarterBrain)             {                 theBrain = new SmarterBrain(                     (SmarterBrain) another.theBrain);             }             else             {                 theBrain = new Brain(another.theBrain);             }         } 

VB.NET (CopyingObjects)

     Public Sub New(ByVal another As Person)         theAge = another.theAge          If TypeOf another.theBrain Is SmarterBrain Then             theBrain = New SmarterBrain( _                 CType(another.theBrain, SmarterBrain))         Else             theBrain = New Brain(another.theBrain)         End If     End Sub 

Here you have modified the copy constructor of the Person class to use Runtime Type Identification (RTTI). It seems to fix the problem.

But what do you think about this solution? Not exactly elegant, is it? Actually, it's awful. It requires Person, which aggregates Brain, to know about all the subclasses of Brain. (The upside of code like this is job security. You will be around forever fixing and tweaking it.)

As it stands, the Person class is not extensible for the addition of new types of Brains. It fails the Open-Closed Principle (OCP). Refer to [Martin03] for details on this and other object-oriented design principles.

Sidebar 4.1. The Open-Closed Principle (OCP)

The Open-Closed Principle (OCP), proposed by Bertrand Myers, states that a software module must be open for extension but closed for modification or change. In other words, you should not have to modify the code to make it extensible.


How can you fix the code so it makes a proper copy of the object? The correct option is the prototype pattern, which is based on abstraction and polymorphism [Freeman04, Gamma95]. You depend on a prototypical instance to create a copy. This is discussed in the next gotcha, "Clone() has limitations"

IN A NUTSHELL

Writing a public copy constructor leads to extensibility problems. You should not use a public copy constructor in C++, Java, and the .NET languages.

SEE ALSO

Gotcha #20, "Singleton isn't guaranteed process-wide," Gotcha #24, "Clone() has limitations," Gotcha #27, "Object initialization sequence isn't consistent," and Gotcha #28, "Polymorphism kicks in prematurely."



    .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