Lecture
In formulating our approaches to the architecture of the inventory tracking system, we must keep in mind three matters of an organizational nature: the division of functions between the client and server parts, the transaction management mechanism, and the strategy for implementing the client part of the application.
Client/Server Architecture
The most important question in implementing a client/server architecture is not so much the question of where the boundary between these two parts will be drawn, as of how to make this division sensibly. Returning to first principles, we know the answer to this question: we must concentrate on the behavior of each abstraction, based on an analysis of the use cases of each entity, and only then take a decision on where to place that behavior. After we have done this work with respect to several of the principal objects, the general mechanisms will become clear, and understanding them will help us to place the remaining abstractions correctly.
As an example, let us consider the behavior of the classes Order and ProductRecord. An analysis of the first of them gives us the following list of necessary operations:
The service operations listed can be expressed immediately in C++, once we have given two new type definitions:
// types of identification numbers
typedef unsigned int OrderID;
// type describing the local currency
typedef float Money;
We now obtain the following class definition:
class Order {
public:
Order();
Order(OrderID);
Order(const Order&);
~Order();
Orders operator=(const Orders);
int operator==(const Orders) const;
int operator!=(const Orders) const;
void setCustomer(Customer&);
void setOrderAgent(OrderAgent&);
void addItem(Product&, unsigned int quantity = 1);
void removeItem(unsigned int index, unsigned int quantity = 1);
OrderID orderID() const;
Customer& customer() const;
OrderAgent& orderAgent() const;
unsigned int numberOfItem() const;
Product& itemAt (unsigned int) const;
unsigned int quantityOf(unsigned int) const;
Money totalValue() const;
protected:
...
};
Let us note the presence of several variants of the constructor. The first of them is used by default (Order()) to create an object with a new unique value of the identifier OrderID. The copy constructor also creates an object with a unique identifier, but at the same time copies into it the state of the object used as the argument.
The last constructor takes an OrderID as its argument, that is, it constructs an object already existing in the database and extracts its parameters from the database. In other words, in this case we are rematerializing an object that exists in the database. Such an operation, of course, requires the performance of certain actions: when restoring an object from the database, the corresponding SQL mechanism must either make the object shared or synchronize the state of the two objects created in different applications. The details, of course, are hidden in the implementation and are inaccessible to the client, who uses the object by means of the ordinary object interface.
Implementing the approach described does not present any particular difficulty. If the class Order is designed so that its state is fully determined by the identifier OrderID, then the implementation of the operations reduces to ordinary read and write operators against the database. Copies of objects are synchronized, since the corresponding table in the database serves as a single repository of state for all representations of one object.
The object diagram in Figure 10-6 illustrates the SQL mechanism described using the example of the invoicing scenario. The following events are realized in the scenario:

Figure 10-6. Invoicing.
The mechanism described presupposes that we can rely on the mechanism existing in the database for record locking and mutual exclusion of access (imagine what might happen if one record were updated simultaneously by two applications). If this locking mechanism must be visible to the client, then one can make use of the same approach that we used in creating the class library in Chapter 9. Below we will show that the transaction execution mechanism makes it possible to modify several records in the database in a single pass, thereby ensuring the integrity of the database.
Once the mechanism described has been implemented, the question of where to place the business logic is of tactical significance rather than anything else. In this sense the situation does not differ from the one we would have in a non-object-oriented architecture. But in an object-oriented architecture these decisions can be changed, hiding the very fact of the change from the client. Thus, the client is not affected by the changes we make in the course of tuning the system.
As an example, let us consider two different cases. Adding records about product availability to the database, or deleting them, must obviously be coordinated with the business logic, and so it seems natural to place the business logic on the server. Adding information about new products to the database requires their precise definition and unambiguous identification. In addition, this data must be made accessible to all clients so that they can update their cached tables. Deleting a product from the database also requires a check for the presence of orders for that product and a warning to the corresponding clients [It was precisely for these semantic relationships that triggers were invented: they describe the reaction to certain significant events in the database. Having adopted an object-oriented view of the world, one can formalize this convention on the use of triggers, encapsulating them as part of the semantics of operations on database objects].
In contrast, calculating the cost of orders is a more local operation and is better performed in the client part of the application. In carrying out the calculations, we request from the database the prices for all the items of the order, add them up, convert them into the required currency, check them against the permissible credit terms, and so on.
Thus, in choosing where to place a function in a client/server architecture we follow two rules: first, implement business rules and algorithms where the necessary information is concentrated; second, place these algorithms in the lower layers of the object-oriented architecture, so that the introduction of changes does not affect the system as a whole.
Now let us return to our example and consider more attentively the class Product. For this class we define the following set of operations:
These operations are common to all kinds of goods. However, an analysis of particular cases shows that there are products for which these characteristics are insufficient. Given that the system being designed is an open one, and that the kinds of goods may be extremely varied, let us give several examples of specific goods and their properties:
The examples listed suggest the necessity of creating some hierarchy of product classes. However, the properties listed are so different that they do not form any hierarchy. In this situation it is more expedient to make use of mixins, which Figure 10-7 also illustrates. Let us note the use in this diagram of constraint adornments that refine the semantics of each abstraction.
What is the sense of inheritance for abstractions that reflect the entities of a relational database? A very great one: building an inheritance hierarchy is accompanied by the isolation of common features of behavior and their mapping into the structure of superclasses. These superclasses will be responsible for implementing the common behavior for all objects, with the exception of those subclasses that refine this behavior (through an intermediate superclass) or extend it (through a mixin superclass). Such an approach not only simplifies the construction of the system, but also increases its resilience to changes introduced, at the expense of reducing redundancy and localizing common structure and behavior.

