Updatable variables and assignment commands are the essence of imperative programming. There are two basic reasons for using variables:
The first use can be avoided; the second use of updatable variables is much more fundamental ("Temporary variables are inessential").
Therefore, instead of writing x := plus(x,y) we write, e.g.,
send x "add y"or simply
x.add(y)This calls the add method of the object named x. Every method has an unwritten (implicit) first argument, which is the object itself.
We say that an object is an instance of a class. The operations of an object typically have side-effects, so they are not pure functions.
The class is the repository for behavior associated with an object, i.e., all objects that are instances of the same class can perform the same actions.
Classes are organized into a tree called the class hierarchy (or inheritance hierarchy). Memory and behavior associated with instances of a class are automatically available to any descendent class (= direct or indirect subclass).
Thus, in the OO paradigm, instead of writing procedures that work on data structures (as in traditional imperative programming), operations (methods) and data are "glued together" a viewed as a unit providing some computation service.
class financial-history = begin state cash, receipts, expenses method init() = cash := 0; receipts := []; expenses := []; method receive(amount) = receipts := append(amount,receipts); cash := cash + amount; method spend(amount) = expenses := append(amount, expenses); cash := cash - amount; method cash() = return cash; end;Individual objects can be declared in the same way as variables:
var myaccount: financial-history;
A class introduces a collection of operations which are imperative. Methods modify objects which have an internal, updatable state.
In contrast, an ADT introduces a new type. Any operation on a value of the new type must take the value as an input parameter. With a pure ADT, there are no operations that modify values of the type. Instead, some operations can generate "fresh", new values of the type.
Classes are typically not pure, because objects can be updated, i.e. modified. However programming with objects is still safer than programming with regular composite types, because updating can only be done through predefined methods. ...
class tax-history parent financial-history = begin state deductions; method init() = deductions := []; method deductible-spend(amount) = deductions := append(amount,deductions); self.spend(amount); // = send self spend(amount) end;Every object of type tax-history has three financial-history state variables, plus one new state variable. It can respond to all financial-history messages, and some additional messages.
Thus, instead of duplicating the effort and copying the state variables and methods from financial-history for the class tax-history, we instead inherit them (since they apply for tax-history as well) and thereby reuse the code of the parent class!
Q: What are the advantages of this approach?
Here is another example showing the distinction in Java:
class Rectangle { // "state", "instance variables", "fields": private int height, width; String desc = "a Rectangle"; ... // constructors Rectangle() { height = 10; width = 10; ... } Rectangle(int height, int width, ...) throws IllegalValueException { if (height < 0) throw new IllegalValueException(); ... this.height = height; this.width = width; ... } //methods public int getArea() { return height*width } public void draw () { ...} ... }Now assume you want to define a class ColorRect having also some color. Under the object composition approach, you could write:
class ColorRect { public Rectangle rect; public Color color; ... }Observe that now every ColorRect object has-a Rectangle object as part of it (hence this is also called part-of relationship).
In contrast, using inheritance we would write
class ColorRectangle extends Rectangle { Color color; // not private/protected/public => same package String desc = "Colored Rectangle"; // "shadows" the parent's field ... // constructor; called after the Rectangle constructor ColorRectangle() { color = new Color(); } // methods ... inherited from parent or overridden: public void draw() { ... // now the colorful stuff } }In the case of inheritance (ColorRectangle) we can reuse the method e.g. getArea() from Rectangle; in the case of ColorRect we have to address the subobject:
... ColorRectangle cr = new ColorRectangle(); ColorRect cr1 = new ColorRect(); ... cr.getArea() ; // OK cr1.getArea() ; // ERROR cr1.rect.getArea() ; // OKWhether to use object composition (has-a) or inheritance (is-a) is not always a trivial decision...
Ways to deal with multiple inheritance include disallowing it in the first place, resolving the naming conflicts, or giving some parent class priority.
One method of an object can call another of the same object if necessary. The name self (or this) refers to the current object, which is the implicit first argument of each method.
Calls to a self-method should refer to the method with the given name in the child class. This allows child class methods to override parent methods.
For example, sending the message id to an object of class D with the following class definitions should give I'm a type D object:
class C = begin method whoami = return "I'm a type C object"; method id = print (self.whoami) end; class D parent C = begin method whoami = return "I'm a type D object"; end;
Under standard static scoping, the name whoami in the body of the method C.id would refer to the method C.whoami. This is not what we want.
Method names should follow dynamic binding: the method corresponding to a name should be found in the runtime environment, not in the compile-time environment.
A powerful feature of dynamic binding is that for polymorphic types, the (runtime) object type (and sometimes even together with the types of the arguments) is used to invoke the correct method.
Thus, since subclasses can override methods of parent classes, polymorphism (of methods/behavior) is achieved: the program code can call/send a message and the correct method is determined at runtime, depending on the actual type (class membership) of the object to which the message is sent.
Object identity is connected to an "object-centric" view of programming: every operation is about some primary object. Syntactically, every method has an unwritten (implicit) first argument, which is the object itself.
obj.method(arg_1, arg_2, ..., arg_n)corresponds to
method(obj, arg_1, arg_2, ..., arg_n)In this way, the class of the host object can also be considered when selecting which of the overloaded methods is being called!
According to this view, the object itself is really a dispatcher: it waits for a message and then selects one of its methods for execution based on which message was received. A class is a function that can generate new objects. For example:
type messages = (next, restart); function newgenerator (initial:real) = begin var seed: real := initial; return function (m: messages) = begin case m of next: seed := transform(seed); restart: seed := initial; end; return seed; end; end; function myrandom = newgenerator(1.2345); function hisrandom = newgenerator(0.1428);
Here newgenerator is a higher-order function that returns another function, which is anonymous. Each returned function is a pseudo-random number generator (PRNG) that can respond to two alternative messages.
The updated PRNG seed is kept in the variable seed which is local
to newgenerator so seed is fresh each time that newgenerator is
called. However seed is global to the anonymous function so each
version of seed keeps its value between calls to the corresponding
individual PRNG.
begin C; exception when e_1 => C_1; when e_2 => C_2; ... end;If the exception e_i is raised while executing the command C, then C is abandoned and C_i is executed instead.
If a recovery command is not provided for an exception, then the exception is propagated to the next enclosing block. For example:
begin begin C; exception when e1 => C1; end; exception when e1 => C1'; when e2 => C2; ... end;In the code above, C1' will never be executed, but C2 may be.
procedure main is type month is (jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec); rainfall: array (month) of float; negative_rainfall: exception; procedure input_data is begin for amonth in month loop begin get(rainfall(amonth)); if rainfall(amonth) < 0.0 then raise negative_rainfall exception when data_error => rainfall(amonth) := 0.0; when negative_rainfall => rainfall(amonth) := 0.0; end; end loop; end; begin ... exception when end_error => put("Insufficient data"); when others => put("Unknown catastrophic error"); end
The language Eiffel enforces a rule of good programming style, that the only ways to leave an exception-handler are
The use of exceptions in the Ada example above does not meet the Eiffel criteria.
In general, there are two main opinions on programming style in
the presence of errors. The first attitude is that code should be
written defensively, and as many errors as possible should be
handled, even if they cannot be fixed completely.
The second
attitude is that errors should be revealed as quickly as
possible, so that they can be fixed properly.
For example, according to the first attitude, one should write
for (i = 0; i <= 10; i++) { ... }According to the second attitude one should write
for (i = 0; i != 10; i++) { ... }Suppose that the code inside the loop erroneously modifies i to have a value greater than 10. The first approach will hide this error, while the second will reveal it to the user, by causing an infinite loop.