A class can have any amount of data and any number of methods. However, for a good object-oriented approach, data should be hidden, or encapsulated, inside the class using it. When you access a date, for example, it makes no sense to change the value of the day by itself. In fact, changing the value of the day might result in an invalid date, such as February 30. Using methods to access the internal representation of an object limits the risk of generating erroneous situations, because the methods can check whether the date is valid and refuse to modify the new value if it is not. Encapsulation is important because it allows the class writer to modify the internal representation in a future version.
The concept of encapsulation is often indicated by the idea of a "black box." You don't know about the internals: You only know how to interface with the black box or use it regardless of its internal structure. The "how to use" portion, called the class interface, allows other parts of a program to access and use the objects of that class. However, when you use the objects, most of their code is hidden. You seldom know what internal data the object has, and you usually have no way to access the data directly. Of course, you are supposed to use methods to access the data, which is shielded from unauthorized access. This is the object-oriented approach to a classical programming concept known as information hiding. However, in Delphi there is the extra level of hiding, through properties, as we'll see later in this chapter.
Delphi implements this class-based encapsulation, but it still supports the classic module-based encapsulation using the structure of units. Every identifier that you declare in the interface portion of a unit becomes visible to other units of the program, provided there is a uses statement referring back to the unit that defines the identifier. On the other hand, identifiers declared in the implementation portion of the unit are local to that unit.
For class-based encapsulation, the Delphi language has three access specifiers: private, protected, and public. A fourth, published, controls run-time type information (RTTI) and design-time information (as discussed in more detail in Chapter 4), but it gives the same programmatic accessibility as public. Here are the three classic access specifiers:
The private directive denotes fields and methods of a class that are not accessible outside the unit that declares the class.
The protected directive is used to indicate methods and fields with limited visibility. Only the current class and its inherited classes can access protected elements. More precisely, only the class, subclasses, and any code in the same unit as the class can access protected members, which opens up to a trick discussed in the sidebar "Accessing Protected Data of Other Classes (or, the "Protected Hack")" later in this chapter. We'll discuss this keyword again in the section "Protected Fields and Encapsulation."
The public directive denotes fields and methods that are freely accessible from any other portion of a program as well as in the unit in which they are defined.
Generally, the fields of a class should be private and the methods public. However, this is not always the case. Methods can be private or protected if they are needed only internally to perform some partial computation or to implement properties. Fields might be declared as protected so that you can manipulate them in inherited classes, although this isn't considered a good OOP practice.
Access specifiers only restrict code outside your unit from accessing certain members of classes declared in the interface section of your unit. This means that if two classes are in the same unit, there is no protection for their private fields.
As an example, consider this new version of the TDate class:
type TDate = class private Month, Day, Year: Integer; public procedure SetValue (y, m, d: Integer); overload; procedure SetValue (NewDate: TDateTime); overload; function LeapYear: Boolean; function GetText: string; procedure Increase; end;
You might think of adding other functions, such as GetDay, GetMonth, and GetYear, which return the corresponding private data, but similar direct data-access functions are not always needed. Providing access functions for each and every field might reduce the encapsulation and make it harder to modify the internal implementation of a class. Access functions should be provided only if they are part of the logical interface of the class you are implementing.
Another new method is the Increase procedure, which increases the date by one day. This calculation is far from simple, because you need to consider the different lengths of the various months as well as leap and non–leap years. To make it easier to write the code, I'll change the internal implementation of the class to Delphi's TDateTime type for the internal implementation. The class definition will change to the following (the complete code is in the DateProp example):
type TDate = class private fDate: TDateTime; public procedure SetValue (y, m, d: Integer); overload; procedure SetValue (NewDate: TDateTime); overload; function LeapYear: Boolean; function GetText: string; procedure Increase; end;
Notice that because the only change is in the private portion of the class, you won't have to modify any of your existing programs that use it. This is the advantage of encapsulation!
The TDateTime type is a floating-point number. The integral portion of the number indicates the date since 12/30/1899, the same base date used by OLE Automation and Microsoft Win32 applications. (Use negative values to express previous years.) The decimal portion indicates the time as a fraction. For example, a value of 3.75 stands for the second of January 1900, at 6:00 a.m. (three-quarters of a day). To add or subtract dates, you can add or subtract the number of days, which is much simpler than adding days with a day/month/year representation.
Properties are a very sound OOP mechanism, or a well-thought-out application of the idea of encapsulation. Essentially, you have a name that completely hides its implementation details. This allows you to modify the class extensively without affecting the code using it. A good definition of properties is that of virtual fields. From the perspective of the user of the class that defines them, properties look exactly like fields, because you can generally read or write their value. For example, you can read the value of the Caption property of a button and assign it to the Text property of an edit box with the following code:
Edit1.Text := Button1.Caption;
It looks like you are reading and writing fields. However, properties can be directly mapped to data, as well as to access methods, for reading and writing the value. When properties are mapped to methods, the data they access can be part of the object or outside of it, and they can produce side effects, such as repainting a control after you change one of its values. Technically, a property is an identifier that is mapped to data or methods using a read and a write clause. For example, here is the definition of a Month property for a date class:
property Month: Integer read FMonth write SetMonth;
To access the value of the Month property, the program reads the value of the private field FMonth; to change the property value, it calls the method SetMonth (which must be defined inside the class, of course).
Different combinations are possible (for example, you could also use a method to read the value or directly change a field in the write directive), but the use of a method to change the value of a property is common. Here are two alternative definitions for the property, mapped to two access methods or mapped directly to data in both directions:
property Month: Integer read GetMonth write SetMonth; property Month: Integer read FMonth write FMonth;
Often, the actual data and access methods are private (or protected), whereas the property is public. For this reason, you must use the property to have access to those methods or data, a technique that provides both an extended and a simplified version of encapsulation. It is an extended encapsulation because not only can you change the representation of the data and its access functions, but you can also add or remove access functions without changing the calling code. A user only needs to recompile the program using the property.
When you're defining properties, take advantage of the extended class completion feature of Delphi's editor, which you activate with the Ctrl+Shift+C key combination. After you write the property name, type, and semicolon, press Ctrl+Shift+C, and Delphi will provide you with a complete definition and the skeleton of the setter method. Write Get in front of the name of the identifier after the read keyword, and you'll also have a getter method with almost no typing.
As an example, I've added properties for accessing the year, the month, and the day to an object of the TDate class discussed earlier. These properties are not mapped to specific fields, but they all map to the single fDate field storing the complete date information. This is why all the properties have both getter and setter methods:
type TDate = class public property Year: Integer read GetYear write SetYear; property Month: Integer read GetMonth write SetMonth; property Day: Integer read GetDay write SetDay;
Each of these methods is easily implemented using functions available in the DateUtils unit (more details in Chapter 3, "The Run Time Library"). Here is the code for two of them (the others are very similar):
function TDate.GetYear: Integer; begin Result := YearOf (fDate); end; procedure TDate.SetYear(const Value: Integer); begin fDate := RecodeYear (fDate, Value); end;
The code for this class is available in the DateProp example. The program uses a secondary unit for the definition of the TDate class to enforce encapsulation and creates a single-date object that is stored in a form variable and kept in memory for the entire execution of the program. Using a standard approach, the object is created in the form OnCreate event handler and destroyed in the form OnDestroy event handler. The program form (see Figure 2.2) has three edit boxes and buttons to copy the values of these edit boxes to and from the properties of the date object.
When writing the values, the program uses the SetValue method instead of setting each of the properties. Assigning the month and the day separately can cause you trouble when the month is not valid for the current day. For example, suppose the day is currently January 31, and you want to assign to it February 20. If you assign the month first, this part of the assignment will fail, because February 31 does not exist. If you assign the day first, the problem will arise when doing the reverse assignment. Due to the validity rules for dates, it is better to assign everything at once.
Properties have several advanced features I'll focus on in future chapters. Specifically, in Chapter 4 I'll cover the TPersistent class, RTTI, and streaming and I'll discuss writing custom Delphi components in Chapter 9, "Writing Delphi Components." Here is a short summary of these more advanced features:
The write directive of a property can be omitted, making it a read-only property. The compiler will issue an error if you try to change the property value. You can also omit the read directive and define a write-only property, but that approach doesn't make much sense and is used infrequently.
The Delphi IDE gives special treatment to design-time properties, which are declared with the published access specifier and generally displayed in the Object Inspector for the selected component. You'll find more on the published keyword and its effect in Chapter 4.
An alternative is to declare properties, often called run-time only properties, with the public access specifier. These properties can be used in program code.
You can define array-based properties, which use the typical notation with square brackets to access an element of a list. The string list–based properties, such as the Lines of a list box, are a typical example of this group.
Properties have special directives, including stored and default, which control the component streaming system (introduced in Chapter 4 and detailed in Chapter 9).
You can usually assign a value to a property or read it, and you can even use properties in expressions, but you cannot always pass a property as a parameter to a procedure or method. This is the case because a property is not a memory location, so it cannot be used as a var or out parameter; it cannot be passed by reference.
One of the key ideas of encapsulation is to reduce the number of global variables used by a program. A global variable can be accessed from every portion of a program. For this reason, a change in a global variable affects the whole program. On the other hand, when you change the representation of a class's field, you only need to change the code of some methods of that class and nothing else. Therefore, we can say that information hiding refers to encapsulating changes.
Let me clarify this idea with an example. When you have a program with multiple forms, you can make some data available to every form by declaring it as a global variable in the interface portion of the unit of one of the forms:
var Form1: TForm1; nClicks: Integer;
This approach works, but the data is connected to the entire program rather than a specific instance of the form. If you create two forms of the same type, they'll share the data. If you want every form of the same type to have its own copy of the data, the only solution is to add it to the form class:
type TForm1 = class(TForm) public nClicks: Integer; end;
The previous class uses public data, so for the sake of encapsulation, you should instead change it to use private data and data-access functions. An even better solution is to add a property to the form. Every time you want to make some information of a form available to other forms, you should use a property, for all the reasons discussed in the section "Encapsulating with Properties." To do so, change the field declaration of the form (in the previous code) by adding the keyword property in front of it, and then press Ctrl+Shift+C to activate code completion. Delphi will automatically generate all the extra code you need.
The complete code for this form class is available in the FormProp example and illustrated in Figure 2.3. The program can create multi-instances of the form (that is, multiple objects based on the same form class), each with its own click count.
Notice that adding a property to a form doesn't add to the list of the form properties in the Object Inspector.
In my opinion, properties should also be used in the form classes to encapsulate the access to the components of a form. For example, if you have a main form with a status bar used to display some information (and with the SimplePanel property set to True) and you want to modify the text from a secondary form, you might be tempted to write
Form1.StatusBar1.SimpleText := 'new text';
This is a standard practice in Delphi, but it's not a good one, because it doesn't provide any encapsulation of the form structure or components. If you have similar code in many places throughout an application, and you later decide to modify the user interface of the form (for example, replacing StatusBar with another control or activating multiple panels), you'll have to fix the code in many places. The alternative is to use a method or, even better, a property to hide the specific control. This property can be defined as
property StatusText: string read GetText write SetText;
with GetText and SetText methods that read from and write to the SimpleText property of the status bar (or the caption of one of its panels). In the program's other forms, you can refer to the form's StatusText property; and if the user interface changes, only the setter and getter methods of the property are affected.
See Chapter 4 for a detailed discussion of how you can avoid having published form fields for components, which will improve encapsulation. But don't rush there: The description requires a good knowledge of Delphi, and the technique discussed has a few drawbacks.