CIS-260 Home http://www.c-jump.com/bcc/
Observations about design and its challenges
Be clear about what you are trying to build
Successful software development is a long-term activity
The systems we construct tend to be at the limit of the complexity that we and our tools can handle.
Experimentation is essential for anything nontrivial
Design and programming are iterative activities
It is easy to underestimate the challenges
It is hard to transform the abstract ideas into practice.
Design and programming are human activities; forget that and all is lost.
Need for experience: design cannot be mastered through theoretical study alone.
Just because a technique worked for you last year and for one project, it does not follow that it will work unmodified for someone else or for a different project.
Our discussion here shifts to larger scale software development.
Those who are not involved in such development can sit back and enjoy a look at the horrors they have escaped.
Most fundamental problem in software development is complexity.
Dealing with complexity: divide and conquer!
Two sub-problems are more than half-solved by their separation!
For both people and programs, separation is easy.
This simple principle can be applied in an amazing variety of ways.
implementations are connected only by a well-defined set of interfaces
processes are broken into distinct activities
interactions are well-defined between the people using the system.
Most experience is required when
selecting parts
specifying the interfaces between parts.
A program needs a clean internal structure to ease:
testing
porting
maintenance
extension
reorganization
understanding.
Successful major piece of software
has an extended life
worked on by a succession of programmers and designers
ported to new hardware
adapted to unanticipated uses
repeatedly reorganized.
Not planning for this is planning to fail.
A balance between the lack of design for a piece of software and overemphasis on structure.
For example,
lack of design leads to endless cutting of corners:
"Just ship this one and fix the problem in the next release."
overemphasis leads to multiple delays and unnecessary reorganizations:
"new structure is so much better..."
An organization has to do this despite changes of personnel, direction, and management structure.
Popular approach: a rigid framework of
easy-to-train (cheap) and interchangeable low-level programmers (aka coders), and
somewhat less cheap, but equally dispensable designers.
Coders are not supposed to make design decisions.
Designers are not supposed to concern themselves with the grubby details of coding.
This approach often fails. If it does work, it produces overly large systems with poor performance.
Creating a framework within which people can utilize diverse talents, develop new skills, contribute ideas, and enjoy themselves is not just the only decent thing to do, but also makes practical and economic sense.
A system cannot be built without a formal design structure.
Simply finding the best people and letting them attack the problem as they think best is often a good start for a project requiring innovation.
However, as the project progresses, formalized design communication is needed.
The purpose of design is to create a clean and relatively simple internal structure.
This is known as program architecture.
Examine the problem.
Create an overall design.
Find standard components.
Customize the components for this design.
Create new standard components.
Customize the components for this design.
Assemble the design.
Aiming for universality in an initial design is a...
...prescription for a project that will never be completed.
One reason that the development cycle is a cycle is that it is essential to have a working system from which to gain experience.
flexibility - encapsulation of data
extensibility - polymorphism, known design patterns.
portability - using standard libraries and components where possible.
reuse and efficiency considerations are also important.
Consider designing a single class.
Typically, this is not a good idea: concepts do not exist in isolation;
rather, a concept is defined in the context of other concepts.
Typically, we design a set of related classes.
A set of classes, united by some logical criteria, is a logical component.
A component is thus the unit of design, documentation, ownership, and often reuse.
Consider design consideration for one component:
Any system needs efficiency through common style
and reliance on common services and data.
Does that mean that a user of one class from a component must understand
and use all other classes from the component?
Find the concepts/classes and their most fundamental relationships.
Refine the classes by specifying the sets of operations on them.
Classify these operations. In particular, consider the needs for construction, copying, and destruction.
Consider minimalism, completeness, and convenience.
Refine the classes by specifying their dependencies.
Consider parameterization, inheritance, and use dependencies.
Specify the interfaces.
Separate functions into public and protected operations.
Specify the exact type of the operations on the classes.
Needless to say that these are steps should be an iterative process!
Find the concepts/classes and their most fundamental relationships.
The key to a good design is to model some aspects of "reality" directly – that is,
capture the concepts of an application as classes,
represent the relationships between classes in well-defined ways such as inheritance, and
do this repeatedly at different levels of abstraction.
But how do we go about finding those concepts?
What is a practical approach to deciding which classes we need?
|
|
|
|
Iterations of:
Discovery of classes
Specifying responsibilities of classes (foundation for future interface and implementation)
Capturing collaboration between classes (foundation for future UML use cases)
Best place to look for answers about the design is in the application domain.
Listen to someone who is an expert user of the system.
Listen to someone who is a somewhat dissatisfied user of the current system being replaced.
Note the vocabulary they use:
Nouns will often correspond to the classes and objects needed.
Verbs may denote operations on objects
Verbs may also suggest well-known design patterns, such as "iterate" or "commit".
Some adjectives can also represented useful classes, consider
"storable", "concurrent", "registered", and "bounded".
Before we begin design analysis with CRC, we need to have a Requirements Summary Statement, or similar document, explaining various aspects of the system, such as:
What the system will be able to do, is ...
Intended purpose of this application is ...
The users of software are ...
Business rules are ...
Typical use cases are ...
But not how will the system do all this!
Sample Summary Statement
|
|
|
|
|
|
|
|
|
|
|
|
Finding Classes, continued:
Inheritance (generalization) is used to represent commonality among concepts.
Commonality must be actively sought.
Generalization and classification are high-level activities that yield lasting results.
Classification should be of aspects of our own model, not aspects valid in other areas.
For example, in mathematics a circle is a kind of an ellipse, but in most programs a circle should not be derived from an ellipse or an ellipse derived from a circle. The often heard arguments because that's the way it is in mathematics is not conclusive and most often wrong.
In most systems circle and ellipse will provide independent sets of operations that are not subsets of each other.
A use case is a description of a particular use of a system. For example, of a use
take phone off the hook,
dial a number,
the phone at the other end rings,
the phone at the other end is taken off hook.
Defining a set of use cases helps us understand what we are trying to build:
During design description use cases verify the system from the user's point of view.
Later use cases become a source of test cases.
Attempt to find and describe all of the use cases could be a costly mistake.
How many cases are enough?
Use a common sence-based approach.
Perhaps when most of the ordinary functioning is covered and a fair bit of the more unusual/error handling issues were touched upon.
An example of a secondary use case would be a variant of the "make a phone call" case, in which
the called phone is off hook, and
the called phone is dialing its caller.
the called phone is dialing other number.
Often there is no way to know in advance "all of the cases".
A very formal, out-of-touch presentation of a design is a very dangerous activity.
There is a strong temptation to present an ideal system –
a system you wished you could build,
a system your high management wish they had.
Could it possibly be produced in a reasonable time?
When executives don't really understand or care about "the details", presentations can become lying competitions:
the team that presents the most grandiose system gets to keep its job, and
clear expression of ideas is often replaced by heavy jargon and acronyms.
Distinguish wishful thinking from realistic planning!
A set of use cases is not a design: the focus then shifts to the system architecture.
The concepts, operations, and relationships come naturally from our understanding of the application.
They are added later when further work on the class structure takes place.
Refine the classes by specifying the sets of operations on them.
It is not possible to separate finding the classes from figuring out what operations are needed
However, there is a practical difference in that finding the classes focusses on the key concepts (encapsulated data) and deliberately deemphasizes the computational aspects of the classes.
Alternatively, specifying the operations focusses on finding a complete and usable set of operations.
It is most often too hard to consider both at the same time.
Consider how an object of the class is to be constructed, copied (if at all), and destroyed.
Define the minimal set of operations required by the concept the class is representing.
Consider which operations could be added for notational convenience.
Consider which operations are to be virtual, that is, operations for which the class can act as an interface for an implementation supplied by a derived class.
Consider what commonality of naming and functionality can be achieved across all the classes of the component.
All of the above is a a statement of minimalism! Adding operations too early constrain the implementation and the further evolution of the system.
Remember: adding public operations reduces the level of abstraction from a concept to its implementation!
It is much easier to add a function once the need for it has been clearly established rather than to remove it once it has become a liability.
Focus on what is to be done rather than how it is to be done.
Focus more on desired behavior than on implementation issues.
Classify operations:
Foundation operators: constructors, destructors and copy operators
Inspectors: operations that do not modify the state of an object
Modifiers: operations that do modify the state of an object
Conversions: operations that produce an object of another type based on the value (state) of the object to which they are applied
Iterators: operations that allow access to or use of a sequence of contained objects
Such classifications are especially useful for maintaining consistency across a set of classes within a component.
Key dependencies are parameterization, inheritance, and use relationships.
The need to consider inheritance and use relationships
Component is the unit of design.
The more classes depend on a class, the more general that class should be, and the fewer implementation details it should reveal.
An interface is an abstract class presenting very general operations.
Most important aim of a design is to provide interfaces that can remain stable in the face of changes.
Fat interface: Resist temptation of "making a class more useful" by adding operations.
Instead, keep classes general and abstract, and consider hierarchies of abstract classes.
Most common reorganizations of a class hierarchy are
factoring the common part of two classes into a new class, and
splitting a class into two new ones.
The result is three classes: a base class and two derived classes.
When should such reorganizations be done? Indicators that such a reorganization is due might be:
If commonality between classes is observed, then factor out the common part. Also,
common patterns of use,
similarity of operations,
similarity of implementations.
Software design is hard, and we need all the help we can get.
Using patterns is a design reuse technique.
Designers must be acquainted with popular patterns in a given application domain.
Programmers should prefer patterns that have some code associated with them as concrete examples.
Large system must have a formal model from which to work.
For a large systems, it is ideal to have a somewhat smaller, related working system, known as prototype.
Very successful when done well, a prototype approach is:
a scaled-down version of the system or a part of the system,
using reduced performance criteria,
enabling exploration of design and implementation choices,
invaluable when designing a user interface.
Danger of having the prototype become a product:
if design effort is not invested in the internal structure of the prototype, it could become
a maintenance nightmare while giving the illusion of an "almost complete" system,
an example of system of poor efficiency and scalability,
time and energy burner while that could have been better spent on the product.
Throw a prototype away before it becomes a burden!
|
|
|
|
|
|
|
|
|
|
|
|