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:
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.
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;
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.
Properties for the TDate Class
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.
Advanced Features of Properties
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:
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;
Adding Properties to Forms
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.
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
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.
|Copyright © 2004-2023 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide||