Re: Managing a project as it scales
- From: Joseph M. Newcomer <newcomer@xxxxxxxxxxxx>
- Date: Fri, 16 Dec 2005 22:05:32 -0500
Every function I write has a standard header. It is a comment my editor adds as part of
the act of creating the empty function body, and it looks like this:
/**************************************************************************************
* CMyClass::DoSomething
* Inputs:
*
* Result:
*
* Effect:
*
**************************************************************************************/
I will then document the parameters, the result and its meaning, and the effect, e.g.,
* Inputs:
* const CString & filename: The name of the file to be opened for output
* UINT count: The number of elements to write to this file
* Result: BOOL
* TRUE if successful
* FALSE if error (caller uses ::GetLastError)
* Effect:
* Opens the file and writes out 'count' elements. This includes writing out
* the file header. If the file already exists, it is replaced.
* Notes:
* File format
* +------------------------------+
* | int FileVersion | 0
* +------------------------------+
* | count | 4
* /+------------------------------+
* | | COLORREF color | 8 + recno * sizeof(record)
* < +------------------------------+
* | | BOOL active | 12 + recno * sizeof(record)
* \ +------------------------------+
* | |
* : :
****************************************************************************************/
The Notes: section is often the longest section, sometimes longer than the code. Not all
functions need Notes: sections, which is why I don't have it automatically added in the
prototype.
Note that I can't control the font, so depending on what font you are using, you may or
may not see what should be a sequence of rectangular boxes.
I left drawings like this in the program I described in an earlier post. I also will put
in decision tables, e.g.
* Notes:
* case ref count
* -----------------------------------------------
* A. NULL == 0 Nothing initialized, nothing to do
* B. NULL > 0 Create new ref table to hold elements
* C. ~NULL == 0 Nothing to add, nothing to do, should not happen
* D. ~NULL > 0 Append elements to table
if(ref == NULL && count == 0)
{ /* A. */
// nothing to do
} /* A. */
else
if(ref == NULL && count > 0)
{ /* B. */
ref = new Table;
append(ref, count);
} /* B. */
else
if(ref != NULL && count == 0)
{ /* C. */
ASSERT(FALSE); // actually, this should never happen
} /* C. */
else
{ /* D. */
append(ref, count);
} /* D. */
While the code is simplistic, it follows the documentation and is easier to understand and
debug. When confronted with complex decision tables, write out the comments first, and
only then implement the code.
Note also that my editor forces every { } block to have a block comment, and when I type
the matching }, it grabs the correct comment from the open brace.
If you have ever once printed out a listing so you could circle the braces to figure out
who matched what, you should instantly recognize that any style of braces that does not
make this trivially obvious is a poor programming style. Typically, you want to minimize
nesting depth anyway, but braces should follow a methodology that is easy to read. As
human beings, we are not very good at counting blank spaces, or dealing with nested
contexts (any good introductory book on cognitive psychology will explain this). So it is
important to never, ever adopt styles that work directly against us. As far as I can
tell, nearly every brace convention mechanism is designed to exacerbate what we do poorly.
I once worked with a compiler, in the early 1970s,that actually took block comments and
forced matching (begin "name" and end "name" were the block delimiters), and that taught
me...35 years ago...that no methodology that did not provide for trivially-decodable
nesting is a flawed methodology.
Rarely is it valuable to maintain external documentation, although I've done it. I once
wrote an 80-page manual on how to support one product I did. It was clear that certain
features were potentially open-ended, so I made them table driven. I described in detail
how to add new entries to the dispatch table, how to write handler methods, how they
interacted with the app, what the most useful methods were to call, etc. How to add new
files to the build (this was pre-VS). How to handle per-file customization (we used the
Microsoft C 3.0 command-line compiler). How to add new overlays. How to read and
interpret the instrumentation for performance optimization. I got a call from the client
about once every three months for the next several years, each time starting out, "The
documentation is wonderful, but we aren't sure how to..." which usually required a new
paragraph or in the worst case new chapter in the documentation, until we converged on
something they could hand to unskilled labor and say "Add a Gloop feature to the Mumble
component" and the person could have in running in a few days.
Remember: unskilled labor is usually the new hire, or yourself six months later. The only
thing that saved me from disaster when I got that project back after five years was the
detailed documentation I put in.
Even the message handlers get procedure header blocks. No system I deliver fails to have
complete headers for each and every function.
Note that Window messages are by definition functions. A user-defined message always has
(I'll eliminate the long **** lines)
/****...****
* UWM_LOG_MESSAGE
* Inputs:
* WPARAM: (WPARAM)(CString *) pointer to the string to be displayed. This
* *must* be heap-allocated!
* LPARAM: unused, should be 0
* Result: LRESULT
* Logically void, 0, always
* Effect:
* Logs the string to the message log
* Notes:
* It is the responsibility of the recipient of the message to delete the WPARAM
* value when it is no longer needed
*****...****/
#define UWM_LOG_MESSAGE_MSG _T("UWM_LOG_MESSAGE-<guid here>")
A window message is an interface, and all interfaces are completely documented.
This particular message will be a registered window message, e.g.,
static UINT UWM_LOG_MESSAGE = ::RegisterWindowMessage(UWM_LOG_MESSAGE_MSG)
or I use the DECLARE_MESSAGE macro I write:
#define DECLARE_MESSAGE(x) static UINT x = ::RegisterWindowMessage(x##_MSG);
so to get the message, I'll write
DECLARE_MESSAGE(UWM_LOG_MESSAGE)
by such techniques, I reduce the complexity of my code, and thoroughly document it.
joe
On Tue, 13 Dec 2005 10:28:49 -0600, BobF <rNfOrSePeAzMe@xxxxxxxxxxx> wrote:
>On 13 Dec 2005 07:52:41 -0800, Alexander wrote:
>
>> I'm a hobbyist programmer working with VC++6.0
>>
>> My projects used to be small and manageable, say, 5+ dialogs and 25+
>> classes. As I develop more complex apps, managing the number of files
>> is getting difficult and I am having problems managing larger projects.
>> At 10+ dialogs with 50+ classes, things start to get out of hand. At
>> 100+ classes, I struggle.
>>
>> How do you guys cope with large projects?
>
>Nobody else has mentioned this yet, so I'll say it:
>
>"Documentation"
Joseph M. Newcomer [MVP]
email: newcomer@xxxxxxxxxxxx
Web: http://www.flounder.com
MVP Tips: http://www.flounder.com/mvp_tips.htm
.
- Follow-Ups:
- Re: Managing a project as it scales
- From: BobF
- Re: Managing a project as it scales
- From: Alexander
- Re: Managing a project as it scales
- References:
- Managing a project as it scales
- From: Alexander
- Re: Managing a project as it scales
- From: BobF
- Managing a project as it scales
- Prev by Date: Re: DoModal isn't reentrant but failure mode could be improved
- Next by Date: Re: From VC6 to .NET 2 questions
- Previous by thread: Re: Managing a project as it scales
- Next by thread: Re: Managing a project as it scales
- Index(es):
Relevant Pages
|