public interface widget { widget
public String path();
public void eval(String cmd);
public void pack(String s);
public void bind(handler h,String s);
public void bind(String p, handler h,String s);
public void configure(String cmd);
public void geometry(int x, int y);
public void xscroll(widget w);
public void yscroll(widget w);
public widget self(); // to define compound widgets
public void redirect(widget inner);
};
The function path delivers the path name of
a widget object.
Each widget created by Tk actually defines a
Tcl command associated with the
path name of the widget.
In other words, an actual widget may be regarded
as an object which can be asked to evaluate commands.
For example a widget `.b' may be asked to change
its background color by a Tcl command like
.b configure -background blue
The function eval
enables the programmer to apply Tcl commands
to the widget directly, as does the configure
command.
The function geometry sets the width and height of
the widget.
As an example look at the code for the drawing canvas
widget depicted in slide [drawing-canvas].
import hush.dv.api.event;
import hush.dv.widgets.canvas;
class draw extends canvas {
boolean dragging;
public draw(String path) {
super(path);
dragging = false;
bind(this);
}
public void press(event ev) {
dragging = true;
}
public void release(event ev) {
dragging = false;
}
public void motion(event ev) {
if (dragging)
circle(ev.x(),ev.y(),2,"-fill black");
}
};
The class draw has an instance variable dragging,
that reflects whether the user is actually drawing a figure.
If dragging is true, motions with the mouse
will result in small dots on the screen.
slide: Drawing canvas
A structural view of the draw class is given in slide [draw-structure].
The draw class is derived from a canvas, which is itself
(indirectly) derived from a handler class.
The handler class dispatches events to predefined
handler methods, such as press, motion and release.
For the draw class we must distinguish between
a handler and a canvas part.
The handler part is defined by the methods
press, release and motion.
The canvas part allows for drawing figures, such as a small circle.
slide: Drawing canvas
In slide [draw-interact] it is depicted how these
two parts interact when the user draws a figure.
Actions of the user result in events that activate
the handler.
Note that the UML sequence diagrams are not completely
adequate here, since it is difficult to express
information concerning the events and the state of the draw
instance.
Widgets may respond to events.
To associate an event with an action,
an explicit binding must be specified
for that particular widget.
Some widgets provide default bindings.
These may, however, be overruled.
The function bind is used to associate
handlers with events.
The first string parameter of
bind may be used to specify
the event type.
Common event types are,
for example,
ButtonPress, ButtonRelease
and Motion, which are the
default events for canvas widgets.
Also keystrokes may be defined as events,
for example Return,
which is the default event for
the entry widget.
The function may be
used to associate a handler object
with the default bindings
for the widget.
Concrete widgets may not override the bind
function itself, but must define the protected
function install.
Typically, the install function consists
of calls to bind
for each of the event types that is
relevant to the widget.
In addition, the widget class offers two
functions that may be used
when defining compound or mega widgets.
The function must by used
to delegate the invocation
of the eval, configure and bind functions
to the widget w.
The function gives access to the widget
to which the commands are redirected.
The function path will still deliver
the path name of the outer widget.
Calling redirect when creating the compound widget
class suffices for most situations.
However, when the default events must be changed or the
declaration
of a handler must take effect for several component widgets,
the function install must be redefined
to handle the delegation explicitly.
The drawtool application
slide: The drawtool interface
In this section we will look at the realization of simple drawing tool.
The example illustrates how to use the
hush library widgets, and
serves to illustrate
in particular
how to construct
compound widgets.
A structural view of the drawtool application is given
in slide [drawtool-structure].
slide: A (partial) class diagram
Usually, the various widgets constituting the user
interface are (hierarchically) related to each other,
such as in the drawtool application which contains
a canvas to display graphic elements,
a button toolbox for selecting the graphic
items and a menubar offering various options
such as saving the drawing in a file.
Widgets in Tk are identified by a path name.
The path name of a widget reflects its possible
subordination to another widget.
See slide [widget-hierarchy].
slide: Widget containment
Pathnames consist of strings separated by dots.
The first character of a path must be a dot.
The first letter of a path must be lower case.
The format of a path name may be expressed in BNF form as
path ::= '.' '.'string path'.'string
For example `.' is the path name of the root widget,
whereas `.quit' is the path name of a widget
subordinate to the root widget.
A widget subordinate to another widget
must have the path name of that widget
as part of its own path name.
For example, the widget `.f.m' may have a widget `.f.m.h'
as a subordinate widget.
Note that the widget hierarchy induced by the path names
is completely orthogonal to the widget class inheritance
hierarchy.
With respect to the path name hierarchy, when speaking of
ancestors we simply mean superordinate widgets.
Our drawing tool consists of a tablet,
which is a canvas with scrollbars
to allow for a large size canvas
of which only a part is displayed,
a menubar, having a File
and an Edit menu, and a toolbox,
which is a collection of buttons for
selecting from among the drawing facilities.
In addition, a help facility is offered.
slide: An interaction diagram
A typical interaction (or use case) with
drawtool is depicted in slide [drawtool-interact].
On selecting the circle menu entry (or toolbox button),
the circle handler is activated to assist
in the drawing of a circle. Details will be given
when discussing the tablet widget.
The toolbox component
As the first component of drawtool,
we will look at the toolbox.
The toolbox is a collection of buttons
packed in a frame.
import hush.dv.api.*;
import hush.dv.widgets.frame;
public class toolbox extends frame { toolbox
tablet tablet;
public toolbox(widget w, tablet t) {
super(w,"toolbox");
tablet = t;
new toolbutton(this,"draw");
new toolbutton(this,"move");
new toolbutton(this,"box");
new toolbutton(this,"circle");
new toolbutton(this,"arrow");
}
public int operator() {
tablet.mode(_event.arg(1)); // reset tablet mode
return OK;
}
};
Each button is an instance of
the class toolbutton.
import hush.dv.api.*;
import hush.dv.widgets.button;
public class toolbutton extends button { toolbutton
public toolbutton(widget w, String name) {
super(w,name);
text(name);
bind(w,name);
pack("-side top -fill both -expand 1");
}
};
When a toolbutton is created,
the actual button is given the name of the button
as its path.
Next, the button is given the name as its text,
the ancestor widget w is declared to be the
handler for the button and the button is packed.
The function text is a member function
of the class button, whereas both
handler and pack are common widget functions.
Note that the parameter name is used as
a path name, as the text to display, and as an argument
for the handler, that will be passed
as a parameter when invoking the handler object.
The toolbox class inherits from the frame widget class,
and creates a frame widget with a path relative to the widget parameter
provided by the constructor.
The constructor further creates the five toolbuttons.
The toolbox is both the superordinate widget
and handler for each toolbutton.
When the function of the toolbox is invoked
in response to pressing a button,
the call is delegated to the mode function
of the tablet.
The argument given to mode corresponds to
the name of the button pressed.
The definition of the toolbutton and toolbox
illustrates that a widget need not necessarily be its
own handler.
The decision, whether to define a subclass which is made
its own handler or
to install an external handler
depends upon what is considered the most convenient
way in which to access the resources needed.
As a guideline, exploit the regularity of the application.
The menubar component
The second component of our drawing tool is the menubar.
import hush.dv.api.widget;
public class menubar extends hush.dv.widgets.menubar { menubar
public menubar(widget w, tablet t, toolbox b) {
super(w,"bar");
configure("-relief sunken");
new FileMenu(this,t);
new EditMenu(this,b);
new HelpButton(this);
}
};
The class menubar, given above,
is derived from the hush widget menubar.
Its constructor requires an ancestor widget,
a tablet and a toolbox.
The tablet is passed as a parameter to the {\em file_menu},
and the toolbox to the {\em edit_menu}.
In addition, a {\em help_button} is created, which provides
online help in a hypertext format when pressed.
A menubar consists of menubuttons to which actual menus are attached.
Each menu consists of a number of entries,
which may possibly lead to cascaded menus.
The second button of the menubar is defined by the
EditMenu.
The EditMenu requires a toolbox and creates
a menubutton.
It configures the button and
defines a menu containing two entries,
one of which is a cascaded menu.
Both the main menu and the cascaded menu
are given the toolbox as a handler.
This makes sense only because
for our simple application
the functionality offered by
the toolbox and EditMenu coincide.
slide: Tablet
The tablet component
The most important component of our drawtool
application is defined by the tablet widget class
given below.
import hush.dv.api.*;
import hush.dv.widgets.*;
public class tablet extends canvas { tablet
int _mode;
canvas canvas;
handler[] handlers;
final int DRAW = 0;
final int MOVE = 1;
final int CIRCLE = 2;
final int BOX = 3;
final int ARROW = 5;
public tablet(widget w, String name, String options) {
super(w,name,"*");
handlers = new handler[12];
init(options);
redirect(canvas); // to delegate to canvas
bind(this); // to intercept user actions
handlers[DRAW] = new DrawHandler(canvas);
handlers[MOVE] = new MoveHandler(canvas);
handlers[BOX] = new BoxHandler(canvas);
handlers[CIRCLE] = new CircleHandler(canvas);
handlers[ARROW] = new ArrowHandler(canvas);
_mode = 0; // drawmode.draw;
}
public int operator() {
handlers [mode].dispatch(_event);
return OK;
}
public int mode(String s) {
int m = -1;
if ("draw".equals(s)) m = DRAW;
if ("move".equals(s)) m = MOVE;
if ("box".equals(s)) m = BOX;
if ("circle".equals(s)) m = CIRCLE;
if ("arrow".equals(s)) m = ARROW;
if (m >= 0) _mode = m;
return _mode;
}
void init(String options) {
widget root = new frame(path(),"-class tablet");
canvas = new canvas(root,"canvas",options);
canvas.configure("-relief sunken -background white");
canvas.geometry(200,100);
scrollbar scrollx = new Scrollbar(root,"scrollx");
scrollx.orient("horizontal");
scrollx.pack("-side bottom -fill x -expand 0");
scrollbar scrolly = new Scrollbar(root,"scrolly");
scrolly.orient("vertical");
scrolly.pack("-side right -fill y -expand 0");
canvas.pack("-side top -fill both -expand 1");
canvas.xscroll(scrollx); scrollx.xview(canvas);
canvas.yscroll(scrolly); scrolly.yview(canvas);
}
};
The various modes supported by the drawing tool are
enumerated as final constants.
The tablet class itself inherits from the canvas
widget class.
This has the advantage that it offers the full functionality
of a canvas.
In addition to the constructor and function,
which delegates the incoming event to the appropriate
handler
according to the {\em _mode} variable,
it offers a function mode, which sets the mode of the
canvas as indicated by its string argument,
and a function init that determines the creation
and geometrical layout of the component widgets.
As instance variables, it contains an integer {\em _mode}
variable and an array of handlers that contains
the handlers corresponding to the modes supported.
Although the tablet must act as a canvas,
the actual tablet widget is nothing but a frame
that contains a canvas widget as one of its components.
This is reflected in the invocation of the
canvas constructor (super).
By convention, when the options parameter is
instead of the empty string, no actual widget
is created but only an abstract widget,
as happens when calling the widget class constructor.
Instead of creating a canvas right away,
the tablet constructor creates a top frame,
initializes the actual component widgets,
and redirects the eval, configure and bind
invocations to the subordinate canvas
widget.
It then
binds itself to be its own handler,
which results in binding itself to be the handler for the
canvas component.
Note that reversing the order of calling redirect
and bind would be disastrous.
After that it creates the handlers for the various modes
and sets the initial mode to move.
The function takes care of dispatching calls
to the appropriate handler.
The dispatch function must be called to pass
the tk, argc and argv parameters.
The drawtool class
Having taken care of the basic components
of the drawing tool, that is the toolbox,
menubar and tablet widgets, all that remains
to be done is to define
a suitable {\em file_handler},
appropriate handlers for the
various drawing modes
and a {\em help_handler}.
We will skip these, but look at the
definition of the drawtool class instead.
In particular, it will be shown how we may grant
the drawtool the status of a veritable
Tk widget, by defining a
drawtool handler class
and a corresponding drawtool widget command.
import hush.dv.api.*;
import hush.dv.widgets.frame;
import hush.dv.widgets.canvas;
public class drawtool extends canvas { drawtool
widget root;
tablet tablet;
public drawtool() { System.out.println("meta handler created"); }
public drawtool(String p, String options) {
super(p,"*"); // create empty tablet
init(options);
}
public int operator() {
System.out.println("Calling drawtool:" + _event.args(0) );
String[] argv = _event.argv();
if ("self".equals(argv[1])) tk.result(self().path());
else if ("drawtool".equals(argv[0]))
create(argv[1],_event.args(2));
else if ("path".equals(argv[1])) tk.result(path());
else if ("pack".equals(argv[1])) pack(_event.args(2));
else self().eval( _event.args(1) ); // send through
return OK;
}
void create(String name, String options) {
drawtool m = new drawtool(name,options);
}
void init(String options) {
root = new frame(path(),"-class Meta");
frame frame = new frame(root,"frame");
tablet = new tablet(frame,"tablet",options);
toolbox toolbox = new toolbox(frame,tablet);
menubar menubar = new menubar(root,tablet,toolbox);
toolbox.pack("-side left -fill y -expand 0");
tablet.pack("-side left -fill both -expand 1");
menubar.pack();
frame.pack("-expand 1 -fill both");
redirect( tablet ); // the widget of interest
}
};
Defining a widget command involves three steps: (I)
the declaration of the binding between
a command and a handler,
(II) the definition of the
function, which actually defines a mini-interpreter,
and (III) the definition of the actual
creation of the widget and its declaration
as a Tcl/Tk command.
Step (I) is straightforward.
We need to define an empty handler, which will
be associated with the drawtool command
when starting the application.
The functionality offered by the
interpreter defined by the
function in (II) is kept quite simple,
but may easily be extended.
When the first argument of the call is drawtool,
a new drawtool widget is created as specified in (III),
except when the second argument is self.
In that case,
the virtual
path of the widget is returned, which is actually
the path of the tablet's canvas.
It is the responsibility of the writer of the script
that the self command is not addressed to the
empty handler.
If neither of these cases apply,
the function eval is invoked
for , with the remaining arguments flattened
to a string.
This allows for using the drawtool almost as an ordinary
canvas.
Canvas c = new DrawTool("draw","");
tk.bind("drawtool",c);
c.circle(20,20,20,"-fill red");
c.rectangle(30,30,70,70,"-fill blue");
c.pack();
In the program fragment above,
the Tcl command drawtool is declared,
with an instance of drawtool as its handler.
(It is assumed that the tk variable refers
to an instance of kit.)
In this way, the drawtool widget
is made available as a command when
the program is used as an interpreter.
In this case, the actual drawtool widget
is made the handler of the command,
to allow for a script to
address the drawtool by calling
drawtool self.
object-oriented programming
Guidelines for design
subsections:
Computing is a relatively young discipline.
Despite its short history, a number of styles
and schools promoting a particular style have emerged.
However, in contrast to other disciplines
such as the fine arts (including architecture)
and musical composition, there is no
well-established tradition of what is to be considered
as good taste with respect to software design.
There is an on-going and somewhat pointless debate as to
whether software design
must be looked at as an art or must be promoted
into a science. See, for example, [Knuth92] and [Gries].
The debate has certainly resulted in new technology
but has not, I am afraid, resulted in
universally valid design guidelines.
The notion of good design in the other
disciplines is usually implicitly defined by
a collection of examples of good design,
as preserved in museums or (art or music) historical works.
For software design, we are still a long way from
anything like a museum, setting the standards of good design.
Nevertheless, a compendium of examples of object-oriented
applications such as [Pinson90] and [Harmon93],
if perhaps not setting the standards
for good design, may certainly be instructive.
Development process -- cognitive factors
- model realize refine
Design criteria -- natural, flexible, reusable
- abstraction -- types
- modularity -- strong cohesion (class)
- structure -- subtyping
- information hiding -- narrow interfaces
- complexity -- weak coupling
slide: Criteria for design
The software engineering literature abounds
with advice and tools to measure the quality of
good design.
In slide [3-design-criteria], a number of the criteria commonly found
in software engineering texts is listed.
In software design, we evidently
strive for a high level of abstraction
(as enabled by a notion of types and a corresponding
notion of contracts),
a modular structure with strongly cohesive units
(as supported by the class construct),
with units interrelated in a precisely
defined way (for instance by a client/server or subtype
relation).
Other desirable properties are
a high degree of information hiding (that is narrowly
defined and yet complete interfaces)
and a low level of complexity (which may be achieved
with units that have only weak coupling, as supported
by the client/server model).
An impressive list, indeed.
Design is a human process, in which cognitive factors
play a critical role.
The role of cognitive factors is reflected
in the so-called fractal design process model
introduced in [JF88], which describes object-oriented
development as a triangle with bases labeled by
the phrases model, realize and refine.
This triangle may be iterated at each of the bases,
and so on.
The iterative view of software development
does justice to the importance of human understanding,
since it allows for a simultaneous understanding
of the problem domain and the mechanisms needed
to model the domain and the system architecture.
Good design involves taste.
My personal definition of good design would
certainly also involve cognitive factors
(is the design understandable?), including
subjective criteria such as is it pleasant
to read or study the design?
Individual class design
A class should represent a faithful model of a single concept,
and be a reusable, plug-compatible component
that is robust, well-designed and extensible.
In slide [class-design], we list a number of suggestions
put forward by [McGregor92].
Class design -- guidelines
- only methods public -- information hiding
- do not expose implementation details
- public members available to all classes -- strong cohesion
- as few dependencies as possible -- weak coupling
- explicit information passing
- root class should be abstract model -- abstraction
slide: Individual class design
The first two guidelines enforce the principle of
information hiding,
advising that only methods should be public and
all implementation details should be hidden.
The third guideline states a principle
of strong cohesion by requiring that
classes implement a single protocol
that is valid for all potential clients.
A principle of weak coupling is enforced by
requiring a class to have as few dependencies as possible,
and to employ explicit information passing
using messages instead of inheritance
(except when inheritance may be used in a type
consistent fashion).
When using inheritance, the root class should be
an abstract model of its derived classes,
whether inheritance is used to realize
a partial type or to define a specialization
in a conceptual hierarchy.
The properties of classes, including their interfaces
and relations with other classes, must be laid
down in the design document.
Ideally, the design document should present
a complete and formal description of the
structural, functional and dynamic aspects of the system,
including an argument showing that the various models
are consistent.
However, in practice this will seldom be realized,
partly because object-oriented design techniques
are as yet not sufficiently matured to allow
a completely formal treatment, and partly because
most designers will be satisfied with a non-formal
rendering of the architecture of their system.
Admittedly, the task of designing is already
sufficiently complex, even without the additional
complexity of a completely formal treatment.
Nevertheless, studying the formal underpinnings
of object-oriented modeling based on types and polymorphism
is still worthwhile, since it will sharpen the
intuition with respect to the notion of behavioral
conformance and the refinement of contracts,
which are both essential for
developing reliable object models.
And reliability is the key to reuse!
Inheritance and invariance
When developing complex systems or class libraries,
reliability is of critical importance.
As shown in section [contracts],
assertions provide a means by which to check the runtime
consistency of objects.
In particular, assertions may be used to check that
the requirements for behavioral conformance
of derived classes are met.
Invariant properties -- algebraic laws
class employee { employee
public:
employee( int n = 0 ) : sal(n) { }
employee* salary(int n) { sal = n; return this; }
virtual long salary() { return sal; }
protected:
int sal;
};
Invariant
k == (e->salary(k))->salary()
slide: Invariant properties as algebraic laws
Invariant properties are often conveniently
expressed in the form of algebraic laws that must
hold for an object.
Naturally, when extending a class by inheritance
(to define a specialization or refinement)
the invariants pertaining to the base class
should not be disrupted.
Although we cannot give a general guideline
to prevent disruption, the example discussed here clearly
suggests that hidden features should be carefully
checked with respect to the invariance properties
of the (derived) class.
The example is taken from [Bar92].
In \sliref{object-invariant}, we have defined a class employee.
The main features of an employee are the (protected)
attribute sal (storing the salary of an employee)
and the methods to access and modify the salary
attribute.
For employee objects,
the invariant (expressing that any amount k
is equal to the salary of an employee whose
salary has been set to k)
clearly holds.
Now imagine that we distinguish between ordinary employees
and managers by adding a permanent
bonus when paying the salary of a manager,
as shown in slide [hidden-bonus].
The reader may judge whether this example is realistic or not.
Problem -- hidden bonus
class manager : public employee { manager
public:
long salary() { return sal + 1000; }
};
Invariant
k =?= (m->salary(k))->salary()
slide: Violating the invariant
Then, perhaps somewhat to our surprise, we find that
the invariant stated for employees no longer
holds for managers.
From the perspective of predictable object behavior
this is definitely undesirable, since invariants
are the cornerstone of reliable software.
The solution to this anomaly is to make the assignment
of a bonus explicit, as shown in slide [explicit-bonus].
Solution -- explicit bonus
class manager : public employee { manager'
public:
manager* bonus(int n) { sal += n; return this; }
};
Invariant -- restored
k + n == ((m->salary(k))->bonus(n))->salary()
slide: Restoring the invariant
Now, the invariant pertaining to managers may be strengthened
by including the effects of assigning a bonus.
As a consequence, the difference in salary
no longer occurs as if by magic but is directly
visible in the interaction with a manager object, as it should be.
An objective sense of style
The guidelines presented by [LH89] were
among the first, and they still provide good advice
with respect to designing class interfaces.
Good Object-Oriented Design
- organize and reduce dependencies between classes
Client
-- A method m is a client of C if m calls a method of C
Supplier
-- If m is a client of C then C is a supplier of m
Acquaintance
-- C is an acquaintance of m if C is a supplier of m but not (the type of) an argument of m or (of) an instance variable
of the object of m
- C is a preferred acquaintance of m if an object of C is created in m or C is the type of a global variable
- C is a preferred supplier of m if C is a supplier and C is (the type of) an instance variable, an argument or a preferred acquaintance
slide: Clients, suppliers and acquaintances
In slide [Demeter], an explicit definition of the dual notions
of client and supplier has been given.
It is important to note that not all
of the potential suppliers for a class may
be considered safe.
Potentially unsafe suppliers are distinguished
as acquaintances, of which those that are either
created during a method call or stored in a global
variable are to be preferred.
Although this may not be immediately obvious,
this excludes suppliers that are
accessed in some indirect way, for instance
as the result of a method call to
some safe supplier.
As an example of using an unsafe supplier,
consider the call
screen->cursor()->move();
which instructs the cursor associated with the screen
to move to its home position.
Although screen may be assumed to be a safe supplier,
the object delivered by need
not necessarily be a safe supplier.
In contrast, the call
screen->move_cursor();
does not make use of an indirection
introducing a potentially unsafe supplier.
The guideline concerning the use of safe suppliers is known
as the Law of Demeter, of which the underlying
intuition is that the programmer should not be bothered
by knowledge that is not immediately apparent
from the program text (that is the class interface)
or founded in well-established conventions
(as in the case of using special global variables).
See slide [4-demeter].
Law of Demeter
ignorance is bliss
Do not refer to a class C in a method m unless C is (the type of)
1. an instance variable
2. an argument of m
3. an object created in m
4. a global variable
- Minimize the number of acquaintances!
Class transformations
- lifting -- make structure of the class invisible
- pushing -- push down responsibility
slide: The Law of Demeter
To remedy the use of unsafe suppliers,
two kinds of program transformation are suggested
by [LH89].
First, the structure of a class should be made invisible
for clients, to prohibit the use of a component
as (an unsafe) supplier.
This may require the lifting of primitive actions
to the encompassing object, in order to make these
primitives available to the client in a safe way.
Secondly, the client should not be given
the responsibility of performing
(a sequence of) low-level actions.
For example, moving the cursor should not
be the responsibility of the client of the screen,
but instead of the object representing the screen.
In principle, the client need not be
burdened with detailed knowledge
of the cursor class.
The software engineering principles underlying
the Law of Demeter may be characterized
as representing a compositional approach,
since the law enforces the use of immediate
parts only.
As additional benefits, conformance
to the law results in hiding the component
structure of classes, reduces the coupling of control
and, moreover, promotes reuse by
enforcing the use of localized (type)
information.
object-oriented programming
From specification to implementation
subsections:
Designing an object-oriented system
requires the identification of
object classes and the characterization of
their responsibilities,
preferably by means of contracts.
In addition, one must establish
the relationships between the object classes
constituting the system and
delineate the facilities the system offers to the user.
Such facilities are usually derived from
a requirements document and may be formally specified
in terms of abstract operations on the system.
In this section we will look at
the means we have available to express
the properties of our object model,
and we will study how we may
employ abstract specifications of system
operations to arrive at the integration
of user actions and the object model
underlying a system in a seamless way.
The approach sketched may be characterized
as event-centered.
Structural versus behavioral encapsulation
Object-oriented modeling has clearly been inspired
by or, to be more careful, shows significant
similarity to the method of semantic modeling
that has become popular for developing information systems.
In an amusing paper, [Ki89] discusses how semantic
modeling and object-oriented modeling are related.
Apart from a difference in terminology,
semantic modeling differs from object-oriented
modeling primarily by its focus on structural aspects,
whereas object-oriented modeling is more concerned with
behavioral aspects, as characterized
by the notion of responsibilities.
Typically, semantic modeling techniques provide
a richer repertoire for constructing types,
including a variety of methods for aggregation
and a notion of grouping by association.
See slide [3-semantic].
The object-oriented counterpart of aggregation
may be characterized as the has-a or part-of
relation, that is usually expressed by
including the (part) object as a data member.
Associations between objects cannot be expressed
directly in an object-oriented framework.
On an implementation level, the association
relation corresponds to membership of a common collection,
or being stored in the same container.
However, the absence of an explicit association
relation makes it hard to express general m-n relations,
as, for example, the relation between students and courses.
Object-oriented modeling
- is-a -- inheritance
- has-a, uses -- delegation
- uses -- templates
slide: Relations between objects
The influence of a semantic modeling background can be clearly
felt in the OMT method.
The object model of OMT is a rather direct generalization
of the entity-relationship model.
Entities in the entity-relationship model
may only contain (non-object) data members,
which are called attributes.
In contrast, objects (in the more general sense)
usually hide object and non-object data members,
and instead provide a method interface.
Moreover, object-oriented modeling focuses on
behavioral properties, whereas semantic modeling has been
more concerned with (non-behavioral) data types
and (in the presence of inheritance) data subtypes.
Relations, as may be expressed in the entity-relationship
model, can partly be expressed directly in terms
of the mechanisms supported by object-oriented
languages.
For instance, the is-a relation corresponds
closely (although not completely) with
the inheritance relation.
See slide [3-challenges].
Both the has-a and uses relation is
usually implemented by including (a pointer to)
an object as a data member.
Another important relation is the is-like relation,
which may exist between objects that are neither related
by the inheritance relation nor by the subtype relation,
but yet have a similar interface and hence may be regarded
as being of analogous types.
The is-like relation may be enforced by parametrized
types that require the presence of particular methods,
such as a compare operator in the case of a generic
list supporting a sort method.
Model-based specification
State and operations
Z
- $state == [ decls | constraints ]$
- $op == [ %D state; decls | constraints ]$
Change and invariance
- $ %D state == state /\ state' $
- $ %X state == state = state' $
Verification
- $ state /\ pre( op ) => op $
slide: Model-based specification
Several development methods,
including Responsibility Driven Design and Fusion (see section [Fusion]),
allow for the specification of
user interactions in a semi-formal
way by means of pre- and post-conditions.
These approaches have been inspired
by model-based specification methods
such as VDM and Z, which offer a formal
framework for specifying the requirements
of a system.
Model-based specification methods derive
their name from the opportunity to
specify a mathematical model
capturing the relevant features of the
system.
Operations, which may correspond
to user actions, can then be specified
in a purely logical way.
In the following, an outline of
the specification language Z will be given.
More importantly, the specification
of a simple library system will be discussed,
illustrating how we may specify
user actions in an abstract way.
(The use of the Z specification language
is in this respect only of subsidiary
importance.)
In the subsequent section, we will look
at the realization of the library
employing an abstract system of objects
and events corresponding to the user
actions, which reflects the characterization
given in the formal specification.
The specification language Z is based on classical
(two-valued) logic and set theory.
It has been used in a number of industrial projects,
[Hayes92], and to specify the architecture of
complex intelligent systems, [Craig91].
The central compositional unit of specification in Z
is the schema.
A schema may be used to specify both states and operations
in a logical way.
The logic employed in Z is a typed logic.
The specification of a schema consists of
a number of declarations followed by constraints
specifying conditions on the variables introduced
in the declarations.
Declarations may include other schemas, as
in the example specification of the operation op.
The schema $%D state$ itself is a compound schema
that results from the logical conjunction of the schema
state and its primed version $state'$,
which denotes state after applying op.
Both schema inclusion and schema conjunction are
examples of the powerful schema calculus supported by Z,
which enables the user to specify complex systems
in Z.
Moreover, schemas may be decorated to
specify the effects of an operation.
Invariance may be specified
as in $%X state$, which expresses that the
state before applying the operation
is the same as the state (denoted by $state'$)
after applying the operation.
Since schemas are specified in a logical manner,
both pre- and post-conditions are implicitly
specified by the constraints included in the schema.
Hence, to verify that an operation op is legal
for a state it is merely required to verify
that the conditions specified for state hold,
and that, together with the pre-conditions
(which are implicitly specified by the schema for op),
they imply the logical formula characterizing op.
See slide [10-model].
State
Counter
\begin{schema}{Counter}
n : \nat
\where
n \geq 0
\end{schema}
Operations
\begin{schema}{Incr}
\Delta Counter
\where
\mbox{ $ n' = n + 1 $ }
\end{schema}
\begin{schema}{Decr}
\Delta Counter
\where
n > 0 \\
\mbox{ $ n' = n - 1 $ }
\end{schema}
slide: The specification of a Counter in Z
An important property of Z
is that it allows for a graphical
layout of schemas, as illustrated
in the specification of a Counter
given in slide [z-ctr].
The state of a Counter is given by
the Counter schema declaring an integer
variable n, which is constrained
by the condition $ n \geq 0 $.
The operations Incr and Decr are specified
by defining the
state following the operation by,
respectively, $ n' = n + 1 $ and
$ n' = n - 1 $.
Both operations require the declaration
$%D Counter$ to indicate that the
state specified by Counter will
be modified.
In addition, the operation Decr requires
as a pre-condition that $n > 0$,
needed to prevent the
violation of the invariant, which
would happen whenever n became less
than zero.
Counter
Z
$Counter \defs [ n : \nat | n \geq 0 ] $
$Counter::Incr \defs [ \%D Counter, v? : \nat | n' = n + v? ]$
$Counter::Decr \defs [ \%D Counter | n > 0; n' = n - 1 ]$
$Counter::Value \defs [ \%X Counter; v! : \nat | v! = n ]$
Bounded counter
$Bounded::Counter \defs [ Counter | n \leq Max ]$
$Bounded::Incr \defs [ Counter::Incr | n < Max ]$
slide: An alternative specification of the Counter
An alternative specification of the
Counter is given in slide [z-ctr-2].
To emphasize that we may regard
the Counter as an object, the operations
have been prefixed by Counter in a
C++-like manner.
This is only a syntactic device, however,
carrying no formal meaning.
In addition, both the operations
Incr and Decr declare
an integer variable $v?$ which acts,
by convention, as an input parameter.
Similarly, the integer variable
$v!$ declared for the operation value
acts, again by convention, as an output
parameter.
Since Z allows the inclusion of
other schemas in the declaration part
of a schema, we may easily mimic
inheritance as illustrated
in the specification of
$Bounded::Counter$, which is a Counter
with a maximum given by
an integer constant $Max$.
Similarly, we may specify the operations
for the $Bounded::Counter$ by including
the corresponding operations
specified for the Counter, adding conditions
if required.
From a schema we may easily extract the
pre-conditions for an operation by removing from
the conditions the parts involving
a primed variable.
Clearly, the post-condition is then
characterized by the conditions thus eliminated.
For example, the pre-condition of the
$Counter::Incr$
operation is $v? \geq 0$, whereas
the post-condition is $n' = n + v?$
which corresponds to the implementation requirement
that the new value of the Counter
is the old value plus the value of the
argument $v?$.
In a similar way, the pre-condition
for applying the $Bounded::Incr$ operation
is $n + v? \leq Max$.
Note, however, that this pre-condition
is stronger than the original
pre-condition $v? \geq 0$, hence to conform
with the rules for refinement
we must specify what happens when
$ n + v? > Max $ as well.
This is left as an exercise for the reader.
Clearly, although Z lacks a notion
of objects or classes, it may conveniently
be employed to specify the behavior
of an object.
In [Stepney], a number of studies are collected which propose
extending Z with a formal notion of classes and inheritance.
The reader interested in these extensions is invited in particular
to study Object-Z, OOZE and Z++.
As an historical aside, we may note that Z has been of significant
influence in the development of Eiffel (see Meyer, 1992b).
Although the two approaches are quite divergent,
they obviously still share a common interest in correctness.
In contrast to Eiffel, which offers only
a semi-formal way in which to specify the behavior
of object classes, Z allows for
a precise formal specification of
the requirements a system must meet.
To have the specification reflect
the object structure of the system
more closely, one of the extensions
of Z mentioned above may be used.
An example of using (plain) Z
to specify the functionality
of a library system is given below.
The specification of a library
Imagine that you must develop
a program to manage a library,
that is keep a record of the books
that have been borrowed.
State
Library (1)
\begin{schema}{Library}
books : \power Book \\
borrowed : Book \pfun Person
\where
\dom borrowed \subseteq books
\end{schema}
slide: The specification of a library
Before developing a detailed object
model,
you may well reflect on
what user services the library must
provide.
These services include the borrowing
of a book, returning a book and asking
whether a person has borrowed any books,
and if so which books.
These operations are specified
by the schemas Borrow, Return
and Has in slide [z-lib-2].
Operations
Library (2)
\begin{schema}{Borrow}
\Delta Library; b? : Book; p? : Person
\where
b? \not\in \dom borrowed \\
b? \in books \\
borrowed' \mbox{ $ = $ } borrowed \cup { b? \mapsto p? }
\end{schema}
\begin{schema}{Return}
\Delta Library; b? : Book; p? : Person
\where
b? \in \dom borrowed \\
borrowed' \mbox{ $ = $ } borrowed \hide { b? \mapsto p? }
\end{schema}
\begin{schema}{Has}
\Xi Library; p? : Person; bks : \power Book
\where
bks! \mbox{ $ = $ } borrowed ^{-1} \limg { p? } \rimg
\end{schema}
slide: The library operations
Don't be frightened of the mathematical notation
in which these operations are specified.
The notation is only of secondary importance
and will be explained as we go along.
Since we are only interested in the abstract
relations between people and books, we may
assume Book and Person to be primitive types.
The specification given in slide [z-lib-1]
specifies an abstract state,
which is actually a partial function
delivering the person that borrowed
the book if the function is defined for
the book.
The function is partial to allow for the situation where
a book has not been borrowed,
but still lies on the shelves.
The invariant of the library system
states that the domain of
the function borrowed must be a subset
of the books available in the library.
Given the specification of the state, and some
mathematical intuition,
the specification of the operations
is quite straightforward.
When a Borrow action occurs, which
has as input a book $b?$ and a person $p?$,
the function $borrowed'$
is defined by extending borrowed
with the association between $b?$ and $p?$,
which is expressed as
the mapping $b? |-> p?$.
As a pre-condition for Borrow, we have that borrowed
must not be defined for $b?$, otherwise
some person would already have borrowed
the book $b?$.
The Return action may be considered
as the reverse of the Borrow action.
Its pre-condition states that borrowed
must be defined for $b?$
and the result of the operation
is that the association between
$b?$ and $p?$ is removed from
$borrowed '$.
Finally, the operation Has
allows us to query what books are in
the possession of a person $p?$.
The specification of Has employs
the mathematical features of Z
in a nice way.
The output, which is stored in
the output parameter $bks!$,
consists of all the books related
to the person $p?$.
The set of books related to $p?$ is obtained
by taking the relational image of the inversion
of borrowed for the singleton set
consisting of $p?$, that is,
each book x
for which
an association $ x |-> p? $ is in borrowed
is included in the set $bks!$.
Again, it is not the notation that is important
here, but the fact that the specification defines
all top-level user interactions.
Abstract systems and events
User actions may require complex interactions
between the objects constituting
the object model of a system.
Such interactions are often of
an ad hoc character in the sense that they embody one
of the many possible ways in which the functionality
of objects may be used.
What we need is a methodology or paradigm that
allows us to express these interactions in a concise
yet pragmatically amenable way.
In [Henderson93], a notion of abstract systems
is introduced that seems to meet our needs to
a large extent.
See slide [3-abstract].
Abstract systems -- design methodology
- abstract system = abstract data types + protocol
Events -- high level glue
- realization of the interaction protocol
slide: Abstract systems and events
Abstract systems
extend the notion of abstract data types
to capture the
(possible) interactions between collections of objects.
The idea underlying the notion of
an abstract system is to collect
the commands available for the client
or user of the system.
The collection of commands comprising
an abstract system are usually
a (strict) subset of the commands
available in the combined interface
of the abstract data types involved.
In other words, an abstract system
provides a restricted interface,
restricted to safeguard the user
from breaking the protocol of interaction
implicitly defined by the collection
of abstract data types of which the system consists.
An abstract system in itself merely provides
a guideline on how a collection of objects is to be used,
but does not offer a formal means to check whether
a user plays by the rules.
After presenting an example of an abstract
system, we will look at how events
may be used to protect the user against breaking
the (implicit) laws governing the interaction.
Example -- the library
The abstract system comprising a library may be characterized as
in slide [3-library].
In essence, it provides an exemplary interface, that is,
it lists the statements that are
typically used by a client of the library software.
We use typical identifiers to denote objects
of the various types involved.
Abstract system -- exemplary interface
library
p = new person();
b = new book();
p = b->borrower;
s = p->books;
tf = b->inlibrary();
b->borrow(p);
p->allocate(b);
p->deallocate(b);
b->_return(p);
For person* p; book* b; set<book>* s; bool tf;
slide: The library system
The commands available to the user of the
library software are constructors for a person
and a book,
an instruction to get access to the borrower
of a particular book, an instruction to ask what books
a particular person has borrowed,
an instruction to query whether a particular book
is in the library,
and instructions for a person to borrow or return a book.
To realize the abstract system library,
we evidently need the classes book and person.
The class book may be defined as follows.
class book { book
public:
person* borrower;
book() {}
void borrow( person* p ) { borrower = p; }
void _return( person* p ) { borrower = 0; }
bool inlibrary() { return !borrower; }
};
It consists of a constructor, functions to borrow
and return a book, a function to test
whether the book is in the library
and an instance variable containing the borrower of the book.
Naturally, the class book may be improved
with respect to encapsulation
(by providing a method to access the borrower)
and may further be extended to store additional
information, such as the title and publisher of the book.
class person { person
public:
person() { books = new set(); }
void allocate( book* b ) { books->insert(b); }
void deallocate( book* b ) { books->remove(b); }
set* books;
};
The next class involved in the library system
is the class person, given above.
The class person offers a constructor,
an instance variable to store the set of books borrowed
by the person and the functions allocate and deallocate
to respectively insert and remove the books
from the person's collection.
A typical example of using the library system is given below.
book* Stroustrup = new book(); example
book* ChandyMisra = new book();
book* Smalltalk80 = new book();
person* Hans = new person();
person* Cees = new person();
Stroustrup->borrow(Hans);
Hans->allocate(Stroustrup);
ChandyMisra->borrow(Cees);
Cees->allocate(ChandyMisra);
Smalltalk80->borrow(Cees);
Cees->allocate(Smalltalk80);
First, a number of books are defined,
then a number of persons,
and finally (some of) the books that are borrowed
by (some of) the persons.
Note that lending a book involves
both the invocation of $book::borrow$
and $person::allocate$.
This could easily be simplified by extending the function
$book::borrow$ and $book::_return$
with the statements $p->allocate(this)$
and $p->deallocate(this)$ respectively.
However, I would rather take the opportunity to illustrate the use
of events, providing a generic solution
to the interaction problem noted.
Events
[Henderson93] introduces events
as a means by which to control the complexity of relating
a user interface to the functionality provided
by the classes comprising the library system.
The idea underlying the use of events is that for
every kind of interaction with the user
a specific event class is defined that captures
the details of the interaction between the user
and the various object classes.
Abstractly, we may define an event as an entity
with only two significant moments in its life-span,
the moment of
its creation (and initialization)
and the moment of its activation
(that is when it actually happens).
As a class we may define an event as follows.
class Event {
public:
virtual void operator()() = 0;
};
The class $Event$ is an abstract class,
since the application operator that may
be used to activate the event is defined as zero.
class Borrow : public Event { Borrow
public:
Borrow( person* _p, book* _b ) { _b = b; _p = p; }
void operator()() {
require( _b && _p ); // _b and _p exist
_b->borrow(p);
_p->allocate(b);
}
private:
person* _p; book* _b;
};
For the library system defined above we may conceive of two
actual events (that is, possible refinements
of the $Event$ class),
namely a Borrow event and a Return event.
The Borrow event class provides a controlled
way in which to effect the borrowing of a book.
In a similar way, a Return event class may be defined.
class Return : public Event { Return
public:
Return( person* _p, book* _b ) { _b = b; _p = p; }
void operator()() {
require( _b && _p );
_b->_return(p);
_p->deallocate(b);
}
private:
person* _p; book* _b;
};
The operation Has specified in the
previous section has an immediate counterpart
in the $person::books$ data member and need
not be implemented by a separate event.
Events are primarily used
as intermediate between the user (interface)
and the objects comprising the library system.
For the application at hand, using events
may seem to be somewhat of an overkill.
However,
events not only give a precise characterization of
the interactions involved but, equally importantly,
allow for extending the repertoire
of interactions without disrupting
the structure of the application
simply by introducing additional event types.
object-oriented programming
Summary
object-oriented programming
This chapter looked at
application development.
We started with a simple example and
subsequently discussed guidelines for
class design.
We then looked at a more formal approach,
involving the transition from a formal
specification to the actual
implementation based on a notion of abstract systems
and events.
1
- drawing canvas -- in hush
- drawtool -- compound widgets
slide: Section 4.1: The drawtool application
In section 1 we looked at how to develop applications
in hush, as a typical
example of inplementing an interactive editor.
2
- individual class design
- establishing invariants
- an objective sense of style
slide: Section 4.2: Guidelines for design
In section 2, some guidelines
for design were presented. We looked at issues that may
arise when attempting to establish class invariants.
Finally, we discussed the rules imposed by
the Demeter method.
3
- structure versus behavior
- model-based specification
- abstract systems
slide: Section 4.3: From specification to implementation
In section 3, we discussed the distinction
between structural and behavioral aspects of
a system.
We looked at the application of
formal methods to specify the requirements for
a system, and we studied an implementation
based on abstract systems and events which was
derived from the original formal specification.
object-oriented programming
- Give an example of your choice to describe OO application development.
- Discuss possible guidelines for individual class design.
- Discuss how inheritance may affect class invariants.
- What would be your rendering of the Law of Demeter?
Can you phrase its underlying intuition? Explain.
- Define the notions of client, supplier and acquaintance.
What restrictions must be satisfied to speak of
a preferred acquaintance and a preferred supplier?
- Characterize the differences between semantic
modeling and object-oriented modeling.
- How would you characterize the notion of
abstract systems?
- Explain how events may be employed to
maintain system integrity. Give an example!
object-oriented programming
The original paper on hush is [HUSH].
A veritable catalogue of object-oriented
applications can be found in [Harmon93].
A classical paper on class design is [JF88].
For the Law of Demeter, consult [LH89].
The notion of abstract systems
was introduced in [Henderson93],
which also gives a good account of a
formal approach to object-oriented design.
For an introduction to formal methods and Z,
consult [Diller94].
For object-oriented extensions of Z, see [Stepney].
(C) Æliens
04/09/2009
You may not copy or print any of this material without explicit permission of the author or the publisher.
In case of other copyright issues, contact the author.