Principles of Object-Oriented Software Development
[] readme course preface 1 2 3 4 5 6 7 8 9 10 11 12 appendix lectures resources

talk show tell print

Software architecture

To get an overall idea of the structure of a software system is intrinsically difficult. The notion of architecture has proven to be a powerful metaphor for describing the structure of a system, that is the components and their interrelations, in a sufficiently abstract way.


Software architecture

7


Additional keywords and phrases: components, information architecture, multimedia information retrieval, feature detection, portability


slide: Software architecture

In this chapter we will explore the notion of software architecture. We will first look at some definitions. As a preliminary to some technical explorations that illustrate a variety of ways to couple heterogeneous components, we will look at a case study involving a framework for multimedia feature detection, which is to be used for the indexing and retrieval of multimedia objects on the Web. In particular we will look at how to deploy embedded logic for managing meta-information and knowledge, and how to define corresponding collections of objects across language boundaries. As an example, we will discuss the Java and C++ coupling in hush in some detail. Finally, we will discuss some architectural patterns and styles, as well as some solutions for cross-platform development.

Elements of architecture

Software architecture has become an area of research in its own right. The seminal work of  [Shaw96] introduced the notion of software architecture as a means to describe how the various elements of a software system interact to achieve some computational goal. For example, at a high level we can distinguish between a pipe-lined architecture, common to many compilers, and event-driven computation, as it occurs for example in GUI-based systems.

Elements of architecture

 [Wolf]


Models and views

 [Kruchten95]


  • logical -- functional requirements
  • process -- performance, availability, distribution
  • physical -- scalability, configuration
  • development -- organization of software modules
  • scenarios -- instances of use cases

Definitions


slide: Elements of architecture -- models and views

In the definition given in  [Wolf], a software architecture is described as consisting of processing elements, which operate on data, and data elements, which somehow contain the information being processed. In addition there are connection elements that glue the processing and data elements together. Such an abstract view allows for describing a software system at a high level of abstraction and to indicate choice points and alternatives.

A later definition, given in  [Kruchten95], makes a distinction between the levels of abstraction, or points of view, from which a description of a system is possible. It distinguishes between a logical view, which captures the functional requirements, a process view, which indicates non-functional aspects such as performance, availability and distribution, a physical view, which deals with issues such as scalability and configuration, and a development view, which describes the organization of the software modules. In addition,  [Kruchten95] distinguishes a scenario view, which may be used for formulating tests based on properly instantiated use cases. The scenario view may be regarded as orthogonal to the logical, process, physical and development models since it does not affect the structure of the system itself, but rather the way the structure is validated against proper usage tests.

An exhaustive list of definitions of the notion of software architecture is given at the Web site of the Software Engineering Institute (SEI), of which the url is given in slide Elements.

At the time of writing, the most comprehensive book concerning software architectures is  [Practice]. As a definition it proposes:

The software architecture of a program or computing system is the structure of the system, which comprises software components, the externally visible properties of those components, and their interrelationships.

Note the stress on externally visible properties here. It is meant to express that both components and their relations must be described at a suitable level of abstraction. Also note that the phrase relationships between components may cover quite a lot. For example, when considering the architecture of a Web application, issues such as communication protocols and document standards must be considered as well. In addition, the technological infrastructure, elements of which are given in slide infrastructure, must also be taken into account.


Technological infrastructure

 [CS2001]


  • client-platform -- hardware, OS
  • presentation services -- windows, multimedia
  • application software -- code, business logic
  • network -- communication support
  • middleware -- distribution, (object) brokers
  • server platform -- hardware, OS
  • database -- data management system

slide: Technological infrastucture

One may wonder whether the architecture metaphor, which is derived from the construction of buildings, is really appropriate for software systems. Software systems are much more dynamic than buildings, so it might perhaps be more appropriate to focus on dynamic, behavioral aspects rather than structural aspects. As a metaphor, I would prefer for example one related to an ecological system, to stress the dynamic growth that seems to be characteristic of software systems nowadays.

In the definition or rather collection of definitions, given by the IEEE Architecture Working Group, for the terms architect, architectural description, stakeholder and viewpoint, utmost care is taken to suppress the phrase structure. Instead, the notion of architecting is defined as defining, maintaining, improving and certifying proper implementation of an architecture, and an architecture as a collection of views relevant to the stakeholders of a system.

Distributed object architectures

When considering the architecture of a system, invariably the technological infrastructure plays a role. In particular, when considering client/server or distributed object systems the choice for respectively a particular client and server platform, middleware and communication infrastructure may to a large extent determine the characteristics of the software architecture.

Explicit attention for the architecture of a system becomes increasingly relevant as the complexity of the system grows. As argued in  [CorbaPatterns], an architecture is an abstraction that allows for mastering complexity and managing change.


Distributed object patterns

 [CorbaPatterns]


  • Framework
  • (class hierarchies)


  • Applications
  • (wrappers)


  • System
  • (horizontal, vertical, metadata)


  • Enterprise
  • (reference models, infrastructure, policies)


  • Intra/Internet
  • (standards)



slide: Distributed object patterns

 [CorbaPatterns] present a number of patterns based on the Common Object Request Broker Architecture (CORBA). The patterns differ in scale, ranging from frameworks and systems to enterprise-level and intra/Internet-level infrastructures. According to  [CorbaPatterns], software problems are due to inadequate definition and transfer of software boundaries. They criticize traditional object-oriented analysis and design methods for not paying sufficient attention to the actual interfaces that define these boundaries which may be regarded as a contract between the supplier of a service and its clients. At the higher enterprise and intra/internet levels, policies and standards are perhaps more important than interfaces per se. However, at the framework and system level interface definitions delineate stable boundaries between the components that constitute the system.

In business applications a distinction can be made between horizontal components (covering general functionality, such as GUI-aspects and document interoperability), vertical components (covering domain-specific functionality for one area of business, such as finance), and meta-data, representing the more volatile, knowledge-level aspects of a system.  [CorbaPatterns] observe that each of these component types may cover one third of a system. When to consider information or a service as part of the meta-data must be determined by the extent to which that particular information or service may be considered stable. Architectural decisions must strive for an ecology of change, that is a flexible arrangement of components to promote changes in business-logic and adaptiveness to a changing environment.

