04 — Program Construction and Problem Solving
A practical guide to transforming problem specifications into clear, modular, tested, documented, and maintainable programs.
04 — Building Reliable Programs
A well-constructed program does more than run once. It should produce correct results for valid inputs, handle invalid or unusual inputs safely, remain understandable to other programmers, and support future changes.
Important quality goals include:
Correctness: the program meets its stated requirements.
Safety: invalid inputs and expected failures are handled clearly.
Readability: names, organization, and control flow communicate intent.
: separate parts can be developed, tested, and changed independently.
Testability: important behavior can be checked systematically.
Documentation: users and developers can understand how to use and maintain the program.
Maintainability: improvements do not require rewriting unrelated parts.
Program construction is therefore a cycle of planning, implementation, testing, , documentation, and improvement rather than a single act of writing code.
Takeaway: Quality is a property of the whole development process, not only of the final instructions.
From Specifications to Algorithms
Begin by stating exactly what the program receives, what it must produce, which assumptions are valid, and what should happen when an assumption is violated. For example, an average-score problem may require a nonempty list whose values range from 0 to 100. This is more useful than an instruction that merely says to calculate an average.
divides the problem into tasks such as reading the values, validating them, adding them, dividing by the number of values, and displaying the result. Smaller tasks make responsibilities visible and provide natural boundaries for functions or modules.
An algorithm is a finite, ordered set of precise steps for solving a problem. For an average, the steps can be described as follows:
Start a running total at zero.
Add each score to the running total.
Divide the total by the number of scores.
Return the result.
An can clarify the loop: after each value is processed, the running total equals the sum of all values processed so far. A useful algorithm should be precise enough to translate into code without changing its logic.
Takeaway: A precise specification and a carefully designed algorithm reduce ambiguity before implementation begins.
Clear Code and
Translate the algorithm into a programming language while preserving its logic. Names should communicate purpose, so a name such as average_score is clearer than a name such as x. Consistent indentation, spacing, naming, and organization reduce the effort required to understand code.
Prefer control flow in which each condition and loop has a clear purpose. Handling invalid input early can prevent the main calculation from being hidden among unrelated cases. A function that classifies a score, for example, can first reject values outside the permitted range, then handle passing values, and finally handle the remaining valid values.
A -focused design separates coherent responsibilities. A calculation function should calculate, while a display function should format and print the result. This separation prevents presentation details from being mixed with computational logic.
A module is a separately organized part of a program. A function can act as a module of behavior when it performs one coherent task and communicates through parameters and a return value. reduces duplication, supports independent testing, makes reuse easier, and localizes changes.
Two related design goals are high cohesion, meaning that a component’s elements support one main purpose, and low coupling, meaning that the component depends on as few details of other components as possible.
Takeaway: Clear names, simple control flow, and focused components make programs easier to understand and change.
Interfaces and Documentation
A function’s should state what information it accepts, what result it returns, and how it reports invalid input. For an average-calculation function, the contract might specify a nonempty sequence of scores as input, a numeric average as output, and an error for an empty sequence.
A clear allows one component to use another without knowing its internal implementation. This supports replacement and improvement: the calculation can change internally while callers continue to rely on the same contract.
Useful documentation at an includes:
The purpose of the function, class, or module.
Required input types or conditions.
The result and its meaning.
Errors that may be raised.
Assumptions that callers must satisfy.
Comments are most valuable when they explain a non-obvious reason or decision rather than merely repeating what the code visibly does. Documentation should remain accurate as the implementation changes.
Takeaway: A strong makes responsibilities and expectations explicit at the boundary between components.
Testing and Regression Protection
Testing is the systematic process of checking whether a program behaves as intended. A test case identifies the input, the expected result, the actual result, and whether the two agree. Testing cannot prove that every possible error is absent, but well-designed tests can reveal many defects and provide evidence about important behavior.
A useful test collection includes:
Typical cases: ordinary expected inputs.
Boundary cases: values at or near permitted limits, such as 0 and 100.
Empty cases: empty lists, strings, or files when those inputs are possible.
Invalid cases: values that violate the specification.
Large cases: inputs that may expose performance or resource problems.
Repeated or unusual cases: duplicate values, negative values, or unexpected formats.
A checks a small unit of behavior, usually one function or method. It should state one clear expectation and should not depend unnecessarily on other tests. Automated test frameworks can organize tests, provide assertions, discover test cases, and execute them consistently.
A preserves a behavior that must continue to work after a change. When a defect is discovered, correct the program and add a test that would detect the same defect in the future.
Takeaway: Test design should reflect the program’s specification, including normal behavior, limits, empty inputs, invalid inputs, and previously discovered failures.
, , and Improvement
begins when observed behavior is wrong or unexpected. It differs from testing: testing reveals a mismatch between actual and expected behavior, while investigates the cause and corrects it.
Common error categories include:
Syntax errors: the program violates the language grammar and cannot be parsed correctly.
Runtime errors: a failure occurs while the program is executing, such as division by zero, a missing file, or an invalid conversion.
Logic errors: the program runs but implements the wrong reasoning or produces an incorrect result.
A disciplined process is:
Reproduce the failure with inputs that reliably trigger it.
Read the error message and traceback carefully.
Reduce the problem to the smallest input or shortest sequence that still fails.
Form a specific hypothesis about the cause.
Inspect variable values, conditions, loop behavior, and assumptions.
Make one focused change.
Rerun the failing test.
Run the full test suite to check for regressions.
Document the cause and correction when the problem is significant.
Assertions are useful for detecting violated internal assumptions. Exceptions are generally more appropriate for expected invalid input that the program should report or handle. Catch specific exceptions instead of hiding unrelated failures with an overly broad handler.
improves internal structure without changing externally observable behavior. Safe includes renaming unclear variables, extracting long blocks into functions, removing duplication, simplifying nested conditions, and separating input or output from computation. Establish a testing baseline, make one small change, and rerun the tests after the change.
Takeaway: Evidence-based and incremental make corrections safer and preserve working behavior.
An Integrated Construction Workflow
Before considering a program complete, review both its behavior and its structure. The following checklist connects the major practices:
Does the program meet the stated requirements?
Are inputs validated where necessary?
Are typical, boundary, empty, invalid, and large cases tested?
Are functions focused enough to understand and test?
Are names meaningful and consistent?
Is repeated logic removed or intentionally justified?
Are errors reported clearly?
Are important interfaces, assumptions, and non-obvious decisions documented?
Can another programmer modify the program without guessing its purpose?
Have all tests been rerun after the latest changes?
The complete workflow is cyclical:
Specify the problem and its input and output conditions.
Decompose the problem into manageable responsibilities.
Design a precise algorithm and identify useful invariants.
Implement the design with clear names and simple control flow.
Organize related behavior into cohesive, loosely coupled components.
Test normal, boundary, empty, invalid, and large inputs.
Debug failures using reproducible evidence and focused changes.
Document interfaces, assumptions, and important decisions.
Refactor and improve in small steps while preserving tested behavior.
A strong program is not merely one that produces a result. It is one whose behavior can be explained, checked, corrected, reused, and maintained.