Matt Willard


Code Complete

  • SOFTWARE CONSTRUCTION

get requirements, do architecture/high-level design, then break it down, do detailed coding and debugging, test unit/integration

–this is ideal to make the work easier, make coding more smooth, testing more smooth, and improve it

–improves productivity a lot more and helps prevent more errors/issues down the line

–it’s like making a house: you need a strong blueprint before building or fixing it takes longer and costs more; measure twice, cut once

—-you might make your own code or grab someone else’s; depends on the approach

PREREQUISITES

-gathering prereqs and and designing the architecture makes programming more effective; understand exactly what and how to build it

-what problem are we solving, what else do we require, what features and things do we need, etc.

-how is it going to handle data, classes, databases, user interfaces, input/output, etc.

-we making our own stuff or are we using stuff someone else has made?

–depends on type of software being worked on; even iterative, prereqs is a good idea for each stage of construction

1. define the problem from user standpoint; might not need a program to solve it

2. figure requirements - the details of the problem and get them down to avoid changes later

3. get an idea of how the architecture of the software will go down; wrong architecture will cause issues down the line, pick what fits the program


DESIGN

-architecture blueprints, psuedocode, diagrams, patterns; but it’s good for a big benefit (it’s your big outline before writing a finished thing)

-good top-level design safely contains multiple lower-level designs

-sloppy: has tradeoffs, restrictions, rules of thumb, emergent

-helps manage complexity; break into subsystems with separate objects/concerns to manage it: keep things minimal, extendable without much effort, reusable, lean

LEVELS OF DESIGN:

-whole software system

-subsystems/packages (as few interactions between them as possible)

-division into classes within packages

-division of data and runtimes in classes

-the internal routine design

DESIGN RULES OF THUMB:

-ID real world/synthetic object; their attributes and methods - like a GUI has objects like dialog boxes, windows, fonts, buttons, etc.

-see what can be done to them, what they can do to other objects: methods, inheritance is one of these

-see what parts of object are public and what’s private

-what’s the object’s public interface; what’s exposed

ABSTRACTION: manage complexity by considering the bigger picture for design (a house, an object) and not the details (the doors, the windows, etc.)

ABSTRACTION: look at an object at a high level of detail (the house)

ENCAPSULATE: can’t look at it at any other level of detail (the rooms, doors, etc.); not allowed to know the details

INHERITANCE: when objects share a lot of qualities, you can have a generic overall object (like Building) and then Hotel can inherit the stuff Building can do, plus add its own stuff; good for abstraction

HIDING INFORMATION: hides complexity, keeps stuff secret to prevent rippling changes across the board by a change (like making a new instance of a function)

-ASK: what information do I need to hide (data, methods)

OTHER GOOD DESIGN RULES OF THUMB

-Try to create modules that depend little on other modules

-assign responsibilities to objects

-design for testing

-considering how it can fail

-draw diagram

-make the design modular

=> HOW TO SOLVE IT:

-understand the problem

-devise a plan: restate, solve related problem

-carry out plan

-look back: examine result

DESIGN PRACTICES

-iterate on successive designs

-divde and conquer: refine incrementally and tackle areas individually

-do experimental prototyping with junk, throwaway code to work out specific design problems


CLASSES

-abstract data type: collection of data/operations to work on that data (IE a class)

–think of classes this way to make them easier to use and modify; to make and manipulate real-world entities; treat them as these no matter the level

GOOD CLASS INTERFACES:

-good and abstract with stuff related to the function (data/employee functions for an employee object for example)

-one class does one thing

-focus on exposing only what’s necessary and try to abstract/hide the result; minimize accessibility and keep private where noticed


MAKING GOOD ROUTINES/FUNCTIONS

-reduce complexity, introduce good abstraction, avoid duplicate code, hide things, improve portability and performance

-keep the name clear and description of what it does (name of return value is good for function, a procedure without a return value should get verb/object)

-use naming conventions

-parameters: put in input/modify/output order

-pass only the variables/objects need to maintain abstraction


DEFENSIVE PROGRAMMING

-INPUTS: do testing, decide how to handle bad inputs and garbage inputs, check values of routine data and parameters

-use ASSERTIONS: kind of like test suite stuff, it anticipates a certain response and helps you detect problems (like if you get a certain sum, if this is a file type that’s expected, etc.)

–this is great for testing and dev

–use asserts to verify pre and post conditions

–check for bad input for security like SQL injections

ERROR HANDLING: for conditions you expect to occur (bad inputs)

ASSERTIONS: for conditions that should never occur (bugs)

ERROR HANDLING should be decided based on the needs of the program; base it on a general high-level approach to bad parameters which can range from returning the same data, different data, or killing he process entirely