Case study -- multimedia feature detection

In this section, we will look at the indexing and retrieval of musical fragments. This study is primarily aimed at establishing the architectural requirements for the detection of musical features and to indicate directions for exploring the inherently difficult problem of finding proper discriminating features and similarity measures in the musical domain. In this study we have limited ourselves to the analysis of music encoded in MIDI, to avoid the technical difficulties involved in extracting basic musical properties from raw sound material. Currently we have a simple running prototype for extracting higher level features from MIDI files. In our approach to musical feature detection, we extended the basic grammar-based ACOI framework with an embedded logic component to facilitate the formulation of predicates and constraints over the musical structure obtained from the input.


slide: The extended ACOI architecture

The ACOI framework

The ACOI framework is intended to accomodate a broad spectrum of classification schemes, manual as well as (semi) automatic, for the indexing and retrieval of multimedia objects,  [ACOI].

What are stored are not the actual multimedia objects themselves, but structural descriptions of these objects (including their location) that may be used for retrieval.

The ACOI model is based on the assumption that indexing an arbitrary multimedia object is equivalent to deriving a grammatical structure that provides a namespace to reason about the object and to access its components. However, there is an important difference with ordinary parsing in that the lexical and grammatical items corresponding to the components of the multimedia object must be created dynamically by inspecting the actual object. Moreover, in general, there is not a fixed sequence of lexicals as in the case of natural or formal languages. To allow for the dynamic creation of lexical and grammatical items the ACOI framework supports both black-box and white-box (feature) detectors. Black-box detectors are algorithms, usually developed by a specialist in the media domain, that extract properties from the media object by some form of analysis. White-box detectors, on the other hand, are created by defining logical or mathematical expressions over the grammar itself. In this paper we will focus on black-box detectors only.

As an example, look at the (simple) feature grammar below, specifying the structure of a hypothetical community.



  detector world; // finds the name of the world
  detector people; // checks name, eliminates institutes
  detector company; // looks if there are at least two persons
  
  atom str name;
  
  community: world people company;
  
  world: name;
  people: person*;
  
  person: name;
  

slide: A sample grammar

A community consists of people, and is a community only if it allows for the people to be in each other's company.

A community has a name. The actual purpose of this grammar is to select the persons that belong to a particular community from the input, which consists of names of potential community members. Note that the grammar specifies three detectors. These detectors correspond to functions that are invoked when expanding the corresponding non-terminal in the grammar. An example of a detector function is the personDetector function partially specified below.



  int personDetector(tree *pt, list *tks ){
  ...
  q = query_query("kit=pl src=check.pl");
  
  while (t = next_token(tks)) {
        sprintf(buf,"person(\%s)",t);
        query_eval(q,buf);
        if (query_result(q,0)) 
// put name(person) on tokenstream

putAtom(tks,"name",t); } ... }

slide: A person detector

The personDetector function checks for each token on the input tokenstream tks as to whether the token corresponds to the name of a person belonging to the community. The check is performed by an embedded logic component that contains the information needed to establish whether a person is a member of the community. Note that the query for a single token may result in adding multiple names to the token stream.

The companyDetector differs from the personDetector in that it needs to inspect the complete parse tree to see whether the (implicit) company predicate is satisfied.

When parsing succeeds and the company predicate is satisfied a given input may result in a sequence of updates of the underlying database, as illustrated below.



  V0 := newoid();
  V1 := newoid();
    community_world.insert(oid(V0),oid(V1));
      world_name.insert(oid(V1),"casa");
    community_people.insert(oid(V0),oid(V1));
  V2 := newoid();
      people_person.insert(oid(V1),oid(V2));
        person_name.insert(oid(V2),"alice");
      people_person.insert(oid(V1),oid(V2));
        person_name.insert(oid(V2),"sebastiaan");
      ...
  

slide: Database updates

Evidently, the updates correspond to assigning appropriate values to the attributes of a structured object, reflecting the properties of the given community.

The overall architecture of the ACOI framework is depicted in slide acoi. Taking a feature grammar specification, such as the simple community grammar, as a point of reference, we see that it is related to an actual feature detector (possibly containing an embedded logic component) that is invoked by the Feature Detector Engine (FDE) when an appropriate media object is presented for indexing. The feature grammar and its associated detector further result in updating respectively the data schemas and the actual information stored in the (Monet) database. The Monet database, which underlies the ACOI framework, is a customizable, high-performance, main-memory database developed at the CWI and the University of Amsterdam, see  [MONET].

At the user end, a feature grammar is related to a View, Query and Report component, that respectively allow for inspecting a feature grammar, expressing a query, and delivering a response to a query. Some examples of these components are currently implemented as applets in Java 1.1 with Swing, as described in  [ACOI].

Formal specification

Formally, a feature grammar G may be defined as G = (V,T,P,S), where V is a collection of variables or non-terminals, T a collection of terminals, P a collection of productions of the form V -> (V \union T) and S a start symbol. A token sequence ts belongs to the language L(G) if S -*-> ts. Sentential token sequences, those belonging to L(G) or its sublanguages L(G_v) = (V_v,T_v,P_v,v) for v \e (T \union V), correspond to a complex object C_v, which is the object corresponding to the parse tree for v, as illustrated in the community example. The parse tree defines a hierarchical structure that may be used to access and manipulate the components of the multimedia object subjected to the detector.

The anatomy of a MIDI feature detector

Automatic indexing for musical data is an inherently difficult problem. Existing systems rely on hand-crafted solutions, geared towards a particular group of users, such as for example composers of film music, see  [MM]. In this section, we will look at a simple feature detector for MIDI-encoded musical data. It provides a skeleton for future experimentation.

slide: MIDI features

The hierarchical information structure that we consider is depicted in slide midi-structure. It contains only a limited number of basic properties and must be extended with information along the lines of a musical ontology including genre, mood and the like. However, the detector presented here provides a skeleton solution that accommodates an extension with arbitrary predicates over the musical structure in a transparent manner.

