Subing a program

Keywords: 

A redo

Let's have a look at this sub business from another angle. Earlier, Klaus and Jeremy wrote a program to convert pounds and ounces in kilos. Here's some output:

Output

The program would handle three different types of input error. First, if an input was not a valid number.

Error

Second, if the input was negative:

Error

Third, if both pounds and ounces were empty, or zero.

Error

Here's their code:

  1. Private Sub cmdConvert_Click()
  2.     'Declare varibles.
  3.     Dim pounds As Single
  4.     Dim ounces As Single
  5.     Dim kilos As Single
  6.     Dim userInput As String
  7.     'Input
  8.     'Validate pounds
  9.     userInput = Cells(3, 2)
  10.     If Not IsNumeric(userInput) Then
  11.         MsgBox "Sorry, pounds must be a number."
  12.         End
  13.     End If
  14.     If userInput < 0 Then
  15.         MsgBox "Sorry, pounds cannot be negative."
  16.         End
  17.     End If
  18.     pounds = userInput
  19.     'Validate ounces
  20.     userInput = Cells(4, 2)
  21.     If Not IsNumeric(userInput) Then
  22.         MsgBox "Sorry, ounces must be a number."
  23.         End
  24.     End If
  25.     If userInput < 0 Then
  26.         MsgBox "Sorry, ounces cannot be negative."
  27.         End
  28.     End If
  29.     ounces = userInput
  30.     'Check if both are empty.
  31.     If pounds = 0 And ounces = 0 Then
  32.         MsgBox "Sorry, please enter pounds, ounces, or both."
  33.         End
  34.     End If
  35.     'Processing
  36.     pounds = pounds + ounces / 16
  37.     kilos = pounds * 0.453592
  38.     'Output
  39.     Cells(6, 2) = Round(kilos, 3)
  40. End Sub
Lines 2 to 6 declare variables:

  1. Private Sub cmdConvert_Click()
  2.     'Declare varibles.
  3.     Dim pounds As Single
  4.     Dim ounces As Single
  5.     Dim kilos As Single
  6.     Dim userInput As String
Lines 7 to to 29 get and validate the user input. There are three pieces to it. The first one handles the pounds. That's lines 8 to 18:

  1.     'Validate pounds
  2.     userInput = Cells(3, 2)
  3.     If Not IsNumeric(userInput) Then
  4.         MsgBox "Sorry, pounds must be a number."
  5.         End
  6.     End If
  7.     If userInput < 0 Then
  8.         MsgBox "Sorry, pounds cannot be negative."
  9.         End
  10.     End If
  11.     pounds = userInput

The second part of the input validation handles ounces. That's lines 19 to 29.

  1.     'Validate ounces
  2.     userInput = Cells(4, 2)
  3.     If Not IsNumeric(userInput) Then
  4.         MsgBox "Sorry, ounces must be a number."
  5.         End
  6.     End If
  7.     If userInput < 0 Then
  8.         MsgBox "Sorry, ounces cannot be negative."
  9.         End
  10.     End If
  11.     ounces = userInput
The third part of the input validation tests whether both inputs are zero:

  1.     'Check if both are empty.
  2.     If pounds = 0 And ounces = 0 Then
  3.         MsgBox "Sorry, please enter pounds, ounces, or both."
  4.         End
  5.     End If
So, here's the structure of the program so far:

  • Get input
    • Get and validate pounds
    • Get and validate ounces
    • Test if both inputs are zero

I've left out the part declaring variables, because it doesn't affect the program's logic.

That's all for input. The next part of the program does the processing.

  1.     'Processing
  2.     pounds = pounds + ounces / 16
  3.     kilos = pounds * 0.453592
Not much to it. Like many programs, most of the code is for validation.

The last part of the program does output:

  1.     'Output
  2.     Cells(6, 2) = Round(kilos, 3)
The complete structure of the program:

  • Input
    • Get and validate pounds
    • Get and validate ounces
    • Test if both inputs are zero
  • Processing
  • Output

There's that good ol' IPO.

Sub it

