code-complete.markdown

January 15, 2014 · View on GitHub

Code Complete

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!

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 versus saleNum confusion, consider variable names like salesTotal, salesCount, and salesIndex.

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 or isStatusOk instead of sourceFile and status.

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 and fileIndex.
  • Avoid names with different meanings but similar names, like clientRecs and clientReps.

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 of strcpy().

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 or void 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 a while loop.

16.2: Controlling the Loop

  • Put initialization code immediately before the loop.
  • Use for (;;) or while (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 a goto 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 of 0 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 or return, 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 or return 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, before enum 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.