You get a bonus - 1 coin for daily activity. Now you have 1 coin

9.3. Evolution of the system's design

Lecture



Designing the class interfaces

Once the basic principles for building the system's architecture have been worked out, the remaining work is simple, but often rather boring and tedious. The next stage will consist of implementing three or four families of classes (such as queue, set, and tree) in accordance with the chosen architecture, and then testing them in several applications [Wirfs-Brock believes that a development environment must be tested on at least three applications in order to verify the correctness of the strategic and tactical decisions [15]].

The hardest part of this stage is creating a suitable interface for each base class. And here, while developing individual classes in isolation (see Chapter 6), one must not forget the task of ensuring that all parts of the system fit one another globally. In particular, for the class Set the following protocol may be defined:

® setHashFunction Sets the hash function for the elements of the set.
® clear Empties the set.
® add Adds an element to the set.
® remove Removes an element from the set.
® setUnion Forms the union with another set.
® intersection Finds the intersection with another set.
® difference Removes the elements that are contained in another set.
® extent Returns the number of elements in the set.
® isEmpty Returns 1 if the set is empty.
® isMember Returns 1 if the given element belongs to the set.
® isSubset Returns 1 if the set is a subset of another set.
® isProperSubset Returns 1 if the set is a proper subset of another set.


In a similar way, the protocol of the class BinaryTree:

® clear Destroys the tree and all its children.
® insert Adds a new node at the root of the tree.
® append Adds a child to the tree.
® remove Removes a child from the tree.
® share Structurally shares the given tree.
® swapChild Swaps a child with a tree.
® child Returns the given child.
® leftChild Returns the left child.
® rightChild Returns the right child.
® parent Returns the parent of the tree.
® setItem Sets the item associated with the tree.
® hasChildren Returns 1 if the tree has children.
® isNull Returns 1 if the tree is null.
® isShared Returns 1 if the tree is structurally shared.
® isRoot Returns 1 if the tree has a root.
® itemAt Returns the item associated with the tree.


For similar operations we use similar names. In developing the interface we also check the resulting solution against the criteria of sufficiency, completeness, and primitiveness (see Chapter 3).

Support classes

In implementing the class responsible for manipulating text strings, we found that the capabilities provided by the support classes Bounded and Unbounded were clearly insufficient. The bounded form, in particular, turns out to be inefficient for working with strings from the standpoint of memory, since we have to instantiate this form to accommodate the largest possible string, and consequently waste memory on shorter strings. The unbounded form, in turn, is inefficient from the standpoint of speed: searching for an element in a string may require sequentially scanning all the elements of the linked list. For these reasons we had to develop a third, "dynamic" variant:

® Dynamic The structure is stored on the heap as an array whose length may shrink or grow.


The structure is stored on the heap as an array whose length may shrink or grow.

The corresponding support class Dynamic represents an intermediate variant with respect to the bounded and unbounded classes, providing the speed of the bounded form (direct indexing of elements is possible) and the storage efficiency inherent in the unbounded form (memory is allocated only for the elements that actually exist).

Since the protocol of this class is identical to the protocol of the classes Bounded and Unbounded, adding a new mechanism to the library will not be very difficult. We must create three new classes for each family (for example, DynamicString, GuardedDynamicString and SynchronizedDynamicString). Thus, we introduce the following support class:

template<class Item, class StorageManager>
class Dynamic {
public:

Dynamic(unsigned int chunkSize);

protected:

Item* rep;
unsigned int size;
unsigned int totalChunks;
unsigned int chunkSize;
unsigned int start;
unsigned int stop;
void resize(unsigned int currentLength,
unsigned int newLength, int preserve - 1);
unsigned int expandLeft(unsigned int from);
unsigned int expandRight(unsigned int from);
void shrinkLeft(unsigned int from);
void shrinkRight(unsigned int from);

};

Sequences are divided into chunks in accordance with the constructor argument chunkSize. Thus, the client can regulate the size of the future object.

It is clear from the interface that the class Dynamic has much in common with the classes Bounded and Unbounded. The differences in the implementation of the three types of classes in each family will be minimal.

Let us now turn to the class of associative arrays. Its implementation will require a further reworking of the bounded, dynamic, and unbounded forms. In particular, searching for an element in an associative array takes far too much time if it has to be done by scanning all the elements. But performance can be increased significantly by using open hash tables.

The abstraction of an open hash table is simple. The table is an array of sequences that are called buckets. When placing a new element into the table, we first generate a hash code from that element, and then use the code to select the bucket into which the element will be placed. Thus, an open hash table divides a long sequence into several shorter ones, which significantly speeds up searching.

The corresponding abstraction can be defined as follows:

template<class Item, class Value, unsigned int Buckets, class Container>
class Table {
public:

Table(unsigned int (*hash)(const Item&));
void setHashFunction(unsigned int (*hash)(const Item&));
void clear();
int bind(const Item&, const Value&);
int rebind(const Item&, const Value&);
int unbind(const Item&);
Container* bucket(unsigned int bucket);
unsigned int extent() const;
int isBound(const Item&) const;
const Value* valueOf(const Item&) const;
const Container *const bucket(unsigned int bucket) const;

protected:

Container rep[Buckets];

};

