2.13 — How to design your first programs

Now that you’ve learned some basics about programs, let’s look more closely at how to design a program.

When you sit down to write a program, generally you have some kind of idea for which you’d like to write a program for. New programmers often have trouble figuring out how to convert that idea into actual code. But it turns out, you have many of the problem solving skills you need already, acquired from everyday life.

The most important thing to remember (and hardest thing to do) is to design your program before you start coding. In many regards, programming is like architecture. What would happen if you tried to build a house without following an architectural plan? Odds are, unless you were very talented, you’d end up with a house that had a lot of problems: walls that weren’t straight, a leaky roof, etc… Similarly, if you try to program before you have a good game-plan moving forward, you’ll likely find that your code has a lot of problems, and you’ll have to spend a lot of time fixing problems that could have been avoided altogether with a little thinking ahead.

A little up-front planning will save you both time and frustration in the long run.

In this lesson, we’ll lay out a generalized approach for converting ideas into simple functional programs.

Design step 1: Define your goal

In order to write a successful program, you first need to define what your goal is. Ideally, you should be able to state this in a sentence or two. It is often useful to express this as a user-facing outcome. For example:

  • Allow the user to organize a list of names and associated phone numbers.
  • Generate randomized dungeons that will produce interesting looking caverns.
  • Generate a list of stock recommendations.
  • Model how long it takes for a ball dropped off a tower to hit the ground.

Although this step seems obvious, it’s also highly important. The worst thing you can do is write a program that doesn’t actually do what you (or your boss) wanted!

Design step 2: Define requirements

While defining your problem helps you determine what outcome you want, it’s still vague. The next step is to think about requirements.

Requirements is a fancy word for both the constraints that your solution needs to abide by (e.g. budget, timeline, space, memory, etc…), as well as the capabilities that the program must exhibit in order to meet the users’ needs. Note that your requirements should similarly be focused on the “what”, not the “how”.

For example:

  • Phone numbers should be saved, so they can be recalled later.
  • The randomized dungeon should always contain a way to get from the entrance to an exit.
  • The stock recommendations should leverage historical pricing data.
  • The user should be able to enter the height of the tower.
  • We need a testable version in 7 days.

A single problem may yield many requirements, and the solution isn’t “done” until it satisfies all of them.

Design step 3: Define your tools, targets, and backup plan

When you are an experienced programmer, there are many other steps that typically would take place at this point, including:

  • Defining what target architecture and/or OS your program will run on.
  • Determining what set of tools you will be using.
  • Determining whether you will write your program alone or as part of a team.
  • Defining your testing/feedback/release strategy.
  • Determining how you will back up your code.

However, as a new programmer, the answers to these questions are typically simple: You are writing a program for your own use, alone, on your own system, using an IDE you purchased or downloaded, and your code is probably not used by anybody but you. This makes things easy.

That said, if you are going to work on anything of non-trivial complexity, you should have a plan to backup your code. It’s not enough to just zip or copy the directory to another location on your machine (though this is better than nothing). If your system crashes, you’ll lose everything. A good backup strategy involves getting a copy of the code off of your system altogether. There are lots of easy ways to do this: Zip it up and email it to yourself, copy it to Dropbox or another cloud service, FTP it to another machine, copy it to another machine on your local network, or use a version control system residing on another machine or in the cloud (e.g. github). Version control systems have the added advantage of not only being able to restore your files, but also to roll them back to a previous version.

Design step 4: Break hard problems down into easy problems

In real life, we often need to perform tasks that are very complex. Trying to figure out how to do these tasks can be very challenging. In such cases, we often make use of the top down method of problem solving. That is, instead of solving a single complex task, we break that task into multiple subtasks, each of which is individually easier to solve. If those subtasks are still too difficult to solve, they can be broken down further. By continuously splitting complex tasks into simpler ones, you can eventually get to a point where each individual task is manageable, if not trivial.

Let’s take a look at an example of this. Let’s say we want to clean our house. Our task hierarchy currently looks like this:

  • Clean the house

