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