The grammar given below corresponds in an obvious way with the structure depicted in slide midi-structure.



  
  detector song; ## to get the filename
  detector lyrics; ## extracts lyrics
  detector melody; ## extracts melody
  
  atom str name;
  atom str text;
  atom str note;  
  
  song: file lyrics melody;
  
  file: name;
  lyrics: text*;
  melody: note*;
  
  

slide: A simple feature grammar for MIDI files

The start symbol is a song. The detector that is associated with song reads in a MIDI file. The musical information contained in the MIDI file is then stored a a collection of Prolog facts. This translation is very direct. In effect the MIDI file header information is stored, and events are recorded as facts, as illustrated below for a note_on and note_off event.


  event('kortjakje',2,time=384, note_on:[chan=2,pitch=72,vol=111]).
  event('kortjakje',2,time=768, note_off:[chan=2,pitch=72,vol=100]).
  
After translating the MIDI file into a Prolog format, the other detectors will be invoked, that is the composer, lyrics and melody detector, to extract the information related to these properties.

slide: Processing MIDI file

The actual processing is depicted in slide midi-processing. The input is a MIDI file. As indicated in the top line, the MIDI file itself may be generated from a lilypond file. Lilypond is a \LaTeX-like formatting language for musical scores that also supports the generation of MIDI, described in  [Lily]. As indicated on the bottom line, processing a MIDI file results in a collection of features as well as in a MIDI file and lilypond file. The (result) MIDI file contains an extract of the original (input) MIDI file and the lilypond file contains a score for this extract, which may be presented to the (end) user as the result of a query. This setup allows us to verify whether our extract or abstraction of the original musical structure is effective, simply by comparing the input (midi or lilypond) musical structure with the output (midi or lilypond) extract.

To extract relevant fragments of the melody we use the melody detector, of which a partial listing is given below.



  int melodyDetector(tree *pt, list *tks ){
  char buf[1024]; char* _result;
  void* q = _query;
  int idq = 0; 
  
    idq = query_eval(q,"X:melody(X)");
    while ((_result = query_result(q,idq)) ) {
           printf("note: \%s",_result);
           putAtom(tks,"note",_result);
           }
    return SUCCESS;
  } 
  

slide: The melody detector

The embedded logic component is given the query X:melody(X), which results in the notes that constitute the (relevant fragment of the) melody. These notes are then added to the tokenstream. A similar detector is available for the lyrics.

Parsing a given MIDI file, for example kortjakje.mid, results in updating the Monet database. The updates reflect the structure of the musical information object that corresponds to the properties defined in the grammar.

Implementation status

Currently, we have a running prototype of the MIDI feature detector. It uses an adapted version of public domain MIDI processing software. The embedded logic component is part of the hush framework. It uses an object extension of Prolog that allows for the definition of native objects to interface with the midi processing software. The logic component allows for the definition of arbitrary predicates to extract the musical information, such as the melody and the lyrics.

Queries -- the user interface

Assuming that we have an adequate solution for indexing musical data, we need to define how end users may access these data, that is search for musical objects in the information space represented by the database, for the ACOI project the World Wide Web.

slide: Keyboard interface

For a limited category of users, those with some musical skills, a direct interface such as a keyboard or a score editor, as provided by the hush framework, might provide a suitable interface for querying the musical database. Yet, for many others, a textual description, or a form-based query will be more appropriate.

slide: User Query Processing

In processing a query, we may in some cases derive a partial melody or rhythmic structure from the query, as well as some additional features or criteria. As explained, the output of indexing MIDI files consists of both information concerning features as well as a musical rendering of some of these features. These features can be used to match against the criteria formulated in the query. The musical renderings, which include a partial score, may be presented to the user in response to a query, to establish whether the result is acceptable.

Crossing boundaries

subsections:


It is futile to hope for a single language or paradigm to solve all problems. Therefore, as our small case study concerning multimedia feature extraction indicates, components may differ in how they are realized. Some components are better implemented using knowledge-based systems technology, whereas other components require the use of a systems programming language such as C++. Even within components it may be necessary to transgress the language boundary. For example in Java applications, wrapping legacy applications or operating system-dependent code is usually done using the native language interface.

In this section we will look at some studies (executed within the hush framework) that exemplify a multi-paradigm and multi-lingual approach. We will first look at the issues that arise when embedding a logic (that is Prolog) interpreter. Then we will extend the embedded logic with objects that may correspond to (native) objects in the host language, that is C++. These sections may safely be skipped by readers not interested in logic programming. Finally, we will look at how to realize corresponding collections of objects in (native) C++ and Java.

Embedded logic -- crossing the paradigm boundary

Knowledge is a substantial ingredient in many applications. By knowledge we mean information and rules operating on that information, to obtain derived information. As in any (software) engineering effort, maintenance, that is knowledge maintenance, is of crucial importance. When we do not avoid the dispersion of knowledge and information in the actual code of the system, maintenance will be difficult. Put differently, for reasons of flexibility and maintenance we need to factor out the (volatile) knowledge and information components.

Traditionally, the information components are often taken care of by a database that allows for the formulation of views to obtain (possibly aggregate) information. Logic or logic programming is a strictly more powerful mechanism to deal with information and knowledge. In our group, we have been studying the use of logic programming in knowledge-intensive software engineering applications.