Let's change the program, using subs. The first version will just makes subs for I, P, and O. Here's the original code, with the chunks marked. We'll make a sub for each one.

  1. Private Sub cmdConvert_Click()
  2.     'Declare varibles.
  3.     Dim pounds As Single
  4.     Dim ounces As Single
  5.     Dim kilos As Single
  6.     Dim userInput As String
  7.     'Input
  8.     'Validate pounds
  9.     userInput = Cells(3, 2)
  10.     If Not IsNumeric(userInput) Then
  11.         MsgBox "Sorry, pounds must be a number."
  12.         End
  13.     End If
  14.     If userInput < 0 Then
  15.         MsgBox "Sorry, pounds cannot be negative."
  16.         End
  17.     End If
  18.     pounds = userInput
  19.     'Validate ounces
  20.     userInput = Cells(4, 2)
  21.     If Not IsNumeric(userInput) Then
  22.         MsgBox "Sorry, ounces must be a number."
  23.         End
  24.     End If
  25.     If userInput < 0 Then
  26.         MsgBox "Sorry, ounces cannot be negative."
  27.         End
  28.     End If
  29.     ounces = userInput
  30.     'Check if both are empty.
  31.     If pounds = 0 And ounces = 0 Then
  32.         MsgBox "Sorry, please enter pounds, ounces, or both."
  33.         End
  34.     End If
  35.     'Processing
  36.     pounds = pounds + ounces / 16
  37.     kilos = pounds * 0.453592
  38.     'Output
  39.     Cells(6, 2) = Round(kilos, 3)
  40. End Sub

Processing

Let's start with the last two chunks. They're easiest. Here's processing:

  1.     'Processing
  2.     pounds = pounds + ounces / 16
  3.     kilos = pounds * 0.453592
Start by giving the code a name:

  1. 'Processing
  2. Sub convertToKilos()
  3.     pounds = pounds + ounces / 16
  4.     kilos = pounds * 0.453592
  5. End Sub
The second thing we need to do is add params (parameters). Params are values that go into and out of the sub. The purpose of the sub is to work out kilos. You can see that in the last executable line of code.

  1. 'Processing
  2. Sub convertToKilos()
  3.     pounds = pounds + ounces / 16
  4.     kilos = pounds * 0.453592
  5. End Sub
So, that's one of the params. We need to know its data type: string, single, integer, whatevs. Look at the declarations in the original code:

  1. Private Sub cmdConvert_Click()
  2.     'Declare varibles.
  3.     Dim pounds As Single
  4.     Dim ounces As Single
  5.     Dim kilos As Single
  6.     Dim userInput As String
kilos is a Single. Let's add that param:

  1. 'Processing
  2. Sub convertToKilos(kilos as Single)
  3.     pounds = pounds + ounces / 16
  4.     kilos = pounds * 0.453592
  5. End Sub
OK, that's the output of convertToKilos(). When I say "output" here, I'm taking about the output of the sub, not the program. We're just doing convertToKilos() now. We can forget the rest of the program for the moment. That's one of the Good Things about subs.

The code needs values for pounds and ounces. Let's add those as params.

  1. 'Processing
  2. Sub convertToKilos(pounds as Single, ounces as Single, kilos as Single)
  3.     pounds = pounds + ounces / 16
  4.     kilos = pounds * 0.453592
  5. End Sub
pounds and ounces are inputs to the sub. That is, the sub takes them, and uses them to compute the output value, kilos. Input params are usually listed before output params in a sub's signature.

The last step is to add comments to the sub, explaining what it does, and what the params are. Don't forget this! These comments are what other programmers will use to call your sub.

  1. 'Compute kilos.
  2. 'Params:
  3. '  pounds: pounds (input)
  4. '  ounces: ounces (input)
  5. '  kilos: result (output)
  6. Sub convertToKilos(pounds as Single, ounces as Single, kilos as Single)
  7.     pounds = pounds + ounces / 16
  8.     kilos = pounds * 0.453592
  9. End Sub
A common mistake is to try to declare params in the sub, like this:

  1. 'Compute kilos.
  2. 'Params:
  3. '  pounds: pounds (input)
  4. '  ounces: ounces (input)
  5. '  kilos: result (output)
  6. Sub convertToKilos(pounds as Single, ounces as Single, kilos as Single)
  7.     Dim pounds As Single
  8.     Dim ounces As Single
  9.     Dim kilos As Single
  10.     pounds = pounds + ounces / 16
  11.     kilos = pounds * 0.453592
  12. End Sub