Cleaning the entire house is a pretty big task to do in one sitting, so let’s break it into subtasks:

  • Clean the house
    • Vacuum the carpets
    • Clean the bathrooms
    • Clean the kitchen

That’s more manageable, as we now have subtasks that we can focus on individually. However, we can break some of these down even further:

  • Clean the house
    • Vacuum the carpets
    • Clean the bathrooms
      • Scrub the toilet (yuck!)
      • Wash the sink
    • Clean the kitchen
      • Clear the countertops
      • Clean the countertops
      • Scrub the sink
      • Take out the trash

Now we have a hierarchy of tasks, none of them particularly hard. By completing each of these relatively manageable sub-items, we can complete the more difficult overall task of cleaning the house.

The other way to create a hierarchy of tasks is to do so from the bottom up. In this method, we’ll start from a list of easy tasks, and construct the hierarchy by grouping them.

As an example, many people have to go to work or school on weekdays, so let’s say we want to solve the problem of “go to work”. If you were asked what tasks you did in the morning to get from bed to work, you might come up with the following list:

  • Pick out clothes
  • Get dressed
  • Eat breakfast
  • Drive to work
  • Brush your teeth
  • Get out of bed
  • Prepare breakfast
  • Get in your car
  • Take a shower

Using the bottom up method, we can organize these into a hierarchy of items by looking for ways to group items with similarities together:

  • Get from bed to work
    • Bedroom things
      • Get out of bed
      • Pick out clothes
      • Get dressed
    • Bathroom things
      • Take a shower
      • Brush your teeth
    • Breakfast things
      • Prepare cereal
      • Eat cereal
    • Transportation things
      • Get in your car
      • Drive to work

As it turns out, these task hierarchies are extremely useful in programming, because once you have a task hierarchy, you have essentially defined the structure of your overall program. The top level task (in this case, “Clean the house” or “Go to work”) becomes main() (because it is the main problem you are trying to solve). The subitems become functions in the program.

If it turns out that one of the items (functions) is too difficult to implement, simply split that item into multiple sub-items/sub-functions. Eventually you should reach a point where each function in your program is trivial to implement.

Design step 5: Figure out the sequence of events

Now that your program has a structure, it’s time to determine how to link all the tasks together. The first step is to determine the sequence of events that will be performed. For example, when you get up in the morning, what order do you do the above tasks? It might look like this:

  • Bedroom things
  • Bathroom things
  • Breakfast things
  • Transportation things

If we were writing a calculator, we might do things in this order:

  • Get first number from user
  • Get mathematical operation from user
  • Get second number from user
  • Calculate result
  • Print result

At this point, we’re ready for implementation.

Implementation step 1: Outlining your main function

Now we’re ready to start implementation. The above sequences can be used to outline your main program. Don’t worry about inputs and outputs for the time being.

Or in the case of the calculator:

Note that if you’re going to use this “outline” method for constructing your programs, your functions won’t compile because the definitions don’t exist yet. Commenting out the function calls until you’re ready to implement the function definitions is one way to address this (and the way we’ll show here). Alternatively, you can stub out your functions (create placeholder functions with empty bodies) so your program will compile.

Implementation step 2: Implement each function

In this step, for each function, you’ll do three things:

  1. Define the function prototype (inputs and outputs)
  2. Write the function
  3. Test the function

If your functions are granular enough, each function should be fairly simple and straightforward. If a given function still seems overly-complex to implement, perhaps it needs to be broken down into subfunctions that can be more easily implemented (or it’s possible you did something in the wrong order, and need to revisit your sequencing of events).

Let’s do the first function from the calculator example:

First, we’ve determined that the getUserInput function takes no arguments, and will return an int value back to the caller. That gets reflected in the function prototype having a return value of int and no parameters. Next, we’ve written the body of the function, which is a straightforward 4 statements. Finally, we’ve implemented some temporary code in function main to test that function getUserInput (including its return value) is working correctly.

