Code Complete
Michael Parker, own this book and took these notes to further my own learning. If you enjoy these notes, please purchase the book!
Chapter 5: Design in Construction
5.1: Design Challenges
Design is "wicked": You must "solve" the problem once in order to define it, and then solve it again to create a solution that works.
Design relies on heuristics, and relies on trial-and-error.
5.2: Key Design Concepts
Managing complexity is the most important technical topic in software development.
Complexity is reduced by dividing a system into subsystems that are ideally independent.
Some desirable characteristics: simple, loosely-coupled, extensible, reusable, lean, stratified, standardized.
Make subsystems meaningful by restricting communications and preventing cycles.
5.3: Design Building Blocks: Heuristics
Real-World Objects: Identify objects' public/protected/private attributes, then public/protected interfaces.
Form abstractions at the right level, allowing you can ignore irrelevant details
Encapsulate; abstraction provides a high level of detail, while encapsulation says you can't change levels.
Information hiding promotes secrets: hiding complexity for easier understanding, or hiding sources of change so its effects are localized.
Asking what a class should hide cuts to the heart of interface design.
Identify and isolate areas likely to change, like nonstandard language features, bad design or construction, or data-size constraints.
Keep coupling loose; one module using some semantic knowledge of a module's inner workings is especially bad.
Design patterns provide vocabulary for efficient communication, and embody accumulated wisdom over years.
Other heuristics: aim for strong cohesion, use preconditions and postconditions, design for test, and keep design modular.
Don't get stuck on a single approach; if you are stuck on all approaches, step away for a bit.
5.4: Design Practices
Iterate; when you come up with something that seems good enough, don't stop, but instead apply what you learned on a second design.
Top-down design is a decomposition strategy, while bottom-up design is a composition strategy.
Top-down design is easy and you can defer construction details.
Bottom-up design typically results in early identification of needed utility functionality.
Prototyping fails when developers don't write the absolute minimum code, and so don't treat the code as throwaway.
Big design problems found not to come from bad designs, but from areas deemed too easy for any design at all.
Capture design work in code comments, on a Wiki, with photos of whiteboards, or UML diagrams.
Chapter 6: Working Classes
6.1: Class Foundations: Abstract Data Types (ADTs)
ADTs hide implementation details, isolate changes, promote informative interfaces, highlight correctness, provide a private namespace, and build on lower-level data.
A class is an ADT with inheritance and polymorphism added in.
6.2: Good Class Interfaces
Each class should implement only one ADT; mixed abstractions move implementation details to the public interface and complicate understanding.
If a subset of a class' methods operate on a subset of its data, move the data and methods into a new class.
Minimize assumptions by the programmer to use an interface; have the compiler or its own form enforce the requirements.
Only add public members to a class that are consistent with its abstraction, even if a convenient utility method.
Abstraction provides models allowing you to ignore implementation details, while encapsulation enforces this principle.
Looking at a class' implementation to determine its use breaks encapsulation, and breaking abstraction isn't far behind.
6.3: Design and Implementation Issues
Don't re-use names of non-overrideable methods from the base class in a derived class.
Move common interfaces, data, and behavior as high as possible in the inheritance tree.
Be wary of classes with only one instance (excluding singletons), and base classes with only one subclass.
A subclass overriding a method to do nothing violates the interface contract and should be addressed in the base class.
Inheritance works against managing complexity and so you should bias against it.
Keep class interfaces small, the implementations insulated, and minimize its collaboration with other classes.
6.4: Reasons to Create a Class
The best reason to create a class is to hide information, thereby reducing complexity.
Classes also isolate complexity, hide implementation details, streamline parameter passing, promote code reuse, and package related operations.
Avoid god classes; if a class retrieves its data from and stores its data in a god class, move that data.
Chapter 7: High-Quality Routines
A bad routine has bad names, bad layout, multiple purposes, too many parameters, poor documentation, uses global variables, and doesn't defend against bad data.
7.1: Valid Reasons to Create a Routine
The most important reason to create a routine is to reduce complexity; using a routine doesn't require knowing its inner workings.
Routines can encapsulate or hide the assumption about the order in which operations must be performed or routines are called.
Putting complicated boolean tests in routines hides the details, summarizes its purpose, and emphasizes its significance.
No block of code is too small to put into a routine, especially if it improves readability.
7.2: Design at the Routine Level
A cohesive routine contains operations that are related; otherwise, it probably does more than one thing.
7.3: Good Routine Names
A routine with long, complicated name may stem from the routine doing too much; break the routine into multiple routines.
Name functions after the returned value; name procedures with a strong verb and an object to provide its context.
7.5: How to Use Routine Parameters
Put parameters in input-modify-output order; if several routines use similar parameters, order the parameters consistently.
If you consistently pass too many arguments to a function, the coupling among your routines is too tight.
Pass the variables or objects that a routine needs to maintain its interface abstraction.
Chapter 8: Defensive Programming
8.1: Protecting Your Program From Invalid Inputs
To handle "garbage-in," check values from external sources, values of method parameters, and then decide how to handle bad inputs.
8.2: Assertions
Use error handling code for conditions you expect to occur; use assertions for conditions that should never occur.
Use assertions to document precondition and postconditions. Don't put executable code in one.
8.3: Error Handling Techniques
Correctness means never returning an inaccurate result; robustness means always trying to do something that allows the program to keep running.
Beware using a neutral value, substituting the next valid data, returning the last result, or substituting the closest legal value.
8.4: Exceptions
Exceptions weaken encapsulation by requiring the caller to know which exceptions might be thrown from the code that's called.
8.5: Barricade Your Program to Contain the Damage Caused by Errors
If public methods of a class checking and sanitizing data, then private methods can assume it is safe.
Convert data to the proper type ASAP; otherwise you increase complexity and increase the chance that someone can crash your program.
8.7: Determining How Much Defensive Programming to Leave in Production Code
Remove code that results in hard crashes in production; but during development, this is invaluable for debugging.
Chapter 9: The Pseudocode Programming Process
9.2: Pseudocode for Pros
When writing pseudocode, avoid syntactic elements from the target programming language.
Catching errors at the "least-value stage," or when the least effort has been invested, contributes to success.
9.3: Constructing Routines by Using the PPP
TODO
Chapter 10: General Issues in Using Variables
10.3 Guidelines for Initializing Variables
Ideally, declare and define each variable close to where it's used, following the principle of proximity.
Initialize named constants once; initialize variables with executable code, such as in a
Startup()
method.
10.4: Scope
A span is the number of lines between successive variable uses; live time is the number between its first use and its last.
To help minimize scope, begin with most restricted visibility, and expand the variable's scope only if necessary.
Maximizing scope may make programs easy to write, but a program in which any method can use any variable at any time is harder to understand.
10.6: Binding Time
The earlier the binding time, the lower the flexibility and the lower the complexity. So add only as much flexibility as needed.
10.8: Using Each Variable for Exactly One Purpose
Use variables for only one purpose; avoid having different values for the variable mean different things (like negative values).
Chapter 11: The Power of Variable Names
11.1: Considerations in Choosing Good Names
A good name tends to express the "what" more than the "how."
To avoid
numSales
versussaleNum
confusion, consider variable names likesalesTotal
,salesCount
, andsalesIndex
.
11.2: Naming Specific Types of Data
Intermediate variables do not warrant a name like
temp
. Such a name may indicate that we aren't sure of their real purposes.Give boolean variables names that imply true or false, like
sourceFileFound
orisStatusOk
instead ofsourceFile
andstatus
.
11.4: Informal Naming Conventions
Variable names can contain: The variable contents, the kind of data, and the scope or visibility of the variable.
11.6: Creating Short Names That Are Readable
When shortening variable names, don't remove just one letter, be consistent, create pronounceable names, and avoid mispronunciation.
11.7: Kinds of Names to Avoid
Avoid names with similar meanings, like
fileNumber
andfileIndex
.Avoid names with different meanings but similar names, like
clientRecs
andclientReps
.
Chapter 12: Fundamental Data Types
12.1: Numbers in General
By replacing magic numbers with constants, changes are reliable, changes can be made easily, and your code is more readable.
12.3: Floating-Point Numbers
To increase accuracy when adding numbers with differing magnitudes, add them starting with the smallest values.
Avoid equality operations and anticipate rounding errors; cope by switching to greater precision, BCD, or integer variables.
12.4: Characters and Strings
To avoid endless strings in C, initialize strings to null, and use
strncpy()
instead ofstrcpy()
.
12.6: Enumerated Types
Use enumerated types for more type-checking, and as a richer alternative to boolean variables.
Explicitly assign their values to specify first and last values for iteration, and an invalid or "null" type.
12.8: Arrays
In C, use or define an
ARRAY_LENGTH()
macro as#define ARRAY_LENGTH(x) (sizeof(x) / sizeof(x[0]))
.
12.9: Creating Your Own Types
Don't name a type created using
typedef
after the underlying data type, and don't refer to predefined types.
Chapter 13: Unusual Data Types
13.1: Structures
By passing only one or two fields from a structure into a method, you promote information hiding from the method.
13.2: Pointers
Symptoms of pointer errors tend to be unrelated to causes of pointer errors.
By isolating pointer operations to methods, you minimize the possibility of propagating careless mistakes through your program.
Allocating dog tags allow you to check for freeing memory twice, or overwriting memory beyond the last byte.
Free pointers at the same scoping level as they were allocated, such as in the same method, or a constructor/destructor pair.
Set a pointer to
NULL
after deallocation; writing to it produces an error, and deallocating twice is more easily caught.In C++, a reference cannot point to
NULL
and the object it refers to cannot be changed.In C, you can use
char
orvoid
pointers for any type of variable.
13.3: Global Data
Passing a global variable to a method, and then referring to both the parameter and global variable is especially tricky.
Initialization order among different "translation units," or files, is not defined in languages like C++.
Try to contain a global variable as a class variable, and provide an accessor for any other code that needs it.
Replace global data with access methods to centralize control over it and protect yourself against changes.
Build access methods at the level of the problem domain rather than at the level of the implementation details.
Chapter 14: Organizing Straight-Line Code
14.1: Statements That Must Be in a Specific Order
Organize code so that dependencies are obvious; if one method initializes data, create and call an
Initialize()
method.
14.2: Statements Whose Order Doesn't Matter
Statements that operate on the same data, perform similar tasks, or have ordering dependencies should appear together.
Chapter 15: Using Conditionals
15.1: if
statements
For both readability and performance, write the nominal path through the code first, then the unusual cases.
Simplify complicated conditional expressions with calls to methods that return boolean values.
15.2: case
statements
Some
case
statements only work on data of certain types; don't create a "phony variable" to use a case statement.Use the
default
statement to detect legitimate defaults, or to detect errors, and nothing else.
Chapter 16: Controlling Loops
16.1: Selecting the Kind of Loop
If you don't know ahead of time exactly how many times you’ll want the loop to iterate, use a
while
loop.Don't code a "loop and a half"; instead, loop forever and break in the middle.
Keep
for
loops simple; if you're explicitly changing the index value, consider awhile
loop.
16.2: Controlling the Loop
Put initialization code immediately before the loop.
Use
for (;;)
orwhile (true)
to write an infinite loop; don't fake it by iterating to a large number.Reserve the
for
loop header for initializing the loop, terminating it, and moving toward termination.Keep statements that control the loop, or move it toward termination, near its beginning or end.
Don't change the index of a
for
loop to make it terminate.Avoid using the loop index value after the loop; instead assign a final value to a variable at the appropriate point inside the loop.
Using a
break
forces the person reading your code to look inside the loop for an understanding of the loop control.Inefficient programmers experiment randomly until they find something that works, perhaps replacing a bug with a more subtle one.
Chapter 17: Unusual Control Structures
17.2: Recursion
For most situations, recursion produces very complicated solutions that chew up stack space. Use it selectively, and prefer iteration.
For local variables in recursive functions, use
new
to create objects on the heap as opposed to on the stack.
17.3: goto
The use of a
goto
statement defeats some compiler optimizations, which rely on orderly flow control.The
try
-finally
construct can sometimes be used to perform the error cleanup that agoto
sometimes performs.Measure the performance of any
goto
statement used to improve efficiency.
Chapter 18: Table-Driven Methods
18.1: General Considerations in Using Table-Driven Methods
With a table driven method, you must address how to look up entires, and what data should be stored in the table.
18.2: Direct Access Tables
A table driven approach generates less code and is easier to change without the need to recompile.
Put a lookup key transformation in its own method to guard against different transformations in different places.
18.4: Stair-Step Access Tables
This puts the upper end of consecutive ranges into a table, and works well with a binary search for larger lists.
Chapter 19: General Control Issues
19.1: Boolean Expressions
Even if a complicated conditional expression is only used once, moving it into its own method is useful for improving readability.
Organize numeric tests so that they follow points on a number line.
Comparing a character against
\0
instead of0
reinforces that the expression works with character data instead of logical data.
19.3: Null Statements
Null statements are uncommon, so make them obvious, such as a comment inside the braces explaining why one is used.
19.4: Taming Dangerously Deep Nesting
Use a
break
block, try to flatten, move some of the nested blocks into their own methods, or use polymorphism.Complicated code is a sign that you don't understand your program well enough to make it simple.
19.5: A Programming Foundation: Structured Programming
The core of structured programming is the simple idea that a program should use single-entry, single-exit control constructs.
Chapter 23: Debugging
23.1: Overview of Debugging Issues
If you don't know what you're telling the computer to do, you're programming by trial and error, and defects are guaranteed.
If your code has a bug, don't blame the compiler, and don't blame the computer. It's your fault.
23.2: Finding a Defect
Locating a defect is like using the scientific method: gather data, formulate a hypothesis, and prove it.
An error that doesn't occur predictably usually results from an initialization error or dangling pointer problem.
Don't just find a test case that produces the error; reduce the test case to the simplest form possible.
If the data doesn't fit the hypothesis, don't discard the data; instead, ask why it doesn't fit, and create a new hypothesis.
Use all available tools to find an error: interactive debuggers, static analysis, memory inspection, and so on.
You often discover your own defect in the act of explaining it to another person. Or try taking a break from the problem.
If you have a syntax error, try removing part of the code and compiling again.
23.3: Fixing a Defect
Understand and fix the problem. Don't fix the symptom; such solutions are incomplete and unmaintainable.
After you make a fix, check it again, and look for similar defects.
23.4: Psychological Considerations in Debugging
Choose variable names that can be easily differentiated from each another.
23.5: Debugging Tools, Obvious and Not-So-Obvious
Set your compiler's warning level to the highest setting, and treat them as errors so that you fix them.
Debuggers allow full examination of data, including structured and dynamically allocated data.
The debugger isn't a substitute for good thinking; the most effective solution is using both together.
Chapter 24: Refactoring
24.1: Kinds of Software Evolution
If you treat modifications as opportunities to tighten up the original design of the program, quality improves.
You know much more after you've written a program; use what you've learned to improve it.
24.3: Reasons to Refactor
Duplicate, long, or nested code, and poor cohesion, abstraction, encapsulation, or information hiding are good reasons.
Never write speculative code or design ahead; it adds complexity, is likely untested, and is unlikely to meet requirements.
24.4: Specific Refactorings
Data level refactorings include naming constants, renaming variables, and introducing intermediate variables.
Statement level refactorings include simplifying boolean expressions, using
break
orreturn
, and swapping conditionals and polymorphism.Routine level refactorings include extracting methods, combining them by parameterizing them, and passing fields or complete objects.
Class implementation refactorings include pulling up or pushing down data or methods, and separating or combining classes with subclasses.
Class interface refactorings include swapping inheritance with delegation, and encapsulating or exposing member variables.
System level refactorings include swapping factory methods with constructors, and error codes with exceptions.
24.5: Refactoring Safely
Keep refactorings small, do one at a time, review the changes thoroughly, and test them.
24.6: Refactoring Strategies
Refactor as you add code, and target error-prone or high-complexity modules especially.
Chapter 25: Code-Tuning Strategies
25.1: Performance Overview
Performance is only loosely related to code speed; when you focus on speed, you ignore other quality characteristics.
The mere act of making resource goals explicit improves the likelihood that they'll be achieved.
Code tuning is the practice of modifying correct code so that it runs more efficiently.
25.2: Introduction to Code Tuning
Complete the code first, and then perfect it. By the Pareto principle, the part that needs to be perfect is usually small.
For a given language, compiler, architecture, and hardware, you must always measure performance to evaluate your changes.
Don't optimize bottlenecks as you go, as they'll monopolize your attention, and they'll likely not be the biggest ones anyway.
Optimizing compilers are better at optimizing straightforward code than they are at optimizing tricky code.
25.3: Kinds of Fat and Molasses
Operations that swap pages of memory are slow; for example, consider the iteration order of a nested loop.
Errors like logging debug information to a file, leaking memory, and bad schema design can also affect performance.
Polymorphic method calls are slightly more expensive than not, while transcendental math functions are extremely expensive.
25.4: Measurement
Use the number of CPU clock ticks allocated to your program rather than the time of day, so that it's unaffected by multitasking.
Chapter 26: Code-Tuning Techniques
Optimizations degrade the internal structure of a program, otherwise they'd be considered standard coding practice.
26.1: Logic
Use
break
orreturn
in loops, order conditional tests by frequency, replace conditionals with table lookups, and evaluate lazily.
26.2: Loops
Bottlenecks in a program are often inside loops because these loops are executed many times.
Putting loops inside conditionals instead of conditionals inside loops is faster, but two loops must now be maintained.
Don't compute the same value inside a loop repeatedly.
Put the loop with the one with the largest iteration bound on the inside, so that it contributes less to the total iterations.
Replace an expression that multiplies the loop index by a factor with addition on each iteration and a cumulative sum.
26.3: Data Transformations
Prefer integers to floating point numbers when you can.
Similar to minimizing accesses to pointers, introduce temporary variables to minimize repeated access to array elements.
Introduce supplementary data or indexes, or cache the data or memoize results.
26.4: Expressions
Initialize constant values at compile time instead of at runtime.
By replacing common subexpressions with an intermediate variable, you also improve the readability of a program.
26.6: Recoding in Assembler
Save the assembler output from your compiler, and use it as a starting point for any optimization.
Chapter 31: Layout and Style
31.1: Layout Fundamentals
The Fundamental Theorem of Formatting is that good visual layout shows the logical structure of a program.
The smaller part is writing code that the computer can read; the larger part is writing code that others can read.
Structure helps experts to perceive, comprehend, and remember important features of programs.
31.2: Layout Techniques
A paragraph of code should be identified with a blank line, and contain statements that accomplish a single task.
Use more parentheses than you think you need.
31.4: Laying Out Control Structures
Blank lines can improve code by opening up natural spaces for comments.
For complicated conditional expressions, put each group of related expressions on its own line if possible.
31.5: Laying Out Individual Statements
Break up a multi-line statement so that the first line is blatantly incorrect syntactically if it stood alone.
When breaking a line, keep related elements together, such as array references, method arguments, and so on.
With one statement per line, code reads from top to bottom, and mapping line numbers to statements is unambiguous.
C++ does not define the order in which terms in an expression or arguments to a method are evaluated.
If your list of variables is so long that alphabetical ordering helps, your method is probably too big.
In C++, putting the asterisk next to the type name wrongly suggests that all variables on the line are pointers.
31.6: Laying Out Comments
Preceding comments with a blank line helps the reader scan the code.
31.8: Laying Out Classes
Put only one class in each file unless you have a compelling reason to do otherwise, but group the methods of each.
When separating methods or parts, blank lines are easy to type and look at least as good as any other separator.
Define an ordering, such as the file description before
#include
statements, beforeenum
definitions, and so on.
Chapter 32: Self-Documenting Code
32.2: Programming Style as Documentation
The main contributor to code-level documentation isn't comments, but good programming style.
32.3: To Comment or Not to Comment
Good comments don't repeat the code, but clarify its intent, or explain it at a higher level of abstraction.
If some code is difficult to comment, either it's bad code or you don't understand it well enough.
32.4: Keys to Effective Comments
If you find yourself adding explanatory comments because the code is tricky, consider improving the code instead.
Good comments summarize blocks of code, or explain its intent, focusing on the problem rather than the solution.
If commenting is time-consuming, either change your commenting style, or simplify the code so that it's easier to comment.
Writing comments after the code takes more time because you can't just write down what you’re already thinking about.
If commenting interrupts your thinking when writing code, design in pseudocode first and then convert the pseudocode to comments.
32.5: Commenting Techniques
Endline comments either repeat the code, or are too constricted to say anything meaningful, so mostly avoid them.
To comment a code block at its level of intent, think about what you would name a method that that did the same thing.
You'll often have a broad comment at the top of the loop and more detailed comments about the operations inside.
Document surprises, such as optimizations and workarounds for errors or undocumented features.
If some code is tricky to you, it will be incomprehensible to someone else. It's bad code and should be rewritten.
To improve the chances that a comment is updated along with a variable, include the variable name in the comment.
Heavy method-level commenting discourages programmers from creating new methods, leading to poorly-factored code.
Method-level comments are far from the code they describe, and so they tend not to be maintained.
If a method uses an algorithm from a book or magazine, document the volume and page number you took it from.
Class interface documentation should describe how to use the class, and not details about its inner workings.
Include authorship information in a top-level comment for other programmers if they need help.
Chapter 34: Themes in Software Craftsmanship
34.1: Conquer Complexity
Coding conventions, descriptive variable names, avoiding
goto
statements, and abstraction all reduce complexity.
34.2: Pick Your Process
The way in which people work together determines whether their abilities are added together or subtracted from each other.
If you code before designing, then it will be harder to embrace changes in design, because code must be thrown away.
34.3: Write Programs for People First, Computers Second
Favoring write-time convenience over read-time convenience is a false economy.
Habits affect all your work, and you can't toggle them at will, so ensure that whatever you're doing is worthy of a habit.
34.4: Program Into Your Language, Not In It
Just because your programming language supports global variables and
goto
statements doesn't mean you should use them.
34.5: Focus Your Attention with the Help of Conventions
Conventions save programmers from answering the same questions, or making the same arbitrary decisions, again and again.
They also convey information concisely, protect against hazards or weaknesses, and add predictability to low-level tasks.
34.6: Program in Terms of the Problem Domain
Top-level code shouldn't be filled with low-level data or code, but should describe the problem that's being solved.
If you're working in a low-level language, you should try and create higher layers for yourself to work in.
Low-level problem domain types are glue between fundamental data structures below and high-level problem domain code above.
34.7: Watch for Falling Rocks
Part of having good judgment in programming is recognizing a wide array of warning signs, or subtle indications of problems.
Program in such a way that you create more warnings that cannot be overlooked.
34.8: Iterate, Repeatedly, Again and Again
Taking several repeated and different approaches produces insight into the problem that's unlikely with a single approach.
34.9: Thou Shalt Rend Software and Religion Asunder
Blind faith in one method precludes the selectivity needed to find the most effective solutions to programming problems.
To experiment effectively, you must be willing to change your beliefs based on the results of the experiment.
Design is a process of carefully planning small mistakes in order to avoid making big ones.