embedded logic



  <query kit=pl src=local.pl cmd=X:email_address(X)>
  <param format=" \%s">
  <param result="">
  <param display="<h4>The query</h4>">
  <param header="<h4>The adresses</h4> <ul>">
  <param footer="</ul>">
  email_address(E) :-
  	person(X),
  	property(X,name:N),
  	property(X,familyname:F),
  	email(X,E),
  	cout(['
  • ', N,' ',F,' has email adress ']), cout([ '<a href=mailto:', E, '>', E, '</a>',nl]). </query>
  • As an example, consider the query above, which is expressed in an SGML/XML like syntax. The query command X:email_address(X) asks for all X for which the predicate email_address(X) holds. The predicate email_address is defined between the query begin and end tags.

    The query tag is an element of one of the text processing filters to provide hypermedia support for software engineering described in  [HypermediaSE]. Processing the fragment above results in an HTML list of names and email addresses. The collection of filters itself is written in lex, yacc and C++. To process the query, an embedded logic programming interpreter is invoked. To merge the output from the query, a handler is installed for the cout command.

    The query example was motivated by the need to maintain Web pages for the administration of a colloquium within our group. The actual knowledge base consists of a list of people and some rules to determine their affiliations and email addresses. The knowledge base is made available by consulting the file local.pl.

    As concerns the implementation, the Java fragment below indicates how to access the logic programming interpreter from a (Java) program.

    
      query pl = new query("kit=pl src=remote.pl"); 
    logic.java

    pl.eval("X:assistant(X)"); String res = null; while ( (res = pl.result()) != null ) { System.out.println("
  • " + res); }
  • After creating a query object, the goal X:assistant(X) is invoked, which can be taken to mean, give me every X for which the predicate assistant(X) holds. The final output is obtained by iterating over the results of the evaluation of that goal. As a comment, multiple results may be obtained in Prolog by backtracking over the possible choice points.

    Distributed knowledge servers

    Maintaining knowledge is difficult. As a rule of thumb, avoid the replication of knowledge as much as possible. However, this means that we may need to access knowledge from remote sources. One (obvious) solution that presents itself is to allow for url-enabled consults, as illustrated in the fragment below.

    
      
    remote.pl

    :- source('http://www.cs.vu.nl/~eliens/db/se/people.pl'). :- source('http://www.cs.vu.nl/~eliens/db/se/institute.pl'). :- source('http://www.cs.vu.nl/~eliens/db/se/property.pl'). :- source('http://www.cs.vu.nl/~eliens/db/se/query.pl').
    This solution has (indeed) be implemented in our filters, since the url addressing scheme is straightforward and easy to implement.

    However, processing the information accessed by url is still done locally. So, the next step that may be suggested is to distribute the knowledge processing itself, for example by using CORBA.

    
      interface query { 
    query.idl

    void source(in string file); long eval(in string cmd); string result(in long id); oneway void halt(); };
    Exploiting the integration of CORBA and hush, we have defined an interface for query in IDL and implemented query client and query server objects. These objects may be created by giving appropriate parameters to the query constructor invocation. This approach allows for embedding remote knowledge processing transparently in our collection of filters. Nevertheless, although we showed that this approach is feasible, we have not addressed the problems that may occur due to the unavailability or faults of the server.

    Native objects -- crossing the language boundary

    Embedding (script) language interpreters is becoming standard practice, as testified by the existence of embeddable interpreters for Tcl, Perl, Python, Javascript, Java, and Prolog. Each of these languages also supports calling native code, that is code written in C or C++, to allow for accessing system resources or simply for reasons of efficiency.

    Native bindings for these languages are available only on the level of functions. Even for Java, native methods of an object are defined as functions that receive a handle to the invoking object. Given a language with objects, possibly by adopting an object extension for the languages without objects, the problem is to find a proper correspondence between objects defined in the high-level (script) language and the native objects defined in C/C++.

    In this (sub)section we will first study an extension of Prolog with objects, and then indicate a solution to establish a close correspondence between the (Prolog) objects and their native counterparts. In the next (sub)section, we will apply this approach to establish a correspondence between Java and C++ objects.


    Objects in Prolog

    • representation -- object(Handler,Class,ID,REF,Ancestors)
    • object definition -- class_method(This,...)
    • object invocation -- self(This):method(...)
    • state variables representation -- value(ID,Key,Value)
    • state variable access -- var(Key) = Value, Var = value(key)
    • native binding - native(Handler,Method,Result)

    slide: Objects in Prolog

    In slide Objects our proposed object extension for Prolog (in particular SWI-Prolog,  [SWI]) is presented. Actually, there are many object extensions of Prolog around, for example the well-known Sicstus Objects. Our extension is motivated by the following considerations:

    requirements


    • low overhead, especially when not needed
    • natural syntax for object clause definitions
    • support for native objects
    In our solution, objects are represented by dynamic fact clauses, containing a Handler, indicating how native calls are to be dealt with, a Class, and object identity ID, possibly a reference REF to a native C/C++ object, and a list of Ancestors.

    Objects (or classes of objects, if you prefer) are defined by a collection of clauses with a head predicate of the form class_method(This,...), specifying the class, method and object identity parameter. The actual invocation of the method takes the form self(This):method(...), where the colon acts as the familiar dot object access parameter. Note that the identity parameter (This) does not occur among the method parameters, but is instead contained in the object specifier. Instead of the keyword self, we may also use a class name to enforce a cast to specific object type when invoking the method. In the actual object extension, we also support object state instance variables, which are however not relevant for our discussion here.

    Object methods may be defined as native by including a goal of the form native(Handler, Method, Result), where Handler specifies the (native) handler to be invoked, Method the actual request, and Result a variable to store the possible outcome of the request. When the Handler parameter is left unspecified, the handler defined for the object will be taken to effect the native call.

    Let's look at some examples first, to augment this admittedly concise description.

    
      
              midi(This):midi,  // create midi object
              Self = self(This),
              Self:open('a.mid'),
              Self:header(0,1,480),
              Self:track(start),
              Self:melody([48,50,51,53,55]), // c d es f g, minor indeed
              Self:track(end), // end track
      
    In the fragment above we see how a midi object is created and how a simple melody is written to a file. Note that we use a variable Self for indicating the object specifier self(This). Below, the actual definition of the midi object (class) is given.

    
      
    midi

    :- use(library(midi:[midi,lily,music,process])). :- declare(midi:object,class(midi),[handler]). midi_midi(This) :- // constructor midi(This):handler(H), // gets Handler from class declare(H,new(midi(This)),[],[],_).
    The constructor for the midi object, for which the method name is equal to the class name, asks whether there is a Handler for midi objects. This handler, which is specified in the declare command above, is then passed to the declare command for the object. Since there is a handler, the constructor for the native midi object (defined in C++) is automatically invoked.

    
      
    native methods

    midi_read(This,F) :- native(_,This,read(F),_). midi_analyse(This,I,O) :- native(_,This,analyse(I,O),_). midi_open(This,F) :- native(_,This,open(F),_). midi_header(This,M) :- native(_,This,header(M,0,480),_). midi_track(This,X) :- native(_,This,track(X),_). midi_tempo(This,X) :- native(_,This,tempo(X),_). midi_event(This,D,C,M,T,V) :- native(_,This,event(D,C,M,T,V),_).
    All the methods listed above are implemented using the native midi C++ object. Note that both the Handler and the Result parameter are left unspecified. The handler is by default taken from the class declaration for the midi object class. There is no result when invoking these native methods.

    
      midi_note(This,D,C,T,V) :- 
              Self = midi(This), // cast to midi
              Self:event(D,C,note_on,T,V),
              Self:event(D,C,note_off,T,V).
      
      midi_melody(This,L) :- self(This):melody(480,1,L,64).
      
      midi_melody(_This,_,_,[],_).
      
      midi_melody(This,D,C,[X|R],V) :-
              Self = self(This),
              Self:note(D,C,X,V),  
              midi_melody(This,D,C,R,V).   // direct invocation
      
    The midi object clauses given above augment the native methods by defining additional predicates, such as note and melody. These clauses also illustrate the liberty we have in casting the object specifier to a specific class or bypassing dynamic method invocation. Clearly, a native binding for the midi object is necessary, since Prolog is highly inappropriate for reading or writing midi files directly. It is however very appropriate for specifying rules for analyzing MIDI files!

    C++ bindings

    To redirect native method calls for our (Prolog) objects to their native C++ counterparts we need some additional machinery. First of all, we have to translate a (Prolog) method call to a format that can be passed to a C++ handler, so that the C++ handler may decide which method to invoke for what object. To get a direct correspondence between objects in Prolog and objects in C++, we store a reference to the C++ object in the REF variable of the Prolog object. When a native method is called, this reference is converted into an object handler or pointer in C++, to which the (native) method invocation will be addressed. We use a smart pointer to encapsulate this reference and to allow for directly invoking (native) methods for the corresponding object type.

    As outlined in section Reactor, in the hush framework we use an event-based mechanism to effect foreign language bindings. This means that the information concerning the native call is stored in an event object that is passed to a handler, which invokes the operator function on the occurrence of an event. In the code fragment below it is shown how native method dispatching is taken care of in the operator function of a C++ kit_object, for which a corresponding object in Prolog is assumed to exist.

    
      int kit_object::operator()() {
              event* e = _event;
      
              vm<kit> self(e);  // smart pointer
              string method = e->_method();
      
              if (method == "kit") { // constructor
                      kit* q = new kit(e->arg(1));
                      _register(q);
                      result( reference((void*)q) );
              } else if (method == "eval") {
                      long res = self->eval(e->arg(1));
                      result( itoa(res) );
              } else if (method == "result") {
                      char* res = self->result( atoi(e->arg(1)) );
                      result(res);
              } else { // dispatch up in the hierarchy
                      return handler_object::operator()();
              }
      
              return 0;
              }
      
    Before checking which method is invoked, which is recorded in the event, we create a smart pointer (self) by instantiating a vm instance for the kit class. (The acronym vm is somewhat inappropriately derived from virtual machine.) If the method is a constructor, the result is a reference, that is an integer encoding of the actual pointer. Otherwise, the method is invoked, simply by addressing the smart pointer self. As a comment, the use of smart pointers is a C++ specific technique based on redefining the dereference operator, as illustrated below. When no matching method can be found, the operator method for a handler object higher up in the hierarchy is invoked. In our example, both the kit_object and the midi_object are directly derived from handler_object. This hierarchy, which is intended to encapsulate the native objects, parallels the original hush class hierarchy in a straightforward way. The smart pointer vm class, that we need for our binding of Prolog objects to native C++ objects, is relatively straightforward.
    
      template <class T>
      class vm  { 
    smart pointer class

    public: vm(event* e) { int p = 0; char* id = e->option("ref"); if (id) { p = atoi(id); } _self = (T*) p; } virtual inline T* operator->() { return _self; } private: T* _self; };
    In summary, the constructor converts the event argument to a reference to the parameterized object type T, which is used as the result of the dereference operator. This allows for invoking methods for object type T without further ado. As a comment, our presentation here is somewhat simplified, since we do not take into account the possibility of upcalls, that is the invocation of Prolog code from C++. We will deal with these additional details when discussing the Java/C++ binding in the next (sub)section.

    Combining Java and C++

    The designers of the Java language have created an elegant facility for incorporating native C/C++ code in Java applications, the Java Native Interface (JNI). Elegant, since native methods can be mixed freely with ordinary methods. When qualifying methods as native, the implementer must provide a dynamically loadable library that contains functions, of which the names and signatures must comply with the JNI standard, defining the functionality of the methods. Nevertheless, the JNI does not provide for generic means to establish a direct correspondence between an object class hierarchy in C++ that (partially) implements a corresponding object class hierarchy in Java. In this section, we will study how such a correspondence is realized in the hush framework, using the Java Native Interface.

    The solution to establishing corresponding object class hierarchies in Java and C++ that we have adopted relies on storing a reference to the native C++ object in the Java object and the conversion of this reference to a smart pointer encapsulating access to the native C++ object. Upcalls, which occur for example when Java handlers are invoked in response to an event, require some additional machinery, as will be explained shortly.

    Each Java class in hush is derived from the obscure class, which contains an instance variable _self that may store a C++ object reference, encoded as an integer.

    
      package hush.dv.api;
      
      class obscure { 
    obscure

    public int _self; // peer object pointer ... };
    The class obscure has been introduced so as not to pollute the handler class, which is the base class for almost every hush class. The (Java) handler class is derived from obscure.

    As an example, look at the (partial) Java class description for kit below.

    
      package hush.dv.api;
      
      public class kit extends handler { 
    kit

    public kit() { _self = init(); } protected kit(int x) { } private native int init(); public native void source(String cmd); public native void eval(String cmd); public String result() { String _result = getresult(); if (_result.equals("-")) return null; else return _result; } private native String getresult(); public native void bind(String cmd, handler h); ... };
    Recall that the kit class is used to encapsulate an embedded interpreter, such as a Tcl or Prolog interpreter. When a kit is constructed, the instance variable _self is initialized with the reference obtained from the native init method, which will be given below. The other methods of kit are either native or result in invoking a native method, possibly with some additional processing.

    Each native method must be implemented as a function, of which the name and signature are fixed by the JNI conventions, as illustrated below.

    
      
    kit.c

    #include <hush/hush.h> #include <hush/java.h> #include <native/hush_dv_api_kit.h> #define method(X) Java_hush_dv_api_kit_##X JNIEXPORT jint JNICALL method(init)(JNIEnv *env, jobject obj) { jint result = (jint) kit::_default; // (jint) new kit(); if (!result) { kit* x = new kit("tk"); session::_default->_register(x); result = (jint) x; } return result; }
    The init method, the full name of which is obtained by expanding the macro call method(init), results in an integer-encoded reference to a kit object, which is newly created if it doesn't already exist.

    
      JNIEXPORT jstring JNICALL method(getresult)(JNIEnv *env, jobject obj)
      {
        java_vm vm(env,obj);
        char *s = vm->result();
        if (s) return vm.string(s);
        else return vm.string("-");
      }
      
    In the getresult method, we see how a smart pointer, instantiated for the kit class, is used to obtain the result from the C++ kit object. The smart pointer takes care of converting the reference stored in the Java object to an appropriate pointer.

    
      JNIEXPORT void JNICALL method(bind)(JNIEnv *env, jobject obj,
      	       jstring s, jobject o)
      {
        java_vm vm(env,obj);
        java_vm* vmp = new java_vm(env,o,"Handler");
        const char *str = vm.get(s);
        handler* h = new handler();
        session::_default->_register(h);
        h->_vmp = vmp;
        h->_register(vmp);
        vm->bind(str,h);
        vm.release(s, str);
      }
      
    In the bind method, which is used to bind a (Java) handler object to some (Tcl or Prolog) command, a new C++ handler is created. This handler is modified to contain a reference to the smart pointer, which (indeed) also gives access to the Java handler object. Notice that calling the Java handler object is an upcall, when viewed from the native implementation.

    In somewhat more detail, the Java handler object is invoked through the C++ handler object created in the bind method of the kit. The C++ handler is activated when an event occurs, or a Tcl or Prolog command is given. Activating the handler amounts to calling the dispatch method with an appropriate event. To decide whether the activation must be passed through to the Java handler object, the handler::dispatch method checks for the availability of a smart pointer, as illustrated below.

    handler::dispatch


    
      
      event* handler::dispatch(event* e) {
      _event = e;
      if (_vmp) {
              return ((vm*)_vmp)->dispatch(e);
      } else {
      
               int result = this->operator()();
      
               if (result != OK) return 0; 
               else return _event;
               }
      }
      
    When the C++ handler contains a smart pointer, the dispatch method is called for that pointer.

    The Java smart pointer template class for the Java/C++ binding is derived from the smart pointer template class introduced in the previous (sub)section.

    
      #include <hush/vm.h> 
      #include 
      
      template< class T >
      class java_vm : public vm< T > { 
    java_vm

    public: java_vm(JNIEnv* env_, jobject obj_) { _env = env_; _obj = obj_; _self = self(); } ... event* dispatch(event* e) {
    java dispatch

    call("dispatch",(int)e); return e; } T* operator->() { return _self; } T* self() { jfieldID fid = fieldID("_self","I"); return (T*) _env->GetIntField( _obj, fid); } void call(const char* md, int i) { // void (*)(int) jmethodID mid = methodID(md,"(I)V"); _env->CallVoidMethod(_obj, mid, i); } private: JNIEnv* _env; jobject _obj; T* _self; };
    Notice how the value of the _self reference field is obtained from the _self attribute of the Java object. Also notice that calling dispatch for the Java handler is mediated by an additional call function, which obtains an explicit reference to the method that must be invoked. In general, there are many possible method signatures for which such a call function could be supplied, but in our case we only need one, to invoke dispatch.

    Discussion

    Interfacing Java and C++ is at first sight not very difficult, especially not when the majority of calls consists of downcalls (from Java to C++) only. The smart pointer device may then be used as a handy abbreviation. The problems occur, however, when upcalls come into play. Due to the simple design of hush, upcalls occur (almost) exclusively through the dispatch method. This is not the result of explicit design, but in retrospect just sheer luck. When upcalls are spread over the code and may vary in signature, they will most likely bring along significant software engineering and maintenance effort.

    Architectural patterns and styles

    subsections:

    When constructing a system, how does one determine an appropriate style? There is no simple answer to this question. According to  [Practice], several forces play a role, for example quality requirements concerning availability and performance and technological constraints that have to do with the platform on which the system is intended to run. Also, as frankly admitted in  [Practice], personal experiences and preferences of the architect play a role.

    Architectural choices lead to a particular decomposition into components and a characterization of the relation between components. Classifying groups of software architectures, we may speak of architectural styles, which may be defined, following  [Shaw96], as descriptions of component types and patterns of runtime control and data transfer.

    In this section we will look at architectural styles for distributed object systems. Three styles will be introduced, and we will discuss how these styles are related to technological constraints imposed by particular component technologies. Then we will investigate how these styles work out in practice, by a simple case study in which we explore the consequences of a particular style for the solution of a specific problem, in our case the problem of dynamically changing a viewpoint or perspective in an interactive visualization system.

    From technology to style

    We distinguish between three different architectural styles:

    • the distributed objects style
    • the (dynamically) downloadable code style
    • the mobile objects style
    This distinction is arbitrary, in the sense that other distinctions are conceivable. However, the distinction above is well motivated by the technology matrix introduced in section
    Technology, as reflected in the feature-based description given below.

    distributed objects downloadable code mobile objects
    Componentobjectobject/classagent
    ConnectorORBvariousmethods
    Creationserverclientany
    Locationserverclientany
    Clientfixedextensibleextensible
    Serverextensiblefixedextensible


    slide: Feature classification

    The distributed objects style comprises software architectures which consist of software components providing services to client applications. Each object is located at a single, fixed place. Objects on different machines are connected by an ORB (Object Request Broker). Example technologies supporting this architectural style are CORBA and DCOM.

    The second architectural style is the (dynamically) downloadable code style. Classes may be downloaded, to be used on client machines for instantiating objects, which will run on the client machine. Example technologies supporting this style are Java applets, JavaBeans, and ActiveX controls.

    Finally, in the mobile objects style, objects may migrate from host to host, carrying both functionality and data when they move. Consequently, mobile objects may communicate with the local objects of the host they currently reside on. Mobile objects are a means to implement agents which wander through a network, collecting information, negotiating with other agents, periodically reporting back results to the user who launched the agents. Technologies supporting the mobile object style are agent ORBs such as Voyager.

    Features

    In slide Features, an overview is given of the characteristic features of each style. Clearly, the styles differ in what are considered as constituent parts (components and connectors), location issues (which determine where objects are created and where they are located during their lifetime), and functionality issues (that is whether either the client or server is functionally extensible).

    We may regard the location issues as the prime discriminators of the architectural styles discussed. Adopting the distributed objects architectural style, new objects can be added at the server-side, where they will stay for the remainder of their lifetime. In contrast, adopting the downloadable code style, objects may be created at the client-side, from classes obtained from the server. Most flexible is the mobile objects style, which allows for objects to reside on either server or client machines.

    The location properties directly affect the way that the system is extensible with new functionality. Clearly, the mobile code style offers the maximum of flexibility and functional extensibility. Nevertheless, as we will discuss shortly, there are tradeoffs involved. The maximum in flexibility and extensibility does not necessarily offer the optimal solution!

    Case study -- perspectives in visualization

    To determine which architectural style to use, or which mix of styles, is to a large extent determined by practical experience. Nevertheless, at the end of this section, we will discuss some rules of thumb that may guide you in the choice of a particular style. However, first we will look at an example that illustrates the consequences of the choice of a particular style. The example comes from the distributed visualization architecture (DIVA) that is explained in more detail in section DIVA. DIVA is being developed in cooperation with ASZ/GAK, the largest social security provider in the Netherlands, for experimenting with business visualization to support decision making. Our case study focuses on how to support the sharing of perspectives in visualizing shared information. For example, one of the users discovers a new way to display information, uncovering aspects that would otherwise remain hidden. This new perspective must then be shared with other users to coerce them, so to speak, to this new point of view. What we will look at, here, is how the choice of a particular style affects the solution for the sharing of perspectives problem.

    slide: Exchanging perspectives

    Distributed objects style

    New functionality can be added by creating a new object at the server. In this case, slide Perspectives(a), the user discovering a new perspective acts as the server. Then, assuming that the discovery of a new perspective is somehow announced to the other users, a user can connect to the server and request for that particular perspective (1). Then, a new visualization object is created (2), which is made accessible to the user requesting for the new perspective (3).

    Downloadable code style

    When a new visualization perspective is discovered, a class is created that can be downloaded by the interested user, slide Perspectives(b). The user connects to the server that contains the new visualization class (1), downloads the class, and instantiates a new visualization object (2). Finally, the information is retrieved from the shared information server and accordingly visualized (3).

    Mobile objects style

    Similar as in the downloadable code style, the new visualization perspective is downloaded from a server to the client, slide Perspectives(c). However, in this case, when a user requests for a new perspective (1), it is not a class, but an object, actually a clone of the object residing at the server, that is transferred to the client's machine (2). The clone, which contains all relevant information, does not have to contact the shared information server to update the user's visualization with a fresh viewpoint.

    Guidelines for selecting a style

    In the DIVA system, we have experimented with all these styles. In our system, we eventually made a choice of the mobile object style for sharing perspectives, since it turned out to provide the most flexible solution. It was also the most natural solution to create display agents for managing perspectives. See section DIVA.

    Nevertheless, for other parts of the system we were forced to choose a different solution. For example, since we use a C++ simulation library for obtaining the information, we had to use distributed objects (read CORBA) for making the information available. And for developing control applets, agent technology seemed to be a bit of an overkill so we restricted ourselves to plain Java technology, that is the downloadable code style.

    Generalizing, from our experience we can formulate the following rules of thumb, listed in slide Guidelines.


    Rules of thumb -- selecting an architectural style

    • Dedicated hardware or legacy code
    • distributed objects


    • Strategic or secret code
    • distributed objects


    • Many users
    • downloadable code


    • Periodic updates
    • downloadable code


    • Communication and negotiation
    • mobile objects



    slide: Rules of thumb

    Because interoperability is a key feature of distributed objects, the distributed objects style is particularly recommended for wrapping dedicated hardware or legacy software systems. Additionally, distributed objects only expose the interface and do not give away the implementation. This may be necessary for strategic or security reasons.

    When a large amount of clients is running an application on a server, the server can easily become overloaded. In this case, moving the processing to the client, by deploying dynamically downloadable classes, is a natural solution. Additionally, when (parts of) an application are updated often, for example because of changing legislation, architectures based on downloadable code are much easier to keep up-to-date. Clients are then automatically using the latest version of the available software.

    The latter guidelines hold for the mobile objects style as well. However, agent technology is much more complex. And there is, generally, an efficiency price to pay. So, it is reasonable to introduce agent technology only when real benefits can be expected from the migration of objects, for example when the communication and negotiation with local objects is substantial.

    Concluding, we may state that the adoption of a style will often be dictated by the technological constraints a system must satisfy. Nevertheless, a word of warning is in place here. Choosing a style may well have consequences for the overall complexity of the system. Minimalism is to be strived for, in this respect. For example, adopting the mobile object style, that is the use of agents, may significantly complicate the semantics of the system, and consequently induce an increased verification and validation effort.

    Cross-platform development

    Platform dependencies form an important category of architectural constraints. In particular, the opportunities offered by one platform may prohibit the deployment of software on other platforms. Nowadays, there are a number of (flavors of) competing platforms, as there are the Unix flavors (of which Linux is becoming a strong contender) and the Windows family, including 3.1 (almost extinct), Windows 95, NT, 98 and (in beta release) Windows 2000. Unix (for example Sun Solaris and SGI IRIX) has by tradition a strong position in the server market. However, Windows NT is growing rapidly in importance. The Windows family, clearly, dominates the (client) desktop market.

    Cross-platform development

    Unix vs NT


    • open toolkits and standards -- OMG CORBA

    Research/GNU

    • AT&T U/WIN -- Posix for 95/NT
    • Cygnus -- GNU-win32

    Commercial

    • NuTCracker/MKS -- porting Unix applications to Windows
    • Wind/U, Mainwin -- porting Windows applications to Unix
    • Tributary -- developing Unix applications from Windows IDE

    slide: Cross-platform development

    Nevertheless, the need to support a variety of platforms will exist at least for some time, and consequently questions with regard to portability and cross-platform development may be important architectural issues.

    Considering the opportunities for platform-independent or cross-platform development, we may distinguish between three approaches:

    As we have discussed previously, many of the open standards, such as OMG CORBA, and proprietary standards such as Sun Java, aim at platform independence. Also, there are numerous GUI toolkits available that offer platform-independent support. A possible disadvantage of this approach is that the platform specific technology can usually not be profited from.

    When it comes to porting applications from Unix to Windows 95/98/NT, we may look at AT&T U/WIN, which provides a POSIX extension for Win32, or Cygnus GNU-win32 support, which offers many of the GNU utilities and libraries for the Windows platform. Similar functionality, as well as support for Motif/X11 GUI capabilities, is offered by the (commercial) NuTCracker environment. (A detailed discussion of the technical merits of the various offerings is beyond the scope of this book. However, the interested reader may find more information in the online version of this book.)

    The Windows platform is not only popular with end-users but also with many developers, who enjoy using the Microsoft Visual Studio suite of tools and (object-oriented) frameworks such as MFC. Recently, toolkits have entered the market that allow for porting Microsoft technologies (including Visual Basic, ActiveX and MFC applications) to the Unix platform, in particular Wind/U from bristol.com and Mainwin from mainsoft.com. As a word of warning, these toolkits are still terribly expensive. Yet for more information, consult the online version of this book.

    For those who wish to develop directly on the Unix platform, but using Microsoft Visual Studio, there is Tributary, from bristol.com, which offers a Unix-server and client-extensions to Visual Studio.

    Discussion

    From an architectural perspective, it should not matter what platform is used for the actual development, nor for what target platform the software is being developed. In practice, however, given the preferences of the developers, the particularities of the platforms, and the instability of the (beta) software running on these platforms, the actual choice may make a big difference. As an admittedly weak advice, tune your strategy to your needs and experience! (And your budget.)

    Summary

    This chapter explored the notion of software architecture, and in particular how both problem-related issues and technological constraints determine the adoption of a particular architectural style.

    Elements of architecture

    1


    • processing elements -- transformation on data
    • data elements -- contain information
    • connections -- glue that holds elements together

    slide: Section 7.1: Elements of architecture

    In section 1, we looked at a number of definitions of the notion of software architecture, including the definition given in  [Practice]. We also looked at the technological infrastructure underlying client/server architectures and discussed some selected distributed object patterns.

    Case study -- multimedia feature detection

    2


    • feature grammar -- structure
    • embedded logic -- rules for recognition
    • architecture -- multimedia information system

    slide: Section 7.2: Case study -- multimedia feature detection

    In section 2, we looked at an experimental musical feature detector, as an example architecture, that uses both a grammar to describe the structural properties of the media items involved, and (embedded) logic to express the rules governing the determination of properties and the retrieval of specified media items.

    Crossing boundaries

    3


    • embedded logic -- crossing the paradigm boundary
    • native objects -- crossing the language boundary
    • combining Java and C++

    slide: Section 7.3: Crossing boundaries

    In section 3, we discussed some of the implications of the architecture sketched in section 2. We looked at embedded logic, as an example of crossing paradigm boundaries, and native objects, as an example of crossing language boundaries. In addition, some of the technical details involved in coupling (native) C++ objects to Java objects were presented.

    Architectural patterns and styles

    4


    • technology matrix -- from technology to style
    • case study -- visualization perspectives

    slide: Section 7.4: Architectural patterns and styles

    In section 4, we discussed how to decide which architectural style to adopt, based on technological constraints on the one hand and application requirements on the other hand. As an illustration, we discuss the alternatives that may arise when realizing an extension to a distributed visualization architecture.

    Cross-platform development

    5


    • from Unix to Windows -- AT&T U/Win, Cygnus GNU-win32
    • from Windows to Unix -- Wind/U, Mainwin

    slide: Section 7.5: Cross-platform development

    Finally, in section 5 we discussed some of the solutions that are available for platform-independent and cross-platform development.

    Questions

    1. What are the elements of a software architecture? What role does a software architecture description play in development?
    2. Give a definition of software architecture. Can you think of alternative definitions?
    3. What kind of patterns can you think of for distributed object architectures?
    4. Give an example of a complex software architecture. Can you relate the description of the architecture to the definition given earlier?
    5. Discuss the possible motivations for deploying embedded logic.
    6. How would you extend a given imperative or declarative language with objects?
    7. Discuss the Java Native Interface. Does it provide a solution for the problem posed in the previous question? Explain.
    8. What determines the choice for an architectural style? Give an example!

    Further reading

    An excellent book on software architectures is  [Practice]. You may also want to visit the SEI architecture site at www.sei.cmu.edu/architecture, which provides definitions, and a wealth of other information. As a discussion of the software engineering implications of CORBA, you may want to read  [CorbaPatterns]. If you are interested in multimedia information systems, read  [MM]. For more information on ACOI, visit the ACOI website on http://www.cwi.nl/~acoi .

    
    

    [] readme course preface 1 2 3 4 5 6 7 8 9 10 11 12 appendix lectures resources
    eliens@cs.vu.nl

    draft version 0.1 (15/7/2001)