With all of the focus on object-oriented languages such as Java, C#, and VB.NET, it might seem odd that we would spend time talking about the lowly function. However, whether you're working in an object-oriented or a traditional procedural language, you still must break the execution of the program into pieces. In an object-oriented language, the breakdown is through objects and methods. In the procedural language, it is into modules and functions or procedures. Although the names change, the concepts don't.
In this article, we'll explore the breakdown of programs into functions, as well as ways that you can try to design your functions so they can be easily understood and reused. Our first stop is to understand how software developers develop software.
Software development is a complex task, one that without special techniques is too complicated for the human mind. It is for that reason that we break down the problem of software development into small chunks. Each of these chunks is something that our human brains can comprehend. In an object-oriented language, we bundle data in the form of properties, and actions in the form of methods, into a chunk called a class. In non-object-oriented languages, we bundle data into structures, and actions into functions or procedures.
By bundling pieces of the software development process into chunks we can use abstraction to ignore the other pieces of the solution. We focus solely on the one chunk while assuming that all the others will work as intended. This is the heart of abstraction. You focus on a small part of the problem—ignoring other parts so the software development problem becomes manageable.
No matter what we call the chunks that we create, we are attempting to develop an organizational structure that will allow the software to be created by our limited human minds. By encapsulating the software development activity into these chunks, we can abstract in our minds the rest of the solution. The result is a problem small enough to be pondered by the human brain.
Every software developer uses abstraction to manage the difficult task of creating software. He or she uses abstraction, either through the use of functions that were previously written, or through the use of application programming interfaces that exist with software packages or in the operating system itself. One of the key differences between good software and poor software is the ability to easily understand what each chunk does and how the chunks fit together.
An extension of the idea of breaking the software into chunks is to develop reusable libraries of software code. These libraries are just reusable chunks of code that are applicable to more than one problem. When designing software, it's important to keep in mind the possible similarities between the problem being solved and other, future, problems. So, one of the objectives of writing functions is to create an organization of functions within the solution or library that is readily understandable and readily adaptable to the next task. This goal must be kept in mind when deciding how to name a function, what parameters it should take, and how it is presented to other developers.
In writing, one of the first things that you learn is that there is always a target audience. Your writing will be much cleaner and more concise when you clearly understand to whom you are speaking. In this article, the target audience is the software developer. That doesn't mean that there will not occasionally be others that read your work; however, it allows you to tailor your message to a certain group.
The same is true of functions. When you create a function, you should have a specific mindset of how the function will be used and by whom. For instance, there are some functions that are designed for internal use only. These functions would be declared as private and therefore not exposed to the general developer running across your code.
Let's say that you created a set of functions that controlled a robotic arm. At the end of each operation, you might need to check whether any of the safety limits for the robotic arm had been exceeded or if the emergency stop button had been pressed on the arm itself. The function that performs this check is a function that is for internal use only. There is no reason why the developer using your functions for the robotic arm should ever need to call the function directly. For this reason, the function would be declared such that the scope is very small. In an object-oriented language, it would be declared as private or protected. In a procedural language, it would be declared as private so that only the procedures in the same module could call it.
On the other hand, the robotic arm operations functions—such as move—may have a primary audience of programmers with little or no knowledge of how exactly a robotic arm works. They may be a programmer who is more familiar with the way that material moves through a warehouse, or what pressures can be applied to the product before damage occurs. They should not need to worry about the internal operations that your functions need to perform to accomplish their goal.
It's critical that, when you are developing a function, you identify the kind of audience that will use the function. This will help you to identify the kinds of work the function should do, the kinds of parameters it should take, and how it should return its results.
The overall goal of the functions that you create, whether for a library or in an application, must be to make it easier for other software developers (including yourself) to solve the needs of the user. This means hiding unnecessary complexity from other developers through choosing parameters, returns, and actions that don't require extra effort for the software developer.
Perhaps the most difficult part of creating functions is determining what the parameters should be. Most developers initially struggle to create a function with as few parameters as possible so that they don't have to remember the parameters when the function is called. While the idea is laudable because it minimizes the chance of a mistake, it also creates functions that are highly specialized to the type of work being done by the program and that do not migrate well between applications.
There are a few basic rules about the kinds of parameters that a reusable routine should have. They should be followed no matter how large or small the application may be:
- Global constants should be used within a function only when the constant will never change, no matter what the use of the function is. The function could use constants such as the acceleration of a falling object on earth or the value of mathematical constant Π (pi). However, it should not use a global constant for sales tax rate, rate of climb, or any other numeric variable that may be constant within the current application but may not be of broader applicability. Those variables should be passed as parameters.
- No global variables should be used. Global variables make it hard to change and control the operation of a function. All variables should be passed in, even if they are global. This applies to the working functions; as discussed in a moment, shim functions can be used to simplify the calling structure.
- Pass parameters that the software developer is likely to have. Often there is a choice in terms of the information that the software developer provides to the function. The developer should emphasize ease of use for the user, rather than ease of use for the function's developer. If the developer is likely to use a standardized unit of measure, make sure that all of the parameters are accepted in that unit of measure.
These rules tend to create functions that take a lot of parameters. Incidentally, you'll notice that most API functions, particularly those in the operating system, contain a large number of parameters. This is not accidental. It is the result of designing functions to meet the needs of the broadest audience.
To address the real issue of limiting the change of error, other functions, called shim functions, may be created. These shim functions take a smaller number of parameters and provide the application defaults back to the master functions. They are not designed so that they can be reused; they are designed specifically to make it easier to call the master functions.
In cases where the language supports overloading, the shim functions are really overloaded versions of the master function. This allows for a simple interface without the problems of managing shim function naming.
Overloading is the process of creating a function with the same name and a set of parameters different than those of a similar function with the same name. In other words, you can create a function called FunctionA that takes a parameter of an integer, and another FunctionA that takes a parameter of a string. The compiler figures out which version of FunctionA needs to be called at compile time based on the parameters that you're passing.
The beauty of function overloading is that you can use it to develop shim functions with the same name as the master functions. You might have one function that takes only one parameter and all of the other parameters are defaulted. That shim function calls the other, master, function with the same name by supplying defaults for the additional parameters that the master function takes.
Overloading is a useful technique to bridge the gap between coding functions to use global variables and migrating to an environment where more parameters are passed.
Sometimes a language doesn't have the ability to overload functions, or you need to create functions that do something very similar to, but not exactly the same as, another function. The difficulty is defining names for the functions that reflect their unique purpose without causing the names to be excessively long.
There are no hard-and-fast rules for naming functions. However, here are some guidelines that you may want to consider when naming your functions:
- Pick your verbs carefully. The differences among display, prepare, process, and present may seem subtle but could be important if you have functions doing similar functions.
- Consider suffixing your low-level functions with "raw." The keyword "raw" will help distinguish the low-level functions from the higher-level functions. It is easily assumed that the higher-level function without the "raw" suffix will call the function with the "raw" suffix.
- Consider a global search and replace when a name is misleading or inaccurate. Although as software developers we have been conditioned against the use of search and replace, it has a purpose. One very valid reason to use a global search and replace of text is to change the name of a function when it is misleading, inaccurate, or imprecise. Sometimes when trying to determine what to name a new function, you run across the situation where the function name already exists. In some cases, this is because the function with the same name is imprecise, or is in some other way not adequate. A global search and replace can solve this problem. Obviously, it would be difficult or impossible to do this if the function is in a shared library or is already used by a large number of other functions.
As with the guidelines for selecting parameters for a function, these are not hard-and-fast rules. They must be tempered with the individual requirements of the project—and how your team works on projects.
The lowly function is a basic foundation for software development whether in an object-oriented language or not. The function is one of the basic ways in which we as humans can break the software development problem into small, meaningful units that we can develop without error.
Critically important to the development of functions is the ability to understand their purpose in terms of allowing us to break the software development problem into smaller, manageable pieces. It is also equally important to consider the kind of developer who might use the functions that we write. We must minimize the amount of complexity that these functions impose upon other developers.
All information that a function needs should be passed in and not taken through global variables. The only way to provide flexibility and portability of functions between projects is to pass parameters. If it is necessary to reduce the number of parameters to eliminate the possibility of entering the parameters incorrectly, a shim function should be used. A shim function is a function that serves no purpose other than calling a master function and providing the correct parameters.
Equally important is choosing a name for the function. Choosing a name that concisely conveys the action of the function is necessary to ensuring that the function architecture can be easily understood.
Good luck on creating your next set of functions.
About the Author
Robert Bogue, MCSE (NT4/W2K), MCSA, A+, Network+, Server+, I-Net+, IT Project+, E-Biz+, CDIA+ has contributed to more than 100 book projects and numerous other publishing projects. He writes on topics from networking and certification to Microsoft applications and business needs. Robert is a strategic consultant for Crowe Chizek in Indianapolis. Some of Robert's more recent books are Mobilize Yourself!: The Microsoft Guide to Mobile Technology, Server+ Training Kit, and MCSA Training Guide (70-218): Managing a Windows 2000 Network. You can reach Robert at Robert.Bogue@CroweChizek.com.