–be consistent and guard against errors you don’t expect

–log errors too

EXCEPTIONS: try to use standard error handling mostly

-make sure they’re standardized; consider centralized exception reporter class

BARRICADES:

you can use validation classes to check input and then respond if it’s not valid, preventing passing to the other classes

-sterilize data, convert data at input time

-debug code can be shitty and slow in dev, this is fine; plan to remove most of it and check for it when test compiling; remove the stuff that causes massive slowdown

-leave in debug code that checks for big errors and keeps it crashing gracefully if needed


PSUEDOCODE PROGRAMMING PROCESS

BUILD CLASS

make general design

make each routine in class

review and test

BUILD ROUTINE

design routine

check design

code routine

review and test

PSUEDOCODE: just English statements to describe the operations

then build code around it

-design routine in psuedocode and define/solve problem

-name and decide how to test

-think about error handling and efficiency

-psuedocode starts high level; then it’s checked: iterate and keep the best

CODING THE ROUTINE

declare routine

turn psuedocode into comments

fill code below each comment

see if code needs to be factored further into its own routines

CHECK CODE

check for errors

compile and test; step through in debugger and use test cases planned

clean up errors and correct for quality

BE SYSTEMATIC: refactor iteratively, do test-driven development, or use that psuedocode


VARIABLES

-make each variable have one purpose

-declare them explicitly with clear names in naming conventions; keeps it consistent and establishes relationships

–identify: global var, member var, type def, named constants, 

-init them as they’re declared with values closed to where first used

-try to keep them constant where possible

-keep them local in their scope when possible, and reference it as little as possible for a variety of reasons

–for a lot of risky reasons, global variables are bad; keep it on the smallest segment of code that needs to see it (routine > class > access routines to share data)

-try to assign it just before it’s used


DATA TYPES

-avoid “magic” data, where they appear in a middle of a program without explanation: you probably want to use named concepts to keep it reliable and make changes more easily

-ENUMERATED TYPES: data type that lets you describe members of a class of objects in English:

ENUM COLOR

color red

color blue

color green

readable, reliable, easily modified, can sit in for boolean

-NAMED CONSTANTS: can’t change value when assigned; great for keeping a variable in one place if used throughout your program

–also helps makes things more clear when referring to them

ARRAYS: try to keep indexes within bounds of array, try to access them sequentially (or use stacks/queues); make sure indexes are clear

CREATE YOUR OWN DATA TYPES: great for abstracting data and making it more protected by change; only needing to make changed in one place, etc.

–hides other data inside and makes sure other parts of the program don’t need to know

–functionally oriented names, don’t redefine or touch a predefined type

GLOBAL DATA:

-best to hide data within classes where possible; less hassle in the long run, easier to reuse code since you’re not referencing global data; keeps it modular

-make variables local to routines/classes as much as possible; strive for modularity

