Software Engineering (Comp 375) final exam review guide

General things you should be able to do relating to pre-midterm material

1. Be familiar with general SE steps (analysis/design/coding/testing)
2. Be familiar with OO versions of these
3. Be able to devise a reasonable decomposition into classes for a given problem.

It is recommended that you review the midterm exam and the midterm study guide (still online).

Since the midterm, we covered:

Testing

16.1,16.2: basic ideas
16.3: whitebox [glassbox] testing - basic ideas
16.4: Graph-theoretic basis of whitebox testing
Flow graphs
Independent paths and basis sets
Cyclotomic complexity
deriving test cases
16.6: Blackbox testing
Program documents that a blackbox-test designer might use
16.6.3: Boundary value analysis
Generating test cases from state transition diagrams
Exercises to think about: 16.1, 16.13
Given a small procedure (eg a sort procedure), be able to construct a flow graph, find a basis set, and then prepare test cases that force execution of each path (16.4.3)

ch 22: Object-oriented testing
22.2: CRC card enactment
22.4.6: Test cases from use cases
22.4.4, 22.4.5: class testing
Exercises to think about: 22.1, 22.2,

Project Management

3.2: Team models: chief programmer, democratic decentralized,
democratic centralized, controlled centralized
Coordination/communication tools
3.4: the Process; 3.4.2: team choices
Exercises to think about: 3.6-3.9

5.8: Decision Trees; be able to do exercise 5.13.

7.6: Task network
Given a number of stages, be able to make reasonable assumptions about dependencies and then construct a task network graph.

8.5: Technical reviews: scale, scope, and limits
Exercises to think about: 8.5, 8.8, 8.9, 8.10

Software Measures and Metrics

4.3, 4.4: LOC and FP
4.5: Quality measures
Exercises to study: 4.9 - 4.13

18.3: conventional technical software measures (analysis phase)
the Bang metric; primitives; function-strong v data-strong
18.5: source code measures (do not memorize formulas)

23. OO metrics
23.4.1: The CK metrics; issues in OO metric design
WMC, issues in counting class methods, DIT, NOC, CBO, LCOM
(do not memorize acronyms; if I ask about one of these I will explain what it means.)
23.4.2: The LK metrics; collision of NOO measure with Abstract Base Classes
(would the notion that large NOO => design problem apply to Abstract Base Classses?)
Exercises to think about: 23.3, 23.4, 23.5, 23.6, 23.10, 23.11

Software Patterns

You should know what is meant by a Software Pattern, as well as something about the specific patterns of: Singleton, Virtual Ctor (aka clone()), Iterator (and separation of the Iterator interface from the class interface), Composite, and Private Header (used by lists).

Summaries of the patterns are as follows: (to be continued)

Singleton

We arrange for it to be possible to create only a single instance of a class (eg CView, or Document, or PrintSpooler), by making the ctor private and also providing a member function instance() and a private var _instance, initially 0. instance() then works as follows:

Virtual ctor

Providing a virtual member function create(). Some abstract parent class may have to create things, without knowing what they are. The tradeoff is that when create() is actually called, it is in the context of some later-supplied *specific* class that provides a concrete implementation of create().

Example: CView has a CreateDocument() member function. However, CView knows nothing at all about what kind of document to create; that isn't instantiated until the application-specific derived class (eg CWinfigView) is defined. At some places within the abstract portions of MFC, CView does indeed call CreateDocument() for the current View (eg when the "new file" button is pressed). But when we actually execute this, the function that is actually invoked is, say, CWinfigView::CreateDocument, for which we *do* have application-specific information.

Iterator

The most basic idea, for an "internal" iterator, was for a data structure to provide a uniform set of members reset(), next(), current(), and atend().

The next idea was a separate ListIterator class, that essentially corresponded to a pointer to a list element. This meant that list-searching could be const, because the list would not change (only the interator). (With the first method, above, if a list is passed as a const parameter then calling next() etc would be illegal because these iterator functions do change the list state.) It also meant that one could have multiple iterators marking multiple different positions within one list.

