Re: How does one model behavior?

From: Miguel Oliveira e Silva <mos_at_ua.pt>
Date: Wed, 09 Apr 2008 18:24:55 +0100
Message-ID: <47FCFBE7.FC18958E_at_ua.pt>


David Cressey wrote:

> (...)

> Sorry. I don't know enough OO to provide enlightenment. I understand data
> pretty well.
>
> Perhaps you could tell me how you express "what a class has to do".

In OO, a class is (or at least, it should be!) an Abstract Data Type (possibly partial) implementation (an ADT is a type which is completely defined by its public operations and their semantics).

Hence, the behavior of a class is specified by its ADT.

In a pure OO world one manages "data" *only* through the operations (functions or methods, if you will) which are applicable to it.

> This might be close to what I'm asking for when I say "how to you model
> behavior".

Contracts attached to ADTs:

  • class invariants, and;
  • method preconditions and postconditions.

For instance, the behavior of a generic class STACK could be approximated by the following class (using Eiffel syntax):

  • This is a line comment!

deferred class STACK[E] -- a deferred class cannot be instantiated

feature -- exported to everyone

  count: INTEGER is

  • Number of elements in STACK deferred -- the body implementation is deferred to descendant classes! end;

  empty: BOOLEAN is

  • Is STACK empty? do Result := count = 0 -- no need to defer this implementation to descendants end;

  full: BOOLEAN is

  • Is STACK full? deferred end;

  top: E is

  • STACK's last pushed element require -- precondition not empty deferred ensure -- postcondition same_count: count = old count end;

  push(elem: E) is
    require
      not full
    deferred
    ensure

      not empty;
      one_more:   count = old count + 1;
      element_placed_in_the_top:   top = elem;
      correct_old_top:   old empty or else under_top = old top
    end;

  pop is -- procedure (Eiffel promotes strict query/command separation)     require
      not empty
    deferred
    ensure

      not full;
      one_less:   count = old count - 1;
      correct_new_top:   empty or else top = old under_top
    end;

feature {STACK} -- exported only to STACK itself

  under_top: E is

  • Stack's pushed element before top require at_least_two_elements: count >= 2 deferred ensure same_count: count = old count end;

invariant

  (empty and count = 0) or (not empty and count > 0)

end -- STACK

As you can verify there are no explicit data declarations, other than possible function arguments and results (only operations, whose behavior is specified and depends on each other).

This stack specification and construction is also not linked to a possible (state based) internal representation (array, linked list, or any other).
In OO, other than for object creation, a client of a stack may rely only on this interface to manipulate stacks (dynamic binding, together with subtype polymorphism, allows us to put any object of a type descendant of STACK, where a STACK is expected.

Also, although a push message sent to a STACK object carries a "data"(*) element of type E with it, the stack's semantics does not depend, in any way, on the semantics of such element (that is why we can use unbounded parametric polymorphism in the stack class).

(*) is fact, in a pure OO world this is also an object (an instance of a (partial) ADT implementation).

It is also important to note that in Eiffel contracts (invariants, preconditions and postconditions) are inherited in descendant classes, so a STACK descendant is *required* to obey STACK's specification. Hence this (deferred) class defines the behavior of all its possible descendants (STACK_ARRAY[E], STACK_LINKED_LIST[E], etc.).

Best regards,

-miguel Received on Wed Apr 09 2008 - 19:24:55 CEST

Original text of this message