Advanced Object-Oriented Concepts
Chapters 1, "An Introduction to Object-Oriented Concepts," and 2, "How to Think in Terms of Objects," cover the basics of object-oriented (OO) concepts. Before we embark on our journey to learn some of the finer design issues relating to building an OO system, we need to cover several more advanced OO concepts.
Some of these concepts might not be vital to understanding an OO design at a higher level, but they are necessary to anyone actually involved in the design and implementation of an OO system.
Constructors
Constructors are a new concept for people doing structured programming. Constructors do not normally exist in non-OO languages such as C and Basic. Earlier we spoke about special methods that are used to construct objects. In Java, C# and C++, as well as in other OO languages, constructors are methods that share the same name as the class and have no return type. For example, a constructor for the Cabbie class would look like this:
public Cabbie(){ /* code to construct the object */ }
The compiler will recognize that the method name is identical to the class name and consider the method a constructor.
Return Value
Note again that a constructor does not have a return value. If you provide a return value, the compiler will not treat the method as a constructor.
When Is a Constructor Called?
When a new object is created, one of the first things that happens is that the constructor is called. Check out the following code:
Cabbie myCabbie = new Cabbie();
The new keyword creates a new instance of the Cabbie class, thus allocating the required memory. Then the constructor is called, passing the arguments in the parameter list. The developer must do the appropriate initialization within the constructor.
Thus, the code new Cabbie() will instantiate a Cabbie object and call the Cabbie method, which is the constructor.
What's Inside a Constructor?
Perhaps the most important function of a constructor is to initialize the memory allocated when the new keyword is encountered. In short, code included inside a constructor should set the newly created object to its initial, stable, safe state.
For example, if you have a counter object with an attribute called count, you need to set count to zero in the constructor:
count = 0;
Initializing Attributes
In structured programming, a routine named housekeeping (or initialization) is often used for initialization purposes. Initializing attributes is a common function performed within a constructor.
The Default Constructor
If you write a class and do not include a constructor, the class will still compile and you can still use it. If the class provides no explicit constructor, such as in C++, C#, and Java, a default constructor will be provided. It is important to understand that at least one constructor always exists, regardless of whether you write a constructor yourself. If you do not provide a constructor, the system will provide a default constructor for you.
The default constructor calls only the constructor of the superclass. In many cases, the super class will be part of the language framework, like the Object class in Java. For example, if a constructor is not provided for the Cabbie class, the following default constructor is inserted:
public Cabbie(){ super(); }
In this case, if Cabbie does not explicitly inherit from another class, the Object class will be the parent class. Perhaps the default constructor might be sufficient in some cases; however, in most cases some sort of memory initialization should be performed.
Regardless of the situation, it is good programming practice to always include at least one constructor in a class. In any case, if there are attributes in the class, they should be initialized in a constructor.
Providing a Constructor
The rule of thumb is that you should always provide a constructor, even if you do not plan on doing anything inside it. You can provide a constructor with nothing in it and then add to it later. Although there is technically nothing wrong with using the default constructor provided by the compiler, it is always nice to know exactly what your code looks like.
Using Multiple Constructors
In many cases, an object can be constructed in more than one way. To accommodate this situation, you need to provide more than one constructor. For example, let's consider the Count class presented here:
public class Count { int count; public Count(){ count = 0; } }
On the one hand, we simply want to initialize the attribute count to count to zero. We can easily accomplish this by having a constructor initialize count to zero as follows:
public Count(){ count = 0; }
On the other hand, we might want to pass an initialization parameter that allows count to be set to various numbers:
public Count (int number){ count = number; }
This is called overloading a method (overloading pertains to all methods, not just constructors). Most OO languages provide functionality for overloading a method.
Overloading Methods
Overloading allows a programmer to use the same method name over and over, as long as the signature of the method is different each time. The signature consists of the method name and a parameter list (see Figure 3.1).
Thus, the following methods all have different signatures:
public void getCab(); // different parameter list public void getCab (String cabbieName); // different parameter list public void getCab (int numberOfPassengers);Figure 3.1 The components of a signature.
Signatures
Depending on the language, the signature may or may not include the return type. In Java and C#, the return type is not part of the signature. For example, the following methods would conflict even though the return types are different:
public void getCab (String cabbieName); public int getCab (String cabbieName);
The best way to understand signatures is to write some code and run it through the compiler.
By using different signatures, you can construct objects differently depending on the constructor used.
Using UML to Model Classes
Let's return to the database reader example we used earlier in Chapter 2. Consider that we have two ways we can construct a database reader:
Pass the name of the database and position the cursor at the beginning of the database.
Pass the name of the database and the position within the database where we want the cursor to position itself.
Figure 3.2 shows a class diagram for the DataBaseReader class. Note that the diagram lists two constructors for the class. Although the diagram shows the two constructors, without the parameter list, there is no way to know which constructor is which. To distinguish the constructors, you can look at the corresponding code listed below.
No Return Type
Notice that in this class diagram the constructors do not have a return type. All other methods besides constructors must have return types.
Here is a code segment of the class that shows its constructors and the attributes that the constructors initialize (see Figure 3.3):
Figure 3.3 Creating a new object.public class DataBaseReader { String dbName; int startPosition; // initialize just the name public DataBaseReader (String name){ dbName = name; startPosition = 0; }; // initialize the name and the position public DataBaseReader (String name, int pos){ dbName = name; startPosition = pos; }; .. // rest of class }
Note how startPosition is initialized in both cases. If the constructor is not passed the information via the parameter list, it is initialized to a default value, like 0.
How the Superclass Is Constructed
When using inheritance, you must know how the parent class is constructed. Remember that when you use inheritance, you are inheriting everything about the parent. Thus, you must become intimately aware of all the parent's data and behavior. The inheritance of an attribute is fairly obvious. However, how a constructor is inherited is not as obvious. After the new keyword is encountered and the object is allocated, the following steps occur (see Figure 3.4):
The first thing that happens inside the constructor is that the constructor of the class's superclass is called. If there is no explicit call to the superclass constructor, the default is called automatically.
Then each class attribute of the object is initialized. These are the attributes that are part of the class definition (instance variables), not the attributes inside the constructor or any other method (local variables). In the DataBaseReader code presented earlier, the integer startPosition is an instance variable of the class.
Then the rest of the code in the constructor executes.
The Design of Constructors
When designing a class, it is good practice to initialize all the attributes. In some languages, the compiler provides some sort of initialization. As always, don't count on the compiler to initialize attributes! In Java, you cannot use an attribute until it is initialized. If the attribute is first set in the code, make sure that you initialize the attribute to some valid conditionfor example, set an integer to zero.
Figure 3.4 Constructing an object.Constructors are used to ensure that the application is in a stable state (I like to call it a "safe" state). For example, initializing an attribute to zero, when it is intended for use as a denominator in a division operation, might lead to an unstable application. You must take into consideration the fact that a division by zero is an illegal operation. Initializing to zero is not always the best policy.
During the design, it is good practice to identify a stable state for all attributes and then initialize them to this stable state in the constructor.