I2E: Classes
A class, it was said above, is an implementation of an abstract data type. This means that it describes a set of run-time objects, characterized by the features (operations) applicable to them, and by the formal properties of these features.
Such objects are called the direct instances of the class. Classes and objects should not be confused: "class" is a compile-time notion, whereas objects only exist at run time. This is similar to the difference that exists in classical programming between a program and one execution of that program, or between a type and a run-time value of that type.
To see what a class looks like, let us look at a simple example, ACCOUNT
, which describes bank accounts. But before exploring the class itself it is useful to study how it may be used by other classes, called its clients.
A class X
may become a client of ACCOUNT
by declaring one or more entities of type ACCOUNT
. Such a declaration is of the form:
acc: ACCOUNT
The term "entity" generalizes the more common notion of "variable". An entity declared of a reference type, such as acc
, may at any time during execution become " attached to " an object; the type rules imply that this object must be a direct instance of ACCOUNT
-- or, as seen below, of a "descendant" of that class.
An entity is said to be void if it is not attached to any object. By default, entities are void at initialization. To obtain objects at run-time, a routine r
appearing in the client class X
may use a creation instruction of the form
create acc
which creates a new direct instance of ACCOUNT
, attaches acc
to that instance, and initializes all its fields to default values. A variant of this notation, studied below, makes it possible to override the default initializations.
Once the client has attached acc
to an object, it may call on this object the features defined in class ACCOUNT
. Here is an extract with some feature calls using acc
as their target:
acc.open ("Jill")
acc.deposit (5000)
if acc.may_withdraw (3000) then
acc.withdraw (3000)
print (acc.balance)
end
These feature calls use dot notation, of the form target_name.feature_name
, possibly followed by a list of arguments in parentheses. Features are of two kinds:
- Routines, such as
open
,deposit
,may_withdraw
,withdraw
, represent computations applicable to instances of the class. - Attributes represent data items associated with these instances.
Routines are further divided into procedures (commands, which do not return a value) and functions (queries, returning a value). Here may_withdraw
is a function returning a boolean; the other three-routines called are procedures.
In class ACCOUNT
, is feature balance
an attribute, or is it a function with no argument? The above extract of the client class X
doesn't say, and this ambiguity is intentional. A client of ACCOUNT
must not need to know how class ACCOUNT
delivers an account's balance when requested: by looking up a field present in each account object, or by calling a function that computes the balance from other fields. Choosing between these techniques is the business of class ACCOUNT
, not anybody else's. Because such implementation choices are often changed over the lifetime of a project, it is essential to protect clients against their effects. This is known as the Uniform Access Principle, stating that the choice between representing a property through memory (an attribute) or through an algorithm (function) shall not affect how clients use it.
So much for how client classes will typically use ACCOUNT.
Below is a first sketch of how class ACCOUNT
itself might look. Line segments beginning with --
are comments. The class includes two feature
clauses, introducing its features. The first begins with just the keyword feature
, without further qualification; this means that the features declared in this clause are available (or "exported") to all clients of the class. The second clause is introduced by feature {NONE}
to indicate that the feature that follows, called add
, is available to no client. What appears between the braces is a list of client classes to which the corresponding features are available; NONE
is a special class of the Kernel Library, which has no instances, so that add
is in effect a secret feature, available only locally to the other routines of class ACCOUNT
. So in a client class such as X
, the call acc.add ( -3000 )
would be invalid. class
ACCOUNT
feature
balance: INTEGER
owner: PERSON
minimum_balance: INTEGER = 1000
open (who: PERSON)
-- Assign the account to owner who.
do
owner := who
end
deposit (sum: INTEGER)
-- Deposit sum into the account.
do
add (sum)
end
withdraw (sum: INTEGER)
-- Withdraw sum from the account.
do
add (-sum)
end
may_withdraw (sum: INTEGER): BOOLEAN
-- Is there enough money to withdraw sum?
do
Result := (balance >= sum + minimum_balance)
end
feature {NONE}
add (sum: INTEGER)
-- Add sum to the balance
do
balance := balance + sum
end
end -- ACCOUNT
Let us examine the features in sequence. The do
...
end
distinguishes routines from attributes. So here the class has implemented balance
as an attribute, although, as noted, a function would also have been acceptable. Feature owner
is also an attribute.
The language definition guarantees automatic initialization, so that the initial balance of an account object will be zero after a creation instruction. Each type has a default initial value: zero for INTEGER
and REAL
, false for BOOLEAN
, NUL character for CHARACTER
, and a void reference for reference types. The class designer may also provide clients with different initialization options, as will be seen below in a revised version of this example.
The other public features, withdraw, deposit, open,
and may_withdraw
are straight-forward routines. The special entity Result
, used in may_withdraw
, denotes the function result; it is initialized on function entry to the default value of the function's result type. You may only use Result
in functions.
The secret procedure add
serves for the implementation of the public procedures deposit
and withdraw
; the designer of ACCOUNT
judged it too general to be exported by itself. The clause "= 1000
" introduces minimum_balance
as a constant attribute, which will not occupy any space in instances of the class; in contrast, every instance has a field for every non-constant attribute such as balance
.
In Eiffel's object-oriented programming style any operation is relative to a certain object. A client invoking the operation specifies this object by writing the corresponding entity on the left of the dot, as acc
in acc.open ("Jill")
. Within the class, however, the "current" instance to which operations apply usually remains implicit, so that unqualified feature names, such as owner
in procedure open
or add
in deposit
, mean "the owner
attribute or add
routine relative to the current instance".
If you need to denote the current object explicitly, you may use the special entity Current
. For example the unqualified occurrences of add
appearing in the class text above are equivalent to Current
. add
.
In some cases, infix or prefix notation will be more convenient than dot notation. For example, if a class VECTOR
offers an addition routine, most people will feel more comfortable with calls of the form v + w
than with the dot-notation call v.plus (w)
. To make this possible it suffices to give the routine a "+
" alias. The operation is still a normal routine call which can be invoked with either the infix form or the dot-notation.
The above simple example has shown the basic structuring mechanism of the language: the class. A class describes objects accessible to clients through an official interface comprising some of the class features. Features are implemented as attributes or routines; the implementation of exported features may rely on other, secret ones.