-use access routines: hide data in a class and write routines that access/change the data (compare to C#’s getter and setter) and other code need to go through them

-put these variables into classes, don’t slam them in one big object and pass it around


ORGANIZING CODE

-organize code so that the order and dependencies are obvious

-if you draw boxes around your code to group related statements, there shouldn’t be any box overlap (though boxes can be nested)

-have it read straight down, top to bottom, and don’t jump around

CONDITIONALS: write nominal path first then others

-IE: do all the nominal cases first, THEN do the error cases

-consider adding else clauses

-try simplifying complicated cases with boolean; put the most common ones first

-if you can, go for other constructs like switch cases; maybe order those by frequency

–use default for errors

LOOPS:

-counted: loop X amount of times

-cont. evaluated: tests if done on each iteration (you can use a while loop here)

-endless: goes on forever

-iterator: does one for each element in container (table rows)

–> use for loops for simple increments/decrements that you don’t need a while for; prefer these when appropriate

GOOD LOOP PRACTICES:

-minimize what affects the loop

-keep as much possible control out of the loop: what surrounds it as if it were a black box in the middle to know conditions which loop terminates

-enter the loop from one location; put init code close to loop

-use while (True) for infinite loops

-loops should only do one thing

-loops should have meaningful names for their variables; keep them different to avoid cross talk


SOFTWARE QUALITY LANDSCAPE

quality software is:

-free from faults

-easy to learn and use

-uses minimal system resources

-consistently reliable

-prevents unauthorized or imporer access

-can be adapted to other apps/environments

-relatively free from error

-works well in spite of invalid inputs or weird environment

internal quality is:

easy to modify and change/add stuff

easy to modify for other uses besides its own

easy to modify for other operation environments

easy to reuse parts in other systems

easy to read and understand

easy to unit and system test and verify

improve software quality:

set clear objectives so the right stuff can be maximized or balanced

make quality a priority above all

have a good robust testing strategy

have guidelines for actually developing the software

walk through it with peers

have formal tech reviews from stakeholders or do internal/external audit

try to measure your results

use prototypes/tracer bullets to test/refine designs and ideas: iterate

have procedures in mind to stop willy-nilly changes

keep quality assurance going early; remove errors and clean up things/defects early

DEVELOPER TESTING

Big types of testing are:

UNIT/COMPONENT TESTING: testing a class/routine/small program on its own

INTEGRATION TESTING: testing two or more classes/packages/components together to make sure everything’s working together

REGRESSION TESTING: use old test cases to find defects in newer builds

SYSTEM TESTING: test the whole finished system; security, performance, etc.

BLACK BOX TESTING: you don’t see the inner guts of the code

WHITE BOX TESTING: you know the inner code guts

testing of course is important to finding defects and testing software reliably

-you can test during development: test-driven development is writing test cases and then code to support it - this can find defects and requirements problems earlier

-test for the relevant requirements, the design, and the input

-check your work, plan test cases as you go, keep those cases, use a test framework

-use scaffolding to test; mock objects and dummy files

errors: most tend to be in a few routines; often typos, often construction problems and misunderstanding design

IMPROVE TESTING

-do retesting with older tests after changes

-automate testing where you can

-keep test logs and records of defects

DEBUGGING

-learn from them: how can you prevent mistakes next time, how you solve problems, how you fix defects, etc.

STEPS:

-make error occur reliably with a test case that reproduces error; change test case to understand program behavior

-local source of error: gather data, analyze, form hypothesis, see how to prove/disprove it (examine code, do a test)

–refine test cases that prove error; use different hypotheses, list and try different things, split-half troubleshooting

-fix defect

-test fix

-look for similar errors

-divide and conquer parts of the code and check

FIX THE DEFECT

thoroughly understand the problem using test cases and confirm

save original source code

fix the problem, not the symptom

one change at a time and test; use unit tests

use good tools: test frameworks, debuggers, compilers


REFACTORING

it’s good to refactor to clean up repetitive, long code, to reduce complexity and increase abstraction; to make more modular and organized

-SAFE REFACTORING

save code you start with

refactor small changes, one bit at a time: list your steps and note changes you’ll need to make

keep saving checkpoint code

keep testing and retesting

-STRATEGIES

refactor when you add: routine or class, or when you fix defect

use interfaces between clean and ugly code as a transition point

do it during development, might as well


CODE TUNING

improve performance by changing: requirements, program design, class design, less interactions with OS, avoiding i/o, use compiled language, switching hardware

1.develop software with well-designed, easy to understand code

2. improve performance:

-save working version code

-measure system and find bottle necks

-tune design, data types, algorithms where ideal

-measure: if performance doesn’t improve much, revert

INTEGRATION

it’s probably best to write/test a program in small pieces and combine them one at a time to avoid a lot of potential hassle

-make new functional skeleton part of the system

-make and test a class

-attach class to skeleton and run tests

-lots of potential strategies: top down, bottom up, etc.: top down starts with the overall horizontal slice skeleton then adds on top via vertical slices of functionality

consider the build and smoke test: compile/link program into executable then run it to see if anything really goes wrong = the daily build


GOOD LAYOUT PRINCIPLES

-use readable formatting, blank lines, grouping and spaces; helps make it easier to understand

-should have good logical structure of the code

-one statement/declaration per line

-self documenting code: keep it clear, named well, and descriptive

-COMMENTS: stick to comments that summarize or give intent of what’s to come, don’t repeat the code

–keep it simple, do it as you go, especially by doing psuedocode design

THEMES IN SOFTWARE CRAFTSMANSHIP

break down a system into subsystems and focus on a design you can do one piece at a time

define class interfaces that ignore internal workings of class

have that interface preserve abstraction

avoid global data

avoid nesting loops/conditionals deeply

use a carefully planned error handling approach

keep routines short

keep classes only as big as needed

use clear, self-explanatory variable names

minimize parameters passed to routine

pass only the parameters need to preserve interface’s abstraction

use coding conventions to reduce complexity across the board

THE PROCESS

define stable, clear requirements

lay a solid foundation of system/subsystem/class/routine design

write clear, comprehensible, readable code: people first, computers second

work at the highest possible level of abstraction: use descriptive class names and routine calls that indicate exactly what the program is doing: be high-level and clear, anything below that ought to be hidden

Iterate over and over again; build the system in increments and learn/improve off of it