When you build modules, you need to be able to reuse them in as many different situations as possible, both within the current application and perhaps even in other, yet-to-be-developed systems. In order to do this, you will want to create modules that are as independent as possible both from other modules and from data structures defined in your current application.
Think of a module as a black box, which hides implementational complexities, often changing data structures, workarounds, and other sensitive elements of the application. Programmers using the module should not have to know how it does its job; they need only know the module's name, parameters, and return value (if a function). On the flip side (the inside of the module), the module should not be dependent on anything but the values coming in through the parameter list in order to do its job.
Personal computers move steadily towards an architecture of "plug and play:" the various components of the computer can be plugged together in different combinations and from different manufacturers. You will be certain that the result is a functioning computer because the hardware and software manufacturers agree on the standards that govern how the different components interact. They also (try to) make certain that nothing inside their own components causes a conflict with another component. Each product respects the boundaries around another's area of functionality.
The advantages of plug and play in the computer marketplace are many: lowered cost due to reduced reliance on proprietary technology, increased reliability, greater choice for consumers, and easier upgrades. All of these same advantages accrue to software following the same plug and play strategy. If you design your modules so that they are both tightly focused on a particular area of functionality and are simultaneously as generic as possible, and if those modules interact through well-defined interfaces, the resulting applications will:
Contain fewer bugs. The more you rely on the same modules again and again, the less code you will write, and, as a result, there will be a narrower opportunity to introduce new problems into your code.
Adapt more easily to change (enhancement and redesign). Change in one module is less likely to cause a ripple effect through all the rest of your code. As long as the interface through which calls are made to that module does not change, no other programs are affected.
Be developed more rapidly. As the percentage of reusable code increases, you only need to concentrate on building additional layers of modules to add new functionality. If a module's design does not constrain its usefulness, you will use it more often.
But I suppose you have read all of this before. The question I'd like to address now concerns what you can do to improve the plug and play quality of your code. In this section I offer some general tips and examples that I have found helpful.
You build a module to meet a requirement. A requirement is often stated in the most straightforward terms: "Calculate the total profits of a company," "Look up the description for a code," "Display the contents of a PL/SQL table," "Count the number of words in a string." Most developers proceed to implement these requirements in the most straightforward manner possible, without giving much thought to the full variety of ways in which the module might be used. The resulting program will probably work fine for the current requirement. Chances are great, however, that the requirement will expand or you will encounter a slightly different requirement in the next application -- either of which will require a revamping of the module.
Whenever you build a module, take a moment to examine the motivation for the program. What immediate needs does it satisfy? Do you see other ways that the module might, could, or should be used? Can you incorporate these other areas of functionality into the module while still preserving the original purpose of the module? If so, you will end up with a module that, while requiring more code and is more complicated internally, will be more widely used and appreciated.
Sure, I want you to stretch, to use your imagination, to think of the many different contexts in which a module might be used. I do not, however, want you to end up with a kitchen sink module that does everything, or does a little bit of everything for every situation. The trick is to "fill out" the module's applicability without expanding its scope.
If your function is supposed to count the number of separate "words" in a string, by all means broaden your concept of word to include punctuation. Give users of the module the option of counting just actual words, just punctuation, or all words. But don't also "enhance" the module to return the size of the largest word, the size of the smallest word, and the number of spaces in the word. Chances are that no one will use such a program.
A module should never be used as a dumping ground for miscellaneous actions, which otherwise do not seem to have a place to go. For a module to be applicable to many different situations in more than one application, it should do one thing and do it well.
A direct consequence of making a module generic and flexible is that you will increase the length of your parameter list. Compare the parameter lists of the two procedures shown in the following example, in which two modules count the number of words in a string:
FUNCTION number_of_words (string_in IN VARCHAR2) /* || Function returns the number of words in a string. A word || is any set of characters separated by a space. */ RETURN INTEGER; FUNCTION number_of_atomics /* || Function returns the number of atomics in a string. An atomic is || either a "word" (contiguous numbers or letters) or a delimiter. || || Examples of results: || number_of_atomics ('this, is%not.') ==> 7 || number_of_atomics ('this, is%not.', 'WORD') ==> 3 || number_of_atomics ('this, is%not.', 'DELIMITER') ==> 4 || number_of_atomics ('this, is%not.', 'WORD', ' ') ==> 2 */ (string_in IN VARCHAR2, count_type_in IN VARCHAR2 := 'ALL', delimiters_in IN VARCHAR2 := parse.std_delimiters) RETURN INTEGER;
The number_of_atomic function (drawn from the ps_parse package found on the disk) has two additional parameters. The second parameter, count_type_in, determines the type of count to perform on the string. The third parameter, delimiters_in, allows the programmer to specify the characters that act as delimiters in the string. The default values on these parameters not only allow me to call number_of_atomics as simply and easily as number_of_words, but when needed this function can do so much more for me.
Again, you can go too far with a good thing. The human mind seems to be fairly comfortable juggling up to seven items of information at a time. After that, we simply have trouble keeping it all straight. Modules with more than seven or eight parameters are difficult for programmers to use. If you create a module with an enormous parameter list, it will be intimidating and confusing to programmers. Because too much effort will be required to use the module, it is likely to be ignored or used improperly.
If you find that you are passing many attributes of the same entity as individual parameters, you might consider replacing all of those separate parameters with a single record parameter. Suppose you create a module to insert a company into the database. The company table has 25 columns. Are you going to create 25 parameters? Who wants to create all those separate variables and pass them into the module? You could instead create a record based on the table as follows:
DECLARE company_rec company%ROWTYPE; BEGIN ... Fill the columns of the record as needed ... add_company (company_rec); END;
Notice that the add_company module now just takes the record as a parameter, yet you can still pass all the company information you need to the INSERT statement inside add_company.
Global data sure is convenient. You can refer to it anywhere inside your application. You can transfer values from one module to another without having to expand the module's parameter lists. From the standpoint of plug and play, however, global data is a very bad idea.
When a module relies on a global variable, it is dependent on information from outside of that module, which is not passed to the module via the parameter list. As a result, the interface to that module does not completely describe the ways in which a programmer can use the module. In order to use the module, I must make sure that the global variable contains the proper value. If the module changes a global variable or data structure, I must ensure that this change does not affect my program. I cannot, in other words, call the module without worrying about side effects.
If your module relies on global data, it is also much less likely that the module could be reused in an entirely different application. A function that counts the number of atomics in a string could certainly apply to any number of completely distinct systems. If that function refers to a global variable, like a list of delimiters or a maximum size of string, then each system has to have a way to set those values. Who is going to bother with all of that? They'll just write their own version, perhaps making a copy of the original. Yuch!
There are times, however, when global data is both justified and necessary:
Global variables containing system-wide named constants reduce the hardcoding of literal values in your programs.
Global variables offer a way to communicate between application components that might otherwise be mutually deaf and mute.
Global variables can sometimes improve performance over passing parameters, as happens with complex structures like PL/SQL tables.
For these reasons, very few applications succeed in completely resisting the use of global variables. If you are aware of the drawbacks of this kind of data, however, you will at least justify and keep your use of them to a minimum.
Copyright (c) 2000 O'Reilly & Associates. All rights reserved.