Figure 10-7. Product classes.
The Transaction Mechanism
A client/server architecture is built upon the interaction of the client and server parts of an application, and a definite mechanism is required to implement it. Berson indicated that "there are three basic kinds of interaction between processes in a client/server architecture" [20]:
In our example we made use of only the third approach. In the general case, however, all of the listed kinds of interaction may be used, according to performance requirements or as a result of choosing a particular vendor's software. In any case, our choice must be hidden so that it does not affect the higher-level abstractions.
We mentioned the transaction class earlier, but did not dwell on its semantics in detail. Berson defines a transaction as "a unit of information exchange and processing between local and remote programs that reflects a logically complete operation or result" [21]. This is precisely the definition of the abstraction we need: a transaction object is an agent responsible for carrying out some remote action, and therefore it clearly separates the action itself from the mechanism by which it is carried out.
Indeed, the transaction is the principal high-level form of interaction between server and client, as well as among clients. On the basis of this concept one can carry out a concrete analysis of the use cases. In principle, all of the main functions in the inventory tracking system may be viewed as transactions. For example, placing a new order, confirming the receipt of goods, and changing supplier information are all system transactions.
From the outside, we can identify the following operations, which describe the essence of the behavior in the system being designed:
For each transaction a complete list of the operations it must perform is defined. This means that for the class Transaction we must define member functions such as attachOperation, which give other objects the ability to group a set of SQL statements to be executed as a single transaction.
It is interesting to note that this object-oriented view of transactions agrees completely with the principles accepted in database practice. Date defined a transaction as "a sequence of SQL statements (possibly not only SQL) that must be indivisible in the sense of rollback and concurrency control" [Date, C.[E 1987], p.32].
The concept of atomicity is the most essential part of transaction semantics. If within some transaction an operation is performed on several rows of a table, then either all of the actions must be carried out or the contents of the table must be left unchanged. Consequently, when we dispatch a transaction, we mean the execution of a group of operations as a single whole.
Upon the successful completion of a transaction we must commit its results. A transaction may fail for a number of reasons, including network failures or information being locked by other clients. In such situations a rollback to the initial state is performed. The selector status returns the value of the parameter that indicates whether the transaction succeeded.
Executing a transaction becomes somewhat more complicated when working with distributed databases. It is clear enough how to implement a transaction completion protocol when working with a local database, but what must be done when working with data located on several servers? For this, the so-called two-phase commit protocol is used [23]. In this case the agent, that is, an object of the class Transaction, divides the transaction into several fragments and distributes them for execution to different servers. This is called the prepare phase. When all the servers have reported that they are ready to complete, the central transaction agent sends them all the commit command. This is called the commit phase. Only when all of the separated components of the transaction have completed correctly is the main transaction considered complete. If on even one server the operations are executed incompletely, we roll back the entire transaction. This is possible because each instance of Transaction knows how to roll back its own transaction.

