by Steve McConnell
I, Michael Parker, own this book and took these notes to further my own learning. If you enjoy these notes, please purchase the book!
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- A cohesive routine contains operations that are related; otherwise, it probably does more than one thing.
- 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.
- 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.
- To handle "garbage-in," check values from external sources, values of method parameters, and then decide how to handle bad inputs.
- 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.
- 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.
- Exceptions weaken encapsulation by requiring the caller to know which exceptions might be thrown from the code that's called.
- 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.
- Remove code that results in hard crashes in production; but during development, this is invaluable for debugging.
- 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.
- TODO
- 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.
- 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.
- The earlier the binding time, the lower the flexibility and the lower the complexity. So add only as much flexibility as needed.
- Use variables for only one purpose; avoid having different values for the variable mean different things (like negative values).
- A good name tends to express the "what" more than the "how."
- To avoid
numSales
versussaleNum
confusion, consider variable names likesalesTotal
,salesCount
, andsalesIndex
.
- 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
.
- Variable names can contain: The variable contents, the kind of data, and the scope or visibility of the variable.
- When shortening variable names, don't remove just one letter, be consistent, create pronounceable names, and avoid mispronunciation.
- Avoid names with similar meanings, like
fileNumber
andfileIndex
. - Avoid names with different meanings but similar names, like
clientRecs
andclientReps
.
- By replacing magic numbers with constants, changes are reliable, changes can be made easily, and your code is more readable.
- 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.
- To avoid endless strings in C, initialize strings to null, and use
strncpy()
instead ofstrcpy()
.
- 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.
- In C, use or define an
ARRAY_LENGTH()
macro as#define ARRAY_LENGTH(x) (sizeof(x) / sizeof(x[0]))
.
- Don't name a type created using
typedef
after the underlying data type, and don't refer to predefined types.
- By passing only one or two fields from a structure into a method, you promote information hiding from the method.
- 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.
- 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.
- Organize code so that dependencies are obvious; if one method initializes data, create and call an
Initialize()
method.
- Statements that operate on the same data, perform similar tasks, or have ordering dependencies should appear together.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- With a table driven method, you must address how to look up entires, and what data should be stored in the table.
- 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.
- This puts the upper end of consecutive ranges into a table, and works well with a binary search for larger lists.
- 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.
- Null statements are uncommon, so make them obvious, such as a comment inside the braces explaining why one is used.
- 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.
- The core of structured programming is the simple idea that a program should use single-entry, single-exit control constructs.
- 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.
- 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.
- 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.
- Choose variable names that can be easily differentiated from each another.
- 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.
- 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.
- 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.
- 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.
- Keep refactorings small, do one at a time, review the changes thoroughly, and test them.
- Refactor as you add code, and target error-prone or high-complexity modules especially.
- 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.
- 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.
- 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.
- Use the number of CPU clock ticks allocated to your program rather than the time of day, so that it's unaffected by multitasking.
- Optimizations degrade the internal structure of a program, otherwise they'd be considered standard coding practice.
- Use
break
orreturn
in loops, order conditional tests by frequency, replace conditionals with table lookups, and evaluate lazily.
- 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.
- 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.
- 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.
- Save the assembler output from your compiler, and use it as a starting point for any optimization.
- 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.
- 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.
- 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.
- 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.
- Preceding comments with a blank line helps the reader scan the code.
- 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.
- The main contributor to code-level documentation isn't comments, but good programming style.
- 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.
- 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.
- 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.
- Coding conventions, descriptive variable names, avoiding
goto
statements, and abstraction all reduce complexity.
- 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.
- 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.
- Just because your programming language supports global variables and
goto
statements doesn't mean you should use them.
- 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.
- 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.
- 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.
- Taking several repeated and different approaches produces insight into the problem that's unlikely with a single approach.
- 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.