We can run this program many times with different input values and make sure that the program is behaving as we expect at this point. If we find something that doesn’t work, we know the problem is in the code we’ve just written. Once we’re convinced the program is working as intended up to this point, we can remove the temporary testing code, and proceed to implementation of the next function (function getMathematicalOperation).

Remember: Don’t implement your entire program in one go. Work on it in steps, testing each step along the way before proceeding.

Implementation step 3: Final testing

Once your program is “finished”, the last step is to test the whole program and ensure it works as intended. If it doesn’t work, fix it.

Words of advice when writing programs

Keep your programs simple to start. Often new programmers have a grand vision for all the things they want their program to do. “I want to write a role-playing game with graphics and sound and random monsters and dungeons, with a town you can visit to sell the items that you find in the dungeon” If you try to write something too complex to start, you will become overwhelmed and discouraged at your lack of progress. Instead, make your first goal as simple as possible, something that is definitely within your reach. For example, “I want to be able to display a 2-dimensional field on the screen”.

Add features over time. Once you have your simple program working and working well, then you can add features to it. For example, once you can display your field, add a character who can walk around. Once you can walk around, add walls that can impede your progress. Once you have walls, build a simple town out of them. Once you have a town, add merchants. By adding each feature incrementally your program will get progressively more complex without overwhelming you in the process.

Focus on one area at a time. Don’t try to code everything at once, and don’t divide your attention across multiple tasks. Focus on one task at a time. It is much better to have one working task and five that haven’t been started yet than six partially-working tasks. If you split your attention, you are more likely to make mistakes and forget important details.

Test each piece of code as you go. New programmers will often write the entire program in one pass. Then when they compile it for the first time, the compiler reports hundreds of errors. This can not only be intimidating, if your code doesn’t work, it may be hard to figure out why. Instead, write a piece of code, and then compile and test it immediately. If it doesn’t work, you’ll know exactly where the problem is, and it will be easy to fix. Once you are sure that the code works, move to the next piece and repeat. It may take longer to finish writing your code, but when you are done the whole thing should work, and you won’t have to spend twice as long trying to figure out why it doesn’t.

Don’t invest in perfecting early code. The first draft of a feature (or program) is rarely good. Furthermore, programs tend to evolve over time, as you add capabilities and find better ways to structure things. If you invest too early in polishing your code (adding lots of documentation, full compliance with best practices, making optimizations), you risk losing all of that investment when a code change is necessary. Instead, get your features minimally working and then move on. As you gain confidence in your solutions, apply successive layers of polish. Don’t aim for perfect -- non-trivial programs are never perfect, and there’s always something more that could be done to improve them. Get to good enough and move on.

Most new programmers will shortcut many of these steps and suggestions (because it seems like a lot of work and/or it’s not as much fun as writing the code). However, for any non-trivial project, following these steps will definitely save you a lot of time in the long run. A little planning up front saves a lot of debugging at the end.

The good news is that once you become comfortable with all of these concepts, they will start coming more naturally to you. Eventually you will get to the point where you can write entire functions without any pre-planning at all.

2.x -- Chapter 2 summary and quiz
2.12 -- Header guards