Carrying the abstraction a little further, though, the final step was to separate the notion of Iterator from the notion of data structure. An abstract Iterator class was defined:

template <class Item>
class Iterator {
public:
    virtual void reset() = 0;
    virtual void next() = 0;
    virtual Bool atend() = 0;
    virtual Item current() = 0;
protected:
    Iterator();  // ctor is protected but that's not really necessary
};

Now we can, for data structures List, Heap, Hashtable, BinTree, etc, define corresponding classes ListIterator, HeapIterator, HashtableIterator, BinTreeIterator, all derived from Iterator (and friends, most likely, with the corresponding data structure class). Specific iterators could be created, and special-purpose iterator classes that, say, iterated in breadth-first order or in reverse order could be defined.

Most importantly, functions that needed to process all the elements in a data structure could simply take an Iterator as parameter, and the exact details of *what* iterator could be left up to the programmer:

void PrintEmployees (Iterator<Employee> & i) {
     for(i.reset(); !i.atend(); i.next()) i.current().print();
}

Now this could be called with specific iterators:

List<Employee> employees;
ListIterator<Employee> forward(employees);
ReverseListIter<Employee> reverse(employees);
SortedListIter<Employee> sorted(employees);
Tree<Employee> employees2;
TreeIterator<Employee> forward2(employees2);

and we could call PrintEmployees with any of these iterators:

PrintEmployees(forward);
PrintEmployees(reverse);
PrintEmployees(sorted);
PrintEmployees(forward2);

In other words, we have a nice abstract separation between Iterator and the data structures that we are iterating through.

Composite

This is the general model for the situation in winfig with group shapes.  A group shape is a collection of other shapes (primitives or sub-groups) that is to be treated as a unit: it is to be moved, resized, etc together.

There are many other applications of this. For example, in a text editor, the body of the document might consist of glyphs. A glyph is a character, a string of characters (a paragraph), or a picture. More generally, a glyph is a character, picture, or string (composite) of glyphs.

Another example is a tree: a treenode is either a leaf, or is a subtree, ie a composite of leaves/sub-composites.

A Component is either a Composite or a Leaf; Composites are in turn lists of Components.

The programming issue here is whether to place listmember and addmember functions in the Component class, which means figuring out what to do when someone tries to add a member to a Leaf, or in the Composite class, which means that someone has to check whether a given Component is Composite or not before inspecting its members.

The second method is perfectly reasonable, but there is a neat way to get the first to work safely. That is to provide some sort of addmember/getiterator functionality for Leaf objects. addmember might simply always fail when applied to a Leaf. If getIterator() returned an Iterator for a given composite, that would then iterate through the actual constituent Components, then getIterator for a leaf might return a NullIterator or a SingletonIterator.

Private Header

When you have a class with pointers in it, and these pointers are subject to possible deletion/reallocation, a problem arises if these pointers are also copied (ie the object pointed to is shared). If one pointer copy is deleted and reallocated, then the other is now dangling.

Here's a quick example without a class, just bare pointers:

Foo * p = new Foo;
Foo * q = p;
delete q;
q = new Foo; 
// now q points to something valid, but p does not!

One approach is to make sure that pointer assignment is never done; eg by making the copy ctor of the class create a new copy of the actual data pointed to, and have the second pointer point to this new copy. That makes copying expensive, though. Another approach is the private header: the class pointer actually points to another pointer (in a private header class), which in turn points to the actual data. Now, copying the pointers means having multiple pointers to the same private header. But provided we never delete the private headers, only the data to which they point, we're safe. Here is an example, using pointers to pointers. p is the class pointer, *p is the private header, and **p is the actual data.

Foo ** p = new Foo*; *p = new Foo;   // check that these types are ok!
Foo ** q = p;                        // the copy
delete *q;                           // this deletes the shared Foo object
*q = new Foo;                        // and allocates a new one
// now **p is also the newly allocated foo object!

One consequence of this is that now any copy will appear to be a *reference* to the original; anything we do to a copy also affects the original.