"Design refers to the human endeavor of shaping objects to purpose."
-- D. N. Perkins, "Knowledge as Design"
"The essence of design is to balance competing goals and constraints."
-- Kernighan & Pike, "The Practice of Programming"
"KISS: Keep It Simple S_____"
-- unattributed
--------------------------------------------------
Eric S. Raymond describes "The Hacker Attitude":
1. The world is full of fascinating problems waiting to be solved.
2. No problem should ever have to be solved twice.
3. Boredom and drudgery are evil.
4. Freedom is good.
5. Attitude is no substitute for competence.
( http://tuxedo.org/~esr/faqs/hacker-howto.html )
--------------------------------------------------
--------------------------------------------------
--------
Pointers
--------
Dangling pointers:
T * ptr = new T();
...
delete ptr;
...
ptr->...; // oh no !
...
delete ptr; // double deletion !
- Why is this bad ?
- How to avoid:
delete ptr; ptr = 0;
- Why is it better to dereference a null pointer than
a dangling pointer ?
- in the former case, you are guaranteed to be
stopped dead in your tracks (e.g. SIGSEGV),
unless you're running DOS
Allocation/deallocation:
- should happen at the same level (or layer)
- e.g., this is bad:
class String {
char * s;
...
public:
...
// returns a copy of the string
char * getCopy() const {
char * copy = new char[ strlen(s)+1 ];
strcpy( copy, s );
return s;
}
};
- Why is this bad ?
- Client doesn't know if memory was allocated with malloc() or new
- Client may not realize they have to deallocate
- When client is obliged to allocate first, client can choose
the method of allocation, and it is (implicitly) clear
who should deallocate.
- How can client know how much to allocate ?
- What if it's really difficult/inefficient for the client to
find out ahead of time ?
- Then the client can pass in an object that wraps around
the data to be returned, such as an STL container object
- Another tactic is to use smart pointers (objects that
transparently wrap around a dynamically allocated object,
and that use reference counting to automatically detect
when to delete). Then it doesn't matter who "owns"
the object.
--------------------------------------------------
------------
Optimization
------------
Let your optimizer do your dirty work for you.
It's better to keep your code readable.
- Don't
int array[10];
...
int * p = array;
for ( int i = 0; i < 10; ++i ) {
*p = ...;
++p;
}
Instead, do
int array[10];
...
for ( int i = 0; i < 10; ++i ) {
p[i] = ...;
}
- Don't
x >> 1
Instead, do
x / 2
(Exception: if it's more natural to think in terms of bit
shifting, do a bit shift.)
- Don't
x & 7
Instead, do
x % 8
(Exception: if it's more natural to think in terms of
bit ANDing, do the former.)
On the other hand, there are certain optimizations
that should be fairly obvious to do, and that don't
affect legibility. For example,
- Don't
if ( ptr == NULL && ... )
...
else if ( ptr == NULL && ... )
...
Instead, do
if ( ptr == NULL ) {
if ( ... )
...
else
...
}
--------------------------------------------------
--------------------------------------------------
-----------
Refactoring
-----------
- goal: minimize (hidden) dependancies between different parts of code
and to centralize definitions of code/data/comments
- means the final line count of your code is smaller
(but you may have to work more to get there)
- makes your code more maintainable
(i.e. easier to modify in the future)
and easier to understand
- can rarely be done right the first time (except in simple cases)
- iterative process, requires work
- ultimately, the programmer discovers the simplest
set of primitives necessary to accomplish a task
Refactoring can and should be done with
- code
- constants (data)
- comments
- anything else you can think of
--------------------------------------------------
Extreme Programming (XP) principles tell us to
"Refactor mercilessly to keep the design simple as you go
and to avoid needless clutter and complexity"
"[...] always do the simplest thing that could possibly work."
The Camel book (Programming Perl) tells us that
The three principle virtues of a programmer
are Laziness, Impatience, and Hubris.
--------------------------------------------------
--------------------------------------------------
Source code as literature
- Most time spent in software development is not spent writing
new code, but rather debugging and modifying existing code.
- It's easier to write code than to read it :(
(Joel Spolsky)
- Program source code should be designed for human consumption first.
The computer's need to interpret and execute code is secondary.
- Literate programming (Knuth)
- "A methodology that combines a programming language with
a documentation language, thereby making programs more robust,
more portable, more easily maintained, and arguably more fun to
write than programs that are written only in a high-level language.
The main idea is to treat a program as a piece of literature,
addressed to human beings rather than to a computer."
--------------------------------------------------
Identifier names - provide implicit documentation
Often, a well chosen variable or function name makes fewer comments necessary.
This is good:
class X {
public:
// Construct me with priority p and description d.
X( const int p, const char* d );
...
}
But this is better:
class X {
public:
X( const int priority, const char* description );
...
}
--------------------------------------------------
--------
Comments
--------
- too few are bad
- too many are bad
- duplicate comments
- Why are they bad ?
- redundant comments
- You should assume the reader knows the syntax of the language.
- comments should be in the right place
- In the header file:
- brief comments on data members of a class
- stuff that clients of your module need to know
(e.g. what each public method of a class does,
any special requirements or assumptions made about
input, and policies for error reporting)
- In the .cpp file
- Comments specific to the implementation or
private portions of the module
- Just like variables, comments should be "scoped" to be close
to the code they talk about.
- should not introduce more dependancies than necessary
for the programmer to manage
- avoid referring to variable names in comments.
- try to make it easier for other programmers to modify your code
and see immediately what comments they have to update
Example from 1st assignment:
What's wrong with the above code ?
- comments are too long and too decoupled from the
corresponding code
- comments could easily get out-of-sync with the code
Another example from 1st assignment:
/*
formUnionWith()
The function first checks to make sure that the set to be
unioned with is not empty. If it is empty, no further
operation is necessary. [...]
*/
void Set::formUnionWith( const Set & set ) {
int nMember = set.getSize();
if ( nMember == 0 )
return;
...
}
This is better:
void Set::formUnionWith( const Set & set ) {
if ( set.getSize() == 0 ) {
// Set to be unioned with is empty; there's nothing to do.
return;
}
...
}
Another example from 1st assignment:
////////////////////////////////////////////////////
// First checks to make sure the list is not empty,
// then makes sure that the element is there to delete.
////////////////////////////////////////////////////
if ( !isEmpty() && NodeSearch(element, formerNode) ) {
...
}
This is better:
if ( !isEmpty() && NodeSearch(element, formerNode) ) {
...
}
else {
// The node isn't there to delete.
}
General principle:
Strive for economy of expression, in both code *and* comments !
--------------------------------------------------
-----------
Correctness
-----------
Defensive programming
Be conservative. When in doubt, assume whatever is safest.
(This applies both to communicating with clients and
when acting as a client ourself.)
- e.g. can routine foo() return a null pointer ?
- e.g. if read() returns EOF, is it still legal to call read()
again on the same file, or will my program crash ?
Write code to test your own code (i.e. to perform "sanity checks").
Example: if you write a sorted list class, test to make sure it's
sorted after every insertion and deletion.
Assertions
- help to detect and pinpoint bugs, often long before a human tester
notices anything is wrong
- provide implicit way of documenting your code,
and of (partially) confirming (at run time!) assumptions made by the coder
- can be easily removed with a compiler switch for optimized speed
- not good as a general error-handling facility.
Should only be used to detect bugs that are internal to a system.
If an external client (including a human user) misbehaves
or does something illegal, they should be informed in
some way, so that they can handle (or ignore) the error as they choose.
Meanwhile, the internal system should recover from the error
as gracefully as possible.
(In general, errors due to external conditions should be detected
at a low level and handled at a high level.)
Design your code to make errors as obvious as possible
to a human inspector. Avoid weirdness like this:
T* ptr = new T[ N ];
for ( int i = 0; i < N; ++i )
foo( *ptr++ );
delete [] ( ptr - N );
--------------------------------------------------
--------------------------------------------------
------------------------
Design vs Implementation
------------------------
- The cost of changing a design increases very fast
as you progress into the implementation stages.
Try to get a complete design done early
(if not of the entire implementation, then at least
of the modules and interfaces between them).
- On the other hand, John Cage tells us that not knowing
where to start is a common form of paralysis.
His advice: start anywhere.
- If you adopt Cage's advice, when your start writing a
complex piece of code, start with the easy stuff first.
Leave complicated, special cases for last.
--------------------------------------------------
--------------------------------------------------
-----------
Your Health
-----------
- Beware of RSI ! (repetitive strain injury)
- it's for real
- it really sucks
--------------------------------------------------
--------------------------------------------------
"La perfection est atteinte non quand il ne reste rien a ajouter,
mais quand il ne reste rien a enlever."
-- Antoine de Saint-Exupery
(author of Le Petit Prince)
The above serves as a good litmus test for
elegance in design.