299 comments to 2.13 — How to design your first programs

  • Slyde

    Hello Alex. I guess this will be covered at some point. But since I ran in to it this evening, I thought I'd go ahead and ask abt it. I'm able to catch invalid entries in my getOperator(); function through an IF statement and recursion (I hope I'm using that term in the right context here). After a few bad entries, I'll give it one it accepts. But no matter what correct entry I give, it keeps pulling out the * sign to use. This only happens when I put in garbage before entering a correct operator.

    Thanks for your help.

    • abdulilah

      This fixed it for me , I think its something with the flow but I am new to c++.
      I think its going into getOperator() every time until u get the right operator, but the is still coming from the first time the function was called.
      The * you have been getting is probably the default char. Like 0 is the default int if u enter something else into an int value.
      Hope this helps.

    • - Initialize your variables with brace initializers.
      - Only initialize variables to a specific value if you need that value. Use empty curly braces otherwise.
      - Name your variables descriptively.

      @abdulilah is partially right. You're not doing anything with the value returned by `getOperator` in line 38. Either assign to `a` or return immediately.
      Without it, `a` is still ' ' in line 40 and * is your default operator in line 51-52.

      • Slyde

        Thank you both. I was thinking it was being reset to a NULL value each iteration on line 34: char a = ' ';. That's what was throwing me off. Thanks again for the help.

        Works like a charm now.

        • Mike

          I used your code verbatim, even with your changes, and I still get the * after typing a non entry selection followed by a correct entry.

          I added two lines to the code to help me determine where/when the value is being changed back to the original invalid entry.

          In the getOperator(), I added it right before the return.
          In the main(), I added it right before the totalNums().

          The value remains correct up to the return in getOperator. The + operator is displayed correctly

          Yet when it returns to the main(), the value is now recorded as the original invalid entry.

          How is this so? How can it return an invalid entry when the If statement doesn't even allow the code to reach the return command until a correct entry is received?  So I don't understand how the invalid entry even gets transferred out of the function. Obviously, it is though, and it's probably something simple. So I can't wait to have my mine blown.

          • You're not using the return value of `getOperator` in line 20. `getOper` of the first call stays the same. The results of all recursive calls are discarded.

            • Mike

              Thanks again for replying! I really don't know how you have the time or even the patience for beginners like me.

              Ok, so you state the value for 'getOper' stays the same for the first call. But I'm confused (imagine that). So if there is no return value for 'getOperator' in line 20, then how does the caller even receive the value from 'getOper' for the invalid entry during its first call?

              This is a general outline of how I see the code's logic during execution? Please show me where I'm misinterpreting it, and remember I'm a beginner, so any clarification is always welcomed.

              1. Line 28: Choose an Operator: First time, I will enter invalid entry, like $
              2. Line 30: The getOper variable now stores this invalid entry with a $
              3. If statement determines the getOper value is invalid, and thus calls the getOperator()
              4. Back to Line 28: Choose an Operator: this time I choose a valid entry, like +
              5. Line 30: This time getOper stores the correct value, +
              6. Line 35: (for debugging) Confirms and displays the correct value for getOper, +
              7. Line 37: Returns the + value of getOper to the caller, or should anyways
              8. Line 10: Return value is stored in 'oper', BUT for some reason it's back to $
              9. Line 13: (for debugging): Confirms the invalid entry by displaying the $

              • 8. is where you're wrong. This is the return of the second call. When the second call returns, execution continues after line 20 of the first call.
                The returned value of the second call isn't used.
                Every time you call `getOperator`, a new `getOper` is created. I'll append numbers to the variables and calls so you can see the difference.

                1. Call to `getOperator1` from `main`.
                2. Invalid entry ($), stored in `getOper1`.
                3. Call to `getOperator2` from `getOperator1`
                4. Valid entry (+), stored in `getOper2`.
                5. `getOper2` is returned.
                6. Execution continues in `getOperator1`, after line 20 (Note: The returned value isn't used, `getOper1` is still '$').
                7. `getOperator1` returns `getOper1` ($)

                You can mark functions as `nodiscard`

                Now when you call `getOperator` and don't use the return value, you'll get a warning (Your compiler is allowed to ignore the attribute, but most compilers will issue a warning).

                • Mike

                  Thank you for the detailed outline and appending the numbers. It always helps to have some type of visual explanation. This really helped me to see how it is actually being processed/parsed or whatever you call it.

                  I had no idea the getOperator was being incremented or being considered as a separate instance of itself as you described for each time it is being called. I assume this is only because it is calling itself from within itself. I don't believe this has been covered yet in the tutorials, though if so, could you kindly point me to it so I can read more about it?

                  Thanks again!

  • aymnomous

    Can someone explain in section "Implementation step 2: Implement each function" what the Define the function prototype (inputs and outputs) means? Specifically the input and output part.

  • alfonso

    "Clean the house
            Vacuum the carpets
            Clean the bathrooms
                Scrub the toilet (yuck!)
                Wash the sink
            Clean the kitchen
                Clear the countertops
                Clean the countertops
                Scrub the sink
                Take out the trash

    Now we have a hierarchy of tasks, none of them particularly hard."

    I read it : Now we have a hierarchy of tasks, ONE of them is particularly hard.
    I thought - hmm, that one must be the "yuck!" one :)

  • Li

    I pretty much agree with the design philosophy expressed here. The one point I mostly disagree with is the "Focus on one area at a time." paragraph. I agree that you need to focus on one part at a time, once it's working at all. I very much don't agree that you should make that part "fully working" or take that part "through to completion". When you sit down to design your program, it is possible that you will spend enough time and effort to completely (and correctly) define the required program structure, data types, and needed processing, I/O and storage. Unlikely, but possible. The "right way" (arguably) to look at all but the simplest programs is that the process of creating them is a development process. It is iterative, with a lot of feedback (i.e. trial and error). There will almost certainly be some evolution of your understanding about program structure, data types, processing and I/O as you fill out the details. Meaning there will almost certainly be things you need to change/correct/improve.  So, rather than "fully" finish any one part of your program before moving on, I'd say MINIMALLY finish it. Get enough of it together and working and then move on to the next area. Build up its complexity and 'polish' while testing it. You want to spend the least time possible to get to the next "level" and that "level" might be a poor imitation of what the end product will be. Another way to describe this is to work on the 'pieces' one at a time, and improve them sequentially and incrementally. The question is how big of a increment, how much effort, to spend on one piece before moving to the next. I'd say use the law of diminishing returns. Do the obvious stuff to improve a component, test, and move on. To repeat, do NOT spend the time to fully perfect each individual part. Perfect is the enemy of the good.

  • James

    I have been learning the language of C++ for the past two months. I have managed to code a rudimentary calculator.

    Firstly, I tried to code the calculator so that the user is forced to enter 5 numbers.

    The code for this works without any errors.

    After this, I tried to give the user the option to stop at only two numbers, but for some reason, this part of the code is not working.

    When I compile the code for the calculator, it works without any errors but the code I have written for the makeDecision(), makeDecision2(), and makeDecision3() functions (the part of the code that give the user the option to only enter two numbers) are getting ignored.

    Can you please tell me how to fix this problem?

    Here is the code:

    • * Line 33, 34, 35, 75, 76, 77, 140, 142, 144, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170: Initialize your variables with brace initializers. You used copy initialization.
      * Line 39: Initialize your variables with brace initializers. You used direct initialization.
      * Line 18, 23, 39, 56, 68, 81, 94, 138: Limit your lines to 80 characters in length for better readability on small displays.
      * Enable compiler warnings and fix them. Lesson 0.10, 0.11.

      You can't store strings in ints or chars. See lesson 4.4b.
      Line 146-150 aren't calling the functions. Remove the "int" in front of them. Lesson 2.7.

    • Louis Cloete

      To add to @nascardriver's comments:

      1. You seem to know about the bool type. Don't use integers for YesNo. Use bools and let Yes be true and No be false. (see also chapter 4 for more on bool.)
      2. Don't use printf() to print output. Use std::cout. There is no good reason to mix and match printf() and std::cout in C++. You can always use std::cout. It is also simpler and less error prone.

  • Liquid

    I have a little problem with my program, it executes as it should but the outcome is not right.

    • * Line 25, 26, 27, 28, 47, 48, 49: Initialize your variables with brace initializers. You used copy initialization.
      * Line 7, 16: Initialize your variables with brace initializers.
      * Line 48, 51: Limit your lines to 80 characters in length for better readability on small displays.

      Enable compiler warnings (Lesson 0.10, 0.11). @z is unused in @getEquasion.

      • Liquid

        Wow, Thank you ^^ forgot to turn the warnings back on after reinstalling.
        what a stupid mistake now that I think about it ^^

        Well thank you for your great answer ^^

  • Rae

    This is beautifully explained. :)

  • Juan

    Is excellent know where to start!

Leave a Comment

Put all code inside code tags: [code]your code here[/code]