Using the class Container as a template argument makes it possible to apply the hash table abstraction independently of the type of the particular sequence. Consider as an example the (greatly simplified) declaration of an unbounded associative array built on the basis of the classes Table and Unbounded:

template<class Item, class Value, unsigned int Buckets,
class StorageManager>
class UnboundedMap : public Map<Item, Value> {
public:

UnboundedMap();
virtual int bind(const Item&, const Value&);
virtual int rebind(const Item&, const Value&);
virtual int unbind(const Item&);

protected:

Table<Item, Value, Buckets, Unbounded<Pair<Item, Value>, StorageManager>> rep;

};

In this case we instantiate the class Table with the container unbounded. Figure 9-12 illustrates how these classes collaborate.

As evidence of the general applicability of this abstraction, we can use the class Table in implementing the classes for sets and bags.

9.3. Evolution of the systems design

Figure 9-12. Support classes.

Tools

For our library the main role of templates is to parameterize structures by the types of the elements they will contain; this is why such structures are called container classes. But, as can be seen from the definition of the class Table, templates can also be used to pass a class certain information about the implementation.

An even more complicated situation arises when creating tools that operate upon other structures. As has already been noted, algorithms too can be represented as classes whose objects act as the agents responsible for carrying out the algorithm. This approach corresponds to Jacobson's idea of a control object, which serves as the connecting link that brings about the interaction of ordinary objects [16]. The advantage of this approach lies in the possibility of creating families of algorithms united by inheritance. This not only simplifies their use, but also makes it possible to unite conceptually similar algorithms.

Consider as an example algorithms for searching for a pattern within a sequence. There are a whole range of such algorithms:

® Simple Searching for the pattern by sequentially checking the entire structure. In the worst case the time complexity of this algorithm will be O(pn), where p is the length of the pattern and n is the length of the sequence.
® Knuth-Morris-Pratt Searching for the pattern with time complexity O(p+n) (Knuth-Morris-Pratt). The algorithm does not require making copies, so it is suitable for searching in streams.
® Boyer-Moore Searching with sublinear time complexity (Boyere-Moore) O(c(p+n)), where c is less than 1 and inversely proportional to p.
® Regular expression Searching for a pattern specified by a regular expression.


All these algorithms have at least three features in common: they all perform operations on sequences (and hence work with objects having a similar protocol), they require the existence of a comparison operation for the type of the elements among which the search is conducted (the standard comparison operator may prove insufficient), and they have the same call signature (the target string, the search pattern, and the index of the element at which the search will begin).

The comparison operation deserves special discussion. Suppose, for example, that there is an ordered list of a firm's employees. We want to search it by a particular criterion, say, to find groups of three records with employees working in one and the same department. Using the operator operator== defined for the class PersonnelRecord will not give the desired result, since this operator most likely performs the check according to a different criterion, for example, the employee's payroll number. Therefore we will have to develop specially for this purpose a new comparison operator that would query (by calling the appropriate selector) the name of the department in which the employee works. Since each agent that performs pattern matching requires its own equality-testing function, we can develop a general protocol for introducing such a function as part of some abstract base class. Consider as an example the following declaration:

template<class Item, class Sequence>
class PatternMatch {
public:

PatternMatch();
PatternMatch(int (*isEqual)(const Item& x, const Item& y));
virtual ~PatternMatch();
virtual void setIsEqualFunction(int (*)(const Item& x, const Item& y));
virtual int
match(const Sequence& target, const Sequences; pattern, unsigned int start = 0) = 0;
virtual int
match(const Sequence&; target, unsigned int start = 0) = 0;

protected:

Sequence rep;
int (*isEqual)(const Item& x, const Item& y);

private:

void operator=(coust PattemMatcb&) {}
void operator==(const PatternMatch&) {}
void operator!=(const PatternMatch&) {}

};

Assignment and equality-comparison operations for objects of this class and its subclasses are impossible, since we have used the corresponding idioms. We did this because assignment and comparison operations make no sense for agent abstractions.

Now let us describe a concrete subclass that defines the Boyer-Moore algorithm:

template<class Item, class Sequence>
class BMPatternMatch : public PatternMatch<Item, Sequence> {
public:

BMPatternMatch();
BMPattemMatch(int (*isEqual) (const Item& x, const Item& y));
virtual ~BMPattemMatch();
virtual int
match(const Sequence& target, const Seque unsigned int start = 0);
virtual int
match(const Sequence& target, unsigned in

protected:

unsigned int length;
unsigned int* skipTable;
void preprogress(const Sequence& pattern);
unsigned int itemsSkip(const Sequence& pattern, const Item& item);

};

9.3. Evolution of the systems design

Figure 9-13. Pattern matching classes.

The public protocol of this class copies exactly the corresponding protocol of its superclass. In addition, its declaration further includes two data members and two helper functions. One of the features of this class consists in the creation of a temporary table that is used to skip long non-matching sequences. These additional members are needed to implement the algorithm.

Figure 9-13 shows the hierarchy of the pattern matching classes. A hierarchy of this kind is applicable to most of the library's tools. This forms families of classes that are similar in structure, which allows users to find their way among them easily and to choose those that best suit their applications.

created: 2020-12-19
updated: 2026-03-09
130



Was this answer useful?
Choose a quick rating so we can improve the next answer for you.
How satisfied are you?


Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Object Oriented Analysis and Design"

Terms: Object Oriented Analysis and Design