More on
|
|
We have encountered various types of parameters, which are summarized below:
Recall that with a value parameter, which is declared in the formal parameter list by using the keyword ByVal, the procedure receives a copy of the actual parameter's value. With a reference parameter, which is declared in the formal parameter list by using the keyword ByRef, the procedure receives the location (memory address) of the actual parameter. There should be the same number of actual parameters in a procedure call as there are formal parameters in the procedure heading. (VB actually allows you to violate this--more later.) Also, each actual parameter should have the same data type as the formal parameter in the same position. Because value parameters are passed copies of their actual parameters, anything that has a value may be passed to a value parameter. This includes constants, variables, and even expressions. Expressions are evaluated and a copy of the result is sent to the corresponding value parameter. Because a value parameter does not receive the location of the actual parameter, the actual parameter cannot be directly accessed or changed. When a procedure returns, the contents of any value parameters are destroyed, along with the contents of the local variables. The difference between value parameters and local variables is that the values of local variables are undefined when a procedure starts to execute, whereas value parameters are automatically initialized to the values of the corresponding actual parameters. Because the contents of value parameters are destroyed when the procedure returns, they cannot be used to return information to the calling code. When reference parameters are used, the called procedure refers to the corresponding actual parameter directly. Specifically, the procedure is allowed to inspect and modify the caller's actual parameter. When a procedure is invoked using a reference parameter, it is the location (memory address) of the actual parameter, not its value, that is passed to the procedure. There is only one copy of the information, and it is used by both the caller and the called procedure. When a procedure is called, the actual parameter and formal parameter become synonyms for the same location in memory. Whatever value is left by the called procedure in this location is the value that the caller will find there. Therefore, you must be careful using a formal reference parameter because any change made to it affects the actual parameter in the calling code. Only a variable can be passed as an actual parameter to a reference parameter, because a procedure can assign a new value to the actual parameter. (In contrast, remember that an arbitrarily complicated expression can be passed to a value parameter.) The body of a procedure is like any other segment of code, except that it is contained in a separate block within the program. Isolating a segment of code in a separate block means that its implementation details can be "hidden" from view. As long as you know how to call a procedure and what its purpose is, you can use it without knowing how it actually works. For example, you don't know how the code for a library function like IsNumeric is written (its implementation is hidden from view), yet you still can use it effectively. The specification of what a procedure does and how it is invoked defines its interface. By hiding a procedure's implementation, or encapsulating the procedure, we can make changes to it without changing the calling routine, as long as the interface remains the same. For example, you might rewrite the body of a procedure using a more efficient algorithm. The figure below indicates that the function heading, included in the unshaded segment, is known to other procedures within the program. The actual implementation of the function, that is, the statements that make up the body of the function, are in the shaded area, indicating that they are hidden from view.
Encapsulation is what we do in the functional decomposition process when we postpone the solution of a difficult subproblem. We write down its purpose and what information it takes and returns, and then we write the rest of our design as if the subproblem already had been solved. We could hand this interface specification to someone else, and that person could develop a procedure for us that solves the subproblem. We needn't be concerned about how it works, as long as it conforms to the interface specification. Interfaces and encapsulation are the basis for team programming, in which a group of programmers work together to solve a large problem. Thus, designing a procedure can (and should) be divided into two tasks: designing the interface and designing the implementation. We already know how to design an implementation--it is a segment of code that corresponds to an algorithm. To design the interface, we focus on the what, not the how. We must define the behavior of the procedure (what it does) and the mechanism for communicating with it. To define the mechanism for communicating with the procedure, make a list of the following items:
Decide which identifiers inside the module match the values in this list. These identifiers become the variables in the formal parameter list for the procedure. Then the formal parameters are declared in the procedure heading. All other variables that the procedure needs are local and must be declared within the body of the procedure. This process may be repeated for all the modules at each level. Documenting the Direction of Data Flow Another helpful piece of documentation in a procedure interface is the direction of data flow for each parameter in the parameter list. Data flow is the flow of information between the procedure and its caller. Each parameter can be classified as an incoming parameter, an outgoing parameter, or an incoming/outgoing parameter. (Some people refer to these as input parameters, output parameters, and input/output parameters.) For an incoming parameter, the direction of data flow is one-way -- into the procedure. The procedure inspects and uses the current value of the parameter but does not modify it. In a comment section preceding the procedure heading, we include a description of the procedure, as well as a list of the parameters annotated by the classification in, out, or in/out. Pass-by-value is appropriate for each parameter that is incoming only. As you can see in the procedure body, displayAverage does not modify the values of the parameters sum and count. It merely uses their current values. The direction of data flow is one-way -- into the procedure. The data flow for an outgoing parameter is one-way -- out of the procedure. The procedure produces a new value for the parameter without using the old value in any way. Pass-by-reference must be used for an outgoing parameter Finally, the data flow for an incoming/outgoing parameter is two-way -- into and out of the procedure. The procedure uses the old value and also produces a new value for the parameter. Here is an example of a procedure that uses two parameters, one of them incoming only and the other one incoming/outgoing: This procedure first inspects the incoming value of balance so that it can evaluate the expression to the right of the equal sign. Then it stores a new value in balance by using the assignment operation. The data flow for balance is therefore considered a two-way flow of information. Pass-by-value is appropriate for deposit (it's incoming only), but pass-by-reference is required for balance (it's an incoming/outgoing parameter). Functions Returning more than one value from a value-returning function (by modifying the actual parameters) is an unwanted side effect and should be avoided. If your interface design calls for multiple values to be returned or for the values of actual parameters to be changed, then you should use a sub. There aren't any formal rules for determining when to use a sub and when to use a function, but here are some guidelines
Functions provide a way of simulating the mathematical concept of a function. One of the advantages of a modular design is that you can test it long before the code has been written for all of the modules. If we test each module individually, then we can assemble the modules into a complete program with much greater confidence that the program is correct. In this section, we introduce a technique for testing a module separately. Stubs and Drivers Suppose you were given the code for a module and your job was to test it. How would you test a single module by itself? First of all, it must be called by something (unless it is the main procedure). Second, it may have calls to other modules that aren't available to you. To test the module, you must fill in these missing links. When a module contains calls to other modules, we can write dummy procedure called stubs to satisfy those calls. A stub usually consists of an output statement that prints a message such as , "Function IsLeapYear just got called." Even though the stub is a dummy, it allows us to determine whether the procedure is called at the right time. A stub can also be used to print the set of values that are passed to it; this tells us whether or not the module being tested is supplying the proper information. Sometimes the stub will assign new values to its reference parameters to simulate data being read or results being computed to give the module something to keep working on. Because we can choose the values that are returned by the stub, we have better control over the conditions of the test run. In addition to supplying a stub for each call within the module, you may also provide a dummy program -- a driver -- to call the module itself. A driver program contains the bare minimum of code required to call the module being tested. By surrounding a module with a driver and stubs, you gain complete control of the conditions under which it executes. This allows you to test different situations and combinations that may reveal errors Testing and Debugging Hints
|