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:
The program would handle three different types of input error. First, if an input was not a valid number.
Second, if the input was negative:
Third, if both pounds and ounces were empty, or zero.
Here's their code:
Private Sub cmdConvert_Click()
'Declare varibles.
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
Dim userInput As String
'Input
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, pounds must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, pounds cannot be negative."
End
End If
pounds = userInput
'Validate ounces
userInput = Cells(4, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, ounces must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, ounces cannot be negative."
End
End If
ounces = userInput
'Check if both are empty.
If pounds = 0 And ounces = 0 Then
MsgBox "Sorry, please enter pounds, ounces, or both."
End
End If
'Processing
pounds = pounds + ounces / 16
kilos = pounds * 0.453592
'Output
Cells(6, 2) = Round(kilos, 3)
End Sub
Lines 2 to 6 declare variables:
Private Sub cmdConvert_Click()
'Declare varibles.
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
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:
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, pounds must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, pounds cannot be negative."
End
End If
pounds = userInput
The second part of the input validation handles ounces. That's lines 19 to 29.
'Validate ounces
userInput = Cells(4, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, ounces must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, ounces cannot be negative."
End
End If
ounces = userInput
The third part of the input validation tests whether both inputs are zero:
'Check if both are empty.
If pounds = 0 And ounces = 0 Then
MsgBox "Sorry, please enter pounds, ounces, or both."
End
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.
'Processing
pounds = pounds + ounces / 16
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:
'Output
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.
Private Sub cmdConvert_Click()
'Declare varibles.
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
Dim userInput As String
'Input
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, pounds must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, pounds cannot be negative."
End
End If
pounds = userInput
'Validate ounces
userInput = Cells(4, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, ounces must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, ounces cannot be negative."
End
End If
ounces = userInput
'Check if both are empty.
If pounds = 0 And ounces = 0 Then
MsgBox "Sorry, please enter pounds, ounces, or both."
End
End If
'Processing
pounds = pounds + ounces / 16
kilos = pounds * 0.453592
'Output
Cells(6, 2) = Round(kilos, 3)
End Sub
Processing
Let's start with the last two chunks. They're easiest. Here's processing:
'Processing
pounds = pounds + ounces / 16
kilos = pounds * 0.453592
Start by giving the code a name:
'Processing
Sub convertToKilos()
pounds = pounds + ounces / 16
kilos = pounds * 0.453592
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.
'Processing
Sub convertToKilos()
pounds = pounds + ounces / 16
kilos = pounds * 0.453592
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:
Private Sub cmdConvert_Click()
'Declare varibles.
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
Dim userInput As String
kilos is a Single. Let's add that param:
'Processing
Sub convertToKilos(kilos as Single)
pounds = pounds + ounces / 16
kilos = pounds * 0.453592
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.
'Processing
Sub convertToKilos(pounds as Single, ounces as Single, kilos as Single)
pounds = pounds + ounces / 16
kilos = pounds * 0.453592
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.
'Compute kilos.
'Params:
' pounds: pounds (input)
' ounces: ounces (input)
' kilos: result (output)
Sub convertToKilos(pounds as Single, ounces as Single, kilos as Single)
pounds = pounds + ounces / 16
kilos = pounds * 0.453592
End Sub
A common mistake is to try to declare params in the sub, like this:
'Compute kilos.
'Params:
' pounds: pounds (input)
' ounces: ounces (input)
' kilos: result (output)
Sub convertToKilos(pounds as Single, ounces as Single, kilos as Single)
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
pounds = pounds + ounces / 16
kilos = pounds * 0.453592
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:
Private Sub cmdConvert_Click()
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
'Input
getInput pounds, ounces
'Processing
convertToKilos pounds, ounces, kilos
'Output
outputKilos kilos
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.
'Output
Cells(6, 2) = Round(kilos, 3)
We need to:
Give the sub a name.
Add params.
Here's the name part:
Sub outputKilos()
Cells(6, 2) = Round(kilos, 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:
Sub outputKilos()
Cells(6, 2) = Round(kilos, 3)
End Sub
So:
Sub outputKilos(kilos as Single)
Cells(6, 2) = Round(kilos, 3)
End Sub
Now the comments:
'Output kilos.
'Params:
' kilos: kilos (input)
Sub outputKilos(kilos As Single)
Cells(6, 2) = Round(kilos, 3)
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:
Private Sub cmdConvert_Click()
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
'Input
getInput pounds, ounces
'Processing
convertToKilos pounds, ounces, kilos
'Output
outputKilos kilos
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:
'Input
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, pounds must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, pounds cannot be negative."
End
End If
pounds = userInput
'Validate ounces
userInput = Cells(4, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, ounces must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, ounces cannot be negative."
End
End If
ounces = userInput
'Check if both are empty.
If pounds = 0 And ounces = 0 Then
MsgBox "Sorry, please enter pounds, ounces, or both."
End
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.
Sub getInput()
'Input
'Validate pounds
userInput = Cells(3, 2)
...
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:
Sub getInput()
'Input
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, pounds must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, pounds cannot be negative."
End
End If
pounds = userInput
'Validate ounces
userInput = Cells(4, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, ounces must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, ounces cannot be negative."
End
End If
ounces = userInput
'Check if both are empty.
If pounds = 0 And ounces = 0 Then
MsgBox "Sorry, please enter pounds, ounces, or both."
End
End If
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:
'Input and validate pounds and ounces
'Params:
' pounds: value user entered (output).
' ounces: value user entered (output).
Sub getInput(pounds As Single, ounces As Single)
...
pounds and ounces are labeled as output. Why?
Klaus
Because they're output from the sub. The sub hands them back to the main program.
Tara
Right!
A local variable
If we ran the program right now, we'd get an error:
Every variable has to be defined somewhere. For a param, it can be defined in the caller, like this:
Private Sub cmdConvert_Click()
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
'Input
getInput pounds, ounces
'Processing
convertToKilos pounds, ounces, kilos
'Output
outputKilos kilos
End Sub
Sub getInput(pounds As Single, ounces As Single)
...
End Sub
Sub convertToKilos(pounds As Single, ounces As Single, kilos As Single)
...
End Sub
Sub outputKilos(kilos As Single)
...
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:
Sub getInput(pounds As Single, ounces As Single)
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
Line 3 has a variable that isn't from a param:
Sub getInput(pounds As Single, ounces As Single)
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
We could add userInput as a param, and define it in the main program.
Private Sub cmdConvert_Click()
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
Dim userInput As String
'Input
getInput pounds, ounces, userInput
'Processing
convertToKilos pounds, ounces, kilos
'Output
outputKilos kilos
End Sub
Sub getInput(pounds As Single, ounces As Single, userInput as String)
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
...
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:
Private Sub cmdConvert_Click()
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
'Input
getInput pounds, ounces
'Processing
convertToKilos pounds, ounces, kilos
'Output
outputKilos kilos
End Sub
Sub getInput(pounds As Single, ounces As Single)
Dim userInput As String
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
...
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:
Private Sub cmdConvert_Click()
Dim pounds As Single
Dim ounces As Single
Dim kilos As Single
'Input
getInput pounds, ounces
'Processing
convertToKilos pounds, ounces, kilos
'Output
outputKilos kilos
End Sub
'Input and validate pounds and ounces
'Params:
' pounds: value user entered (output).
' ounces: value user entered (output).
Sub getInput(pounds As Single, ounces As Single)
Dim userInput As String
'Validate pounds
userInput = Cells(3, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, pounds must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, pounds cannot be negative."
End
End If
pounds = userInput
'Validate ounces
userInput = Cells(4, 2)
If Not IsNumeric(userInput) Then
MsgBox "Sorry, ounces must be a number."
End
End If
If userInput < 0 Then
MsgBox "Sorry, ounces cannot be negative."
End
End If
ounces = userInput
'Check if both are zero.
If pounds = 0 And ounces = 0 Then
MsgBox "Sorry, please enter pounds, ounces, or both."
End
End If
End Sub
'Compute kilos.
'Params:
' pounds: pounds (input)
' ounces: ounces (input)
' kilos: result (output)
Sub convertToKilos(pounds As Single, ounces As Single, kilos As Single)
pounds = pounds + ounces / 16
kilos = pounds / 2.20462
End Sub
'Output kilos.
'Params:
' kilos: kilos (input)
Sub outputKilos(kilos As Single)
Cells(6, 2) = Round(kilos, 3)
End Sub
Ah!
Top-down versus bottom-up
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
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.
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.