Section 1.7 The Design Recipe Template
Subsection 1.7.1 Overview & Purpose
By now, you have mastered all six steps of the Design Recipe, including the crucial Step 0: Understand & Restate the Problem. This final section provides a complete template that will guide you through each step when solving programming problems.
The template is organized into two key parts for each step:
-
What to Do: Clear instructions for completing the step
-
Why it Matters: Key benefits and connections to previous sections
Following these steps in sequence (beginning with Step 0) will help you create reliable, well-documented code that’s easier to test and maintain. Remember: taking time to understand the problem thoroughly in Step 0 prevents costly mistakes later.
Subsection 1.7.2 The Template At a Glance
Here’s your roadmap through the Design Recipe’s six steps:
-
Step 0: Understand & Restate the Problem
-
Step 1: Data Definitions
-
Step 2: Method Signatures & Purpose Statements
-
Step 3: Examples & Tests
-
Step 4: Skeleton / Method Template
-
Step 5: Implementation, Testing & Refinement
You can document these steps in one file or split them across multiple files—choose what works best for your project. The key is maintaining clear organization so you can easily review and update your work as needed.
Subsection 1.7.3 The Detailed, Fill-In Template
Subsubsection 1.7.3.1 Step 0: Understand & Restate the Problem
-
Carefully read the assignment or project prompt at least twice.
-
Identify ambiguities or missing details—e.g. data format, constraints, edge cases.
-
Ask for clarifications from your TA, instructor, or client if possible. If you cannot get clarification, list your assumptions explicitly.
-
Write a short “restatement” of the problem in your own words. Check that you cover all required inputs, outputs, and behaviors.
Step 0 is your foundation for success. Consider these critical points:
-
Most serious bugs stem from misunderstood requirements, not coding errors
-
Restating the problem reveals gaps in your understanding
-
Clear problem statements make it easier to get help from others
TEMPLATE:
# STEP 0: Restatement of the Problem
## My Understanding
- Main goals:
- Required inputs:
- Expected outputs:
- Key behaviors:
## Key Details / Questions
- Ambiguities:
1. ...
2. ...
- Assumptions:
1. ...
2. ...
## Scope / Edge Cases
- Special cases:
1. ...
2. ...
- Constraints:
1. ...
2. ...
COMMON PITFALLS:
-
Not stating assumptions explicitly, so teammates don’t realize the interpretation you chose.
-
Skipping Step 0 entirely and jumping into coding—risking a major redesign when the real requirements emerge.
Subsubsection 1.7.3.2 Step 1: Data Definitions
-
Describe each structure’s fields (or elements) and valid ranges or constraints. Use your restatement from Step 0 to ensure consistency.
-
If your language is untyped (like Python), note that these constraints may only be enforced by logic, not by the compiler.
In Section 1, you learned that well-defined data serves multiple purposes:
-
Prevents "magic index" errors and confusion about field meanings
-
Acts as the blueprint for the rest of your design
-
Protects against breaking changes when data formats evolve
TEMPLATE:
# STEP 1: Data Definitions
## Main Data Structures
1. TypeName1:
- Purpose: ...
- Structure: ...
2. TypeName2:
- Purpose: ...
- Structure: ...
## Fields & Valid Ranges
TypeName1:
- field1: type, constraints
- field2: type, constraints
TypeName2:
- field1: type, constraints
- field2: type, constraints
## Additional Constraints
1. Relationship rules: ...
2. Cross-field validations: ...
3. Uniqueness requirements: ...
COMMON PITFALLS:
-
Forgetting to include constraints (like “rating must be 1..5”)—leading to silent errors when out-of-range values occur.
-
Using ambiguous data structures (e.g. a bare list) without explaining what each element means.
Subsubsection 1.7.3.3 Step 2: Method Signatures & Purpose Statements
-
For each function or method you plan to write, define its signature (input types, return type).
-
Write a short purpose statement (a docstring or comment) describing what it does, why it does it, and any error conditions.
-
Reference your data definitions from Step 1 to avoid conflicts or contradictions.
Section 2 taught us that function contracts provide several benefits:
-
Clarify how to use each function without reading its implementation
-
Foster better collaboration between team members
-
Prevent accidental misuse of code or invalid data passing
TEMPLATE:
# STEP 2: Method Signatures & Purpose Statements
def function_name(param1: Type, param2: Type) -> ReturnType:
"""
Purpose:
What: Brief description of function's primary task
Why: Context or reason for this function's existence
Args:
param1: Description and constraints
param2: Description and constraints
Returns:
Description of return value and format
Raises:
ErrorType1: Condition that triggers this error
ErrorType2: Condition that triggers this error
"""
# (Repeat for additional functions)
COMMON PITFALLS:
-
Being vague about input constraints (e.g., “param1 is a list,” but not specifying it must be non-empty or must contain only numbers).
-
Omitting error cases and leaving them to be “discovered” during coding, which can cause inconsistent error handling.
Subsubsection 1.7.3.4 Step 3: Examples & Tests
After clarifying the problem (Step 0), defining your data (Step 1), and writing your function’s signature/purpose (Step 2), you’re ready to illustrate specific behavior with Examples & Tests. The best way to ensure you’ve covered all scenarios—normal, edge, and (optionally) error cases—is to list them in a table. Each row typically includes:
-
The type of case (normal usage, edge, or special/error scenario)
-
The input you plan to give the function
-
The expected output you want back
-
A brief note or rationale explaining why that output is correct
Once you have this table of examples, you can turn each row into an automated test, typically using simple
assert statements in Python. That way, any time you change your code, you can re-run these tests to confirm you haven’t broken anything.
count_words| Case | Input | Expected Output | Notes |
|---------------|--------------------|-----------------|--------------------------------|
| Normal usage | "Hello world" | 2 | 2 words separated by a space |
| Empty string | "" | 0 | No words at all |
| Whitespace | " " | 0 | Spaces only, so 0 words |
| Multiple words| "CS is cool" | 3 | 3 words: "CS", "is", "cool" |
In a more advanced course, you might include a row for "invalid inputs" (like
count_words(None)) or out-of-range values. But if you haven’t learned how to handle errors or exceptions in Python yet, it’s okay to keep those scenarios out of your main tests. You can add them once you cover exception handling in a later unit.
Each row translates neatly into a code-based test. For instance:
def test_count_words():
# Normal usage
assert count_words("Hello world") == 2
# Empty string
assert count_words("") == 0
# Whitespace only
assert count_words(" ") == 0
# Multiple words
assert count_words("CS is cool") == 3
print("test_count_words PASSED")
If all assertions pass, you know your function meets these examples. If any fail, Python raises an
AssertionError, pointing you to which scenario is broken. Rerun test_count_words each time you update count_words to make sure you haven’t accidentally broken existing functionality.
TEMPLATE FOR STEP 3:
# STEP 3: Examples & Tests
## 1) Table of Test Cases
| Case / Type | Input(s) | Expected Output | Notes/Rationale |
|---------------|--------------------------|-----------------|----------------------------------------|
| Normal ... | ... | ... | Why do we expect that result? |
| Edge ... | ... | ... | E.g., empty list, 0 or negative, etc. |
| Error ... | ... | ... | If we haven't learned exceptions yet, |
| | | | we can skip or just list them here. |
## 2) Test Function
def test_my_function():
# For each row above, write an assert statement:
# 1. Normal
assert my_function(...) == ...
# 2. Edge
assert my_function(...) == ...
# 3. (Optional) Error case if you know how to test errors
print("test_my_function PASSED")
By separating the table of examples from the automated test code, you ensure that every scenario you identify is explicitly tested. This also makes it easy to review: if someone asks "Have you tested an empty string?" you can point them to the row in the table and the corresponding
assert statement in your test file.
Remember, you can keep your example table in comments or a separate document. The key is to systematically match each row to a line of test code. Later, once you learn about exceptions (like
ValueError), you can expand your table to include error cases and explore techniques for verifying that invalid input triggers the right failure.
Subsubsection 1.7.3.5 Step 4: Skeleton / Method Template
-
Outline how your function will flow: input validation, loops or conditionals, returning data.
-
Do not fill in detailed code yet—just high-level pseudocode or commented steps.
-
Ensure your skeleton aligns with your Step 2 signatures and Step 3 test expectations.
As Section 4 showed, a skeleton provides several benefits:
-
Helps avoid writing everything at once
-
Clarifies structure and reveals missing logic before implementation
-
Enables quick reordering or insertion of steps when needed
TEMPLATE:
# STEP 4: Skeleton / Method Template
def function_name(param1: Type, param2: Type) -> ReturnType:
"""From Step 2: Purpose & signature"""
# 1. Input Validation
# - Check param1 constraints
# - Check param2 constraints
# - Raise appropriate errors
# 2. Setup
# - Initialize variables
# - Prepare data structures
# 3. Main Logic
# - Core algorithm steps
# - Key transformations
# 4. Return
# - Format result
# - Final validation
return result # placeholder
COMMON PITFALLS:
-
Writing the entire function now instead of limiting it to an outline—leading to confusion and missed opportunities for early feedback.
-
Skipping placeholders for validation logic or ignoring how error cases will be handled.
Subsubsection 1.7.3.6 Step 5: Implementation, Testing & Refinement
-
Convert your skeleton comments into real code, line by line or block by block.
-
Immediately run your tests (from Step 3) after each block or change. Fix any bugs or update tests if you discover a mismatch in assumptions.
-
Record any final insights: Did your design change slightly? Did you spot new edge cases?
Section 5 emphasized several key points about implementation:
-
Implementing with a robust design and test coverage reduces stress
-
Quick error detection replaces guesswork in solutions
-
The "Refinement" phase acknowledges and systematically addresses oversights
TEMPLATE:
# STEP 5: Implementation Testing
def function_name(param1: Type, param2: Type) -> ReturnType:
# 1. Input Validation
if not valid(param1):
raise ValueError("param1 must be...")
# 2. Setup
result = initialize()
# 3. Main Logic
# Core implementation
# 4. Return
return result
# Implementation Notes:
# 1. Changes from original design:
# - ...
# 2. Additional edge cases found:
# - ...
# 3. Optimization opportunities:
# - ...
COMMON PITFALLS:
-
Failing to re-run tests frequently, which makes it harder to locate newly introduced bugs.
-
Overlooking small updates to data definitions or signatures when you discover new requirements during implementation.
Subsection 1.7.4 How to Use This Template Effectively
Here are a few practical tips for adopting this template in your labs, assignments, or team projects:
Work in order: Do not skip to Step 5 prematurely. Each step clarifies or locks in details that the subsequent step relies on.
Leverage version control: Commit your Step 0-4 docs before you start coding, so you can track changes and compare your final code (Step 5) to the skeleton.
Review with peers or a TA: An outside reader can catch contradictions or missing constraints. This is especially valuable for Step 0 (restating the problem) and Step 1 (data definitions).
Refer back to section 0-6: If you find yourself struggling with a step, each section offers deeper explanation, examples, and guidance for best practices.
If this process feels “slow,” remember that the time spent clarifying each step before coding often saves you from hours of debugging, rework, or contradictory assumptions.
Subsection 1.7.5 Conclusion & Next Steps
This template combines all Design Recipe steps into a practical framework. By carefully completing each section, you’ll create solutions that are well-designed, thoroughly tested, and easier to maintain.
While you can adapt this template to your needs, preserve the core principles: thoroughly understand the problem first, design before coding, and let tests guide your implementation.
With these tools and techniques, you’re ready to tackle programming challenges systematically and effectively. Remember that mastering the Design Recipe is an ongoing journey—each project helps you refine your approach and build better software.
Now go forth and design great solutions—but don’t forget to start at Step 0!
You have attempted of activities on this page.