Figure 10-8. Transactions.
The view of the transaction class set out above is shown in Figure 10-8. Here we see a hierarchy of transactions. The class Transaction is the base class for all transactions and contains within itself all the key aspects of the behavior. Derived, specialized classes contribute their own particular features to the common behavior. We distinguish, for example, the classes UpdateTransaction and QueryTransaccion, because their semantics are very different: the first of them modifies data on the database server, while the second does not. By distinguishing these and other types of transactions, we gather the most general characteristics into the base class, and in doing so we enrich our vocabulary.
In the course of further design we may well discover other varieties of transactions, which will be represented by their own subclasses. For example, if we become convinced that the operations of adding and deleting data from a particular database have common semantics, then we will introduce the operations AddTransaction and DeleteTransaction to reflect this commonality of behavior.
In any case, the existence of the base class Transaction allows us to perform any atomic action. For example, in C++ it might look like this:
public:
Transaction();
virtual ~Transaction();
virtual void setOperation(const UnboundedCollection<SQLStatement>&);
virtual int dispatch();
virtual void commit();
virtual void rollback();
virtual int status() const;
protected:
...
};
Note that to build this class we used the base classes we defined in Chapter 9. In this case we built the transaction in the form of an indexed collection of statements. To manipulate this collection we used the parameterized class UnboundedCollection.
The architectural decision we have adopted allows a sophisticated user application to execute sets of SQL statements. All the implementation details of the transaction management mechanism turn out to be hidden from simple clients, for which it is enough to execute a few common types of transaction.
Building the client side of the application
Building the client side largely comes down to constructing a graphical user interface. Constructing a convenient, intuitive, and user-friendly interface is, in turn, more art than science. In applications built within a client/server architecture, it is precisely the quality of the interface that determines (in most cases) the popularity of one program or another. When creating the user interface one must take many different factors into account: technical limitations, features of human psychology, traditions, and the tastes of the staff.
In creating our inventory tracking system we may run into two obstacles. First, we need to determine what the "right" user interface should be. Second, it is desirable to determine which generally accepted approaches we can use in creating the interface.
The answer to the first question can be obtained quite simply, but for that one must prototype, prototype, and prototype again. We need to get a working model of the system as early as possible so that we can show it to users and get informed comments from them. The object-oriented approach helps us substantially in this respect, since it is based on the iterative development of the project. At the very earliest stages of the project we will already be able to show users a prototype of the system.
The second question falls within the sphere of project strategy, but for its successful resolution we have a great many good examples. There are commercial products, for example, the X Window System from MIT, Open Look, Windows from Microsoft, MacApp from Apple, NextStep from Next, and Presentation Manager from IBM. All these products differ substantially: some are based on a network and some rest on the concept of a kernel; some allow one to work at the level of pixels, while others treat more complex geometric figures as primitives. In any case, they all make it possible to simplify the creation of a graphical user interface considerably. Not one of the products listed was born overnight. They all developed gradually from the simplest systems and went through a path of trial and error. As a result, these systems have absorbed a set of abstractions sufficient for building a user interface. Since there is no single answer to the question of what the best interface is, several variants of the windowing model exist.
In Chapter 9 we already mentioned that when working with large class libraries (and graphical interface libraries are such libraries) it is important to understand the mechanisms by which they are built. For our problem the principal mechanism is the response of GUI applications to events. Berson pointed out that for the client side of an application the events associated with the following objects are essential [24]:
To this list we will add network events [For example, Microsoft's DDE (Dynamic Data Exchange) and OLE (Object Linking and Embedding) mechanisms are message-based protocols that provide for the exchange of information between Windows applications]. For our architecture they are very essential, since the client side of the application is connected with other components and applications over the network. The semantics described agree well with our approach to building the class Transaction, which may be regarded as an intermediary that forwards events from application to application. From the standpoint of building the client side, network events are simply one kind of event, which allows us to describe a single mechanism for responding to events.
Berson drew attention to the existence of several alternative models of event handling [25]:
| ® Event loop | The loop scans the event queue and for each event calls the corresponding handling procedure. |
| ® Callback | The application registers a callback function for each GUI element; the callback occurs when the element registers an event. |
| ® Hybrid model | A combination of polling in a loop and callback functions. |
Simplifying considerably, one may assert that the MacApp interface uses a loop, Motif uses callback functions, and Microsoft Windows is an example of the hybrid model.
Besides the primary mechanism, we still need to implement a great many other GUI mechanisms: drawing, scrolling, working with the mouse, menus, saving and restoring, printing, editing, error handling, and memory allocation. Of course, a detailed examination of all these questions lies outside the scope of our analysis, since each particular GUI environment has its own implementations of these mechanisms.
We suggest that the developer of the client side of the application choose a suitable GUI development environment, study its basic mechanisms, and apply them correctly.
Comments