Won't work. VBA will complain. When you use a param in a sub's signature, you're declaring it right there

How is convertToKilos() used? Here's the main program:

  1. Private Sub cmdConvert_Click()
  2.     Dim pounds As Single
  3.     Dim ounces As Single
  4.     Dim kilos As Single
  5.     'Input
  6.     getInput pounds, ounces
  7.     'Processing
  8.     convertToKilos pounds, ounces, kilos
  9.     'Output
  10.     outputKilos kilos
  11. End Sub
(Notice how simple the program is!)

pounds, ounces, and kilos are declared in the main program, and used when calling the subs. So, you Dim variables in the caller. Subs get them as params.

Let's summarize what we've done. Here's the program's structure:

  • Input
    • Get and validate pounds
    • Get and validate ounces
    • Test if both inputs are zero
  • Processing
  • Output

A bunch of steps that are easy to understand. We've taken the processing step…

  • Input
    • Get and validate pounds
    • Get and validate ounces
    • Test if both inputs are zero
  • Processing
  • Output

… made a sub for it. That's the goal. A sub for each chunk of the program.

The output sub

This one is easy, too. Here's the original code for it.

  1.     'Output
  2.     Cells(6, 2) = Round(kilos, 3)
We need to:

  • Give the sub a name.
  • Add params.

Here's the name part:

  1. Sub outputKilos()
  2.     Cells(6, 2) = Round(kilos, 3)
  3. End Sub
A good name, that says exactly what the code does.

Now let's do the params. The code only needs one value:

  1. Sub outputKilos()
  2.     Cells(6, 2) = Round(kilos, 3)
  3. End Sub
So:

  1. Sub outputKilos(kilos as Single)
  2.     Cells(6, 2) = Round(kilos, 3)
  3. End Sub
Now the comments:

  1. 'Output kilos.
  2. 'Params:
  3. '  kilos: kilos (input)
  4. Sub outputKilos(kilos As Single)
  5.     Cells(6, 2) = Round(kilos, 3)
  6. End Sub
Line 3 says that kilos is an input. That's correct. kilos is an input to this sub. Even though kilos is an output from the entire program, it's an input for this chunk of code.

Here's the main program again:

  1. Private Sub cmdConvert_Click()
  2.     Dim pounds As Single
  3.     Dim ounces As Single
  4.     Dim kilos As Single
  5.     'Input
  6.     getInput pounds, ounces
  7.     'Processing
  8.     convertToKilos pounds, ounces, kilos
  9.     'Output
  10.     outputKilos kilos
  11. End Sub
You can see the call the outputKilos at the bottom.

The input

OK, this is the complicated one. Here's the original code:

  1.     'Input
  2.     'Validate pounds
  3.     userInput = Cells(3, 2)
  4.     If Not IsNumeric(userInput) Then
  5.         MsgBox "Sorry, pounds must be a number."
  6.         End
  7.     End If
  8.     If userInput < 0 Then
  9.         MsgBox "Sorry, pounds cannot be negative."
  10.         End
  11.     End If
  12.     pounds = userInput
  13.     'Validate ounces
  14.     userInput = Cells(4, 2)
  15.     If Not IsNumeric(userInput) Then
  16.         MsgBox "Sorry, ounces must be a number."
  17.         End
  18.     End If
  19.     If userInput < 0 Then
  20.         MsgBox "Sorry, ounces cannot be negative."
  21.         End
  22.     End If
  23.     ounces = userInput
  24.     'Check if both are empty.
  25.     If pounds = 0 And ounces = 0 Then
  26.         MsgBox "Sorry, please enter pounds, ounces, or both."
  27.         End
  28.     End If
Let's make it into a sub.

The two steps to making a sub from existing code are:

  • The name.
  • The params.

The name is easier.

  1. Sub getInput()
  2.     'Input
  3.     'Validate pounds
  4.     userInput = Cells(3, 2)
  5.     ...
  6. End Sub
OK, now the params. This code gets input from the worksheet:

bc. userInput = Cells(3, 2)

There are no input params; the sub grabs data from the worksheet, and sends it out. Some input subs have input params, e.g., the names of files to open, or the numbers of cells to read from. We'll leave this one without input params.

What about output params? Here they are:

  1. Sub getInput()
  2.     'Input
  3.     'Validate pounds
  4.     userInput = Cells(3, 2)
  5.     If Not IsNumeric(userInput) Then
  6.         MsgBox "Sorry, pounds must be a number."
  7.         End
  8.     End If
  9.     If userInput < 0 Then
  10.         MsgBox "Sorry, pounds cannot be negative."
  11.         End
  12.     End If
  13.     pounds = userInput
  14.     'Validate ounces
  15.     userInput = Cells(4, 2)
  16.     If Not IsNumeric(userInput) Then
  17.         MsgBox "Sorry, ounces must be a number."
  18.         End
  19.     End If
  20.     If userInput < 0 Then
  21.         MsgBox "Sorry, ounces cannot be negative."
  22.         End
  23.     End If
  24.     ounces = userInput
  25.     'Check if both are empty.
  26.     If pounds = 0 And ounces = 0 Then
  27.         MsgBox "Sorry, please enter pounds, ounces, or both."
  28.         End
  29.     End If
  30. End Sub
All that code, just to get values for two params. That's normal in business programming.

Let's add them to the sig, and document them:

  1. 'Input and validate pounds and ounces
  2. 'Params:
  3. '  pounds: value user entered (output).
  4. '  ounces: value user entered (output).
  5. Sub getInput(pounds As Single, ounces As Single)
  6. ...
pounds and ounces are labeled as output. Why?

Klaus
Klaus
Because they're output from the sub. The sub hands them back to the main program.
Tara
Tara
Right!

A local variable

If we ran the program right now, we'd get an error:

Error

Every variable has to be defined somewhere. For a param, it can be defined in the caller, like this:

  1. Private Sub cmdConvert_Click()
  2.     Dim pounds As Single
  3.     Dim ounces As Single
  4.     Dim kilos As Single
  5.     'Input
  6.     getInput pounds, ounces
  7.     'Processing
  8.     convertToKilos pounds, ounces, kilos
  9.     'Output
  10.     outputKilos kilos
  11. End Sub
  12. Sub getInput(pounds As Single, ounces As Single)
  13. ...
  14. End Sub
  15. Sub convertToKilos(pounds As Single, ounces As Single, kilos As Single)
  16. ...
  17. End Sub
  18. Sub outputKilos(kilos As Single)
  19. ...
  20. End Sub
pounds, ounces, and kilos are all defined in the main program, then passed to the subs as params. That works fine.

Here's code from the input sub:

  1. Sub getInput(pounds As Single, ounces As Single)
  2.     'Validate pounds
  3.     userInput = Cells(3, 2)
  4.     If Not IsNumeric(userInput) Then
Line 3 has a variable that isn't from a param:

  1. Sub getInput(pounds As Single, ounces As Single)
  2.     'Validate pounds
  3.     userInput = Cells(3, 2)
  4.     If Not IsNumeric(userInput) Then
We could add userInput as a param, and define it in the main program.

  1. Private Sub cmdConvert_Click()
  2.     Dim pounds As Single
  3.     Dim ounces As Single
  4.     Dim kilos As Single
  5.     Dim userInput As String
  6.     'Input
  7.     getInput pounds, ounces, userInput
  8.     'Processing
  9.     convertToKilos pounds, ounces, kilos
  10.     'Output
  11.     outputKilos kilos
  12. End Sub
  13. Sub getInput(pounds As Single, ounces As Single, userInput as String)
  14.     'Validate pounds
  15.     userInput = Cells(3, 2)
  16.     If Not IsNumeric(userInput) Then
  17.     ...
Is that a good idea? Hmm… userInput is not used by any other sub. Declaring it in the main program lets the program run, but it makes the code untidy, harder to understand, and more prone to bugs. And screaming.

userInput is a temporary variable, only used in getInput. So, we should define it in getInput:

  1. Private Sub cmdConvert_Click()
  2.     Dim pounds As Single
  3.     Dim ounces As Single
  4.     Dim kilos As Single
  5.     'Input
  6.     getInput pounds, ounces
  7.     'Processing
  8.     convertToKilos pounds, ounces, kilos
  9.     'Output
  10.     outputKilos kilos
  11. End Sub
  12. Sub getInput(pounds As Single, ounces As Single)
  13.     Dim userInput As String
  14.     'Validate pounds
  15.     userInput = Cells(3, 2)
  16.     If Not IsNumeric(userInput) Then
  17.     ...
Better! The main program stays clean and tidy. userInput is declared just where it is needed. It doesn't show up anywhere else.

userInput is a local variable. The CPU allocates memory for it when it runs getInput. When the CPU exits the sub, it releases the memory, and userInput ceases to exist.

Use locals when you can. Reserve params for data that needs to pass between subs.

The whole shebang

Here's all the code:

  1. Private Sub cmdConvert_Click()
  2.     Dim pounds As Single
  3.     Dim ounces As Single
  4.     Dim kilos As Single
  5.     'Input
  6.     getInput pounds, ounces
  7.     'Processing
  8.     convertToKilos pounds, ounces, kilos
  9.     'Output
  10.     outputKilos kilos
  11. End Sub
  12. 'Input and validate pounds and ounces
  13. 'Params:
  14. '  pounds: value user entered (output).
  15. '  ounces: value user entered (output).
  16. Sub getInput(pounds As Single, ounces As Single)
  17.     Dim userInput As String
  18.     'Validate pounds
  19.     userInput = Cells(3, 2)
  20.     If Not IsNumeric(userInput) Then
  21.         MsgBox "Sorry, pounds must be a number."
  22.         End
  23.     End If
  24.     If userInput < 0 Then
  25.         MsgBox "Sorry, pounds cannot be negative."
  26.         End
  27.     End If
  28.     pounds = userInput
  29.     'Validate ounces
  30.     userInput = Cells(4, 2)
  31.     If Not IsNumeric(userInput) Then
  32.         MsgBox "Sorry, ounces must be a number."
  33.         End
  34.     End If
  35.     If userInput < 0 Then
  36.         MsgBox "Sorry, ounces cannot be negative."
  37.         End
  38.     End If
  39.     ounces = userInput
  40.     'Check if both are zero.
  41.     If pounds = 0 And ounces = 0 Then
  42.         MsgBox "Sorry, please enter pounds, ounces, or both."
  43.         End
  44.     End If
  45. End Sub
  46. 'Compute kilos.
  47. 'Params:
  48. '  pounds: pounds (input)
  49. '  ounces: ounces (input)
  50. '  kilos: result (output)
  51. Sub convertToKilos(pounds As Single, ounces As Single, kilos As Single)
  52.     pounds = pounds + ounces / 16
  53.     kilos = pounds / 2.20462
  54. End Sub
  55. 'Output kilos.
  56. 'Params:
  57. '  kilos: kilos (input)
  58. Sub outputKilos(kilos As Single)
  59.     Cells(6, 2) = Round(kilos, 3)
  60. End Sub
Ah!

Top-down versus bottom-up

Lily
Lily
OK, we started this lesson with code that the dudes had written. Then we made some subs, and then the main program.

Is that how you work when you don't have existing code?

Tara
Tara
No. What we did here is called bottom-up. We made a sub – computeKilos – and then worked out how to call it.

When I write a program, I don't start with code from the dudes. I start from scratch. I work top-down. I work out the structure, often as bullet points. Then I write the main program, and then the subs.

That's what you should do, too. We did this one bottom up, so you could better understand how subs work. But top-down is the way to go.

The process of taking a chunk of functionality (like getting input, doing processing, or showing output) and making a subroutine out of it is called encapsulation. It's one of the Big Ideas of programming. Encapsulation is key to writing complex software. Take a task, break it into chunks, with a sub for each one. If those chunks are complex, break them up into smaller chunks. And so on.

Big Idea

Encapsulation

Take a chunk of functionality, and make it into a subroutine.

Summary

Earlier, Klaus and Jeremy wrote a program to convert pounds and ounces in kilos. We broke out the I, P, and O code into their own subroutines.