Big title, no? Yes, but it describes—in one sentence—a refactoring method for existing functions and and also a basic pattern to functions design, applying the Open-Close principle (OCP).
The problem states as this: «How to apply the OCP at the most granular level, for example, at functions?». And, in other words, how to open a function for extensions, maintaining its closeness to modifications?». Seems an aim difficult to achieve, because once you define the signature of a function you only can change its behavior. Of course, when we talk about object oriented languages we count on overloading functions that allow us to change parameters and return values to support the new behavior. An also, regarding to design patterns, we have mechanisms as the template method for encapsulating new and future unexpected behavior, postponing the implementation to sub-types. Again, all of them are really good means at our disposal and we use them constantly to support OCP in different ways. But in here, I want to discuss an important alternative sometimes oversight by experienced programmers.
Several weeks ago I jump into the excellent book of one of my programming mentors, Robert C. Martin, «Clean Code». In Chapter 3, specifically, he discuss the good practices to write «clean functions». He promotes that a clean functions should have at most, three parameters or triads. I will not discuss the whole chapter here, but remark that the concept gets deep in my mind. I started to review my concepts and protocols to functions design (GRASP guidelines, e.g.) and my concrete programming practice of defining function’s parameters.
First, let’s look at the two following function signatures:
- GetDiscount (int CustId, byte age, string region, bool isGoldMember,…): decimal
- GetDiscount (ICustomer customer): decimal
Forgetting about the actual implementation of these functions, they seem apparently of the same value. The responsibility for both versions is to evaluate a customer or a set of attributes of a customer and returns a number, indicating the discount. Functions like these are spread everywhere in standard libraries or as methods in classes like Sale, Payment, etc. But, what is not evident is that one of the above options support the Open-Close Principle (OCP) better that the other and, for that single reason—among several others—it should be preferred without second thoughts.
By following Martin’s recommendation, we choose the second function because reduce the amount of passing arguments, but that mere fact hides the most important issue about good function signatures: their capabilities to endure the prove of time. For example, if our application has to support another new criteria to evaluate a discount, the first function should be overloaded with a new version, adding the new parameters and then we must implement that new version for getting a discount amount. But, what if you must change the implementation? If the implementation need to change, the first function fails completely. All clients must re-write the function call to include the new argument.
The second function instead is in a better condition. The application should re-write the new ICustomer interface with the new parameters, and because of the single argument, the client call remains the same. If the interfaces loading is using any mechanism of dynamic-binding by a Reflections library, the task is simplified and truly adaptable to new challenges.
The solution proposed is not only about the convenience of minimal parameters, but about abstractions. Calling abstractions protect clients from variations, hides implementations, low the coupling levels between modules—an application of DIP—and maximize the adaptability of the modules.
Yet another question arise. Respect of the return values, is there any application of the OCP similar of that already discussed for return values? Notice that both methods return a decimal, which is in most languages a float and a primitive type. This concreteness of the return value might be sufficient for most circumstances, but what if we decide to «abstract» a little more the values:
- GetDiscount (ICustomer customer): IDiscount
The above solution is better from the perspective of the OCP. IDiscount might have multiple properties and also methods. One of those properties might be Value of decimal or float type that enclose the original decimal we need to indicate the amount of the discount.
The pattern for the function, the cleaner function, remains as follow:
<FunctionName> ( <Abstract|Interface> <parameterName>): <Abstract|Inteface>
The above lines discuss a simple, but very useful, view about designing and refactoring function signatures. The basic GoF principle of «programming to an interface, not an implementation» applied to functions provides little options than the proposed solution. From the architectural perspective, we should pay attention to the separation of the interface modules from implementation and concrete modules, applying DIP (Dependency-Inversion Principle). That means that we should write the functions at the most abstract level and algorithmically. Thus, the body of the function should show the steps that the component has to take to calculate the discount avoiding concrete types, and also, hiding primitives in abstract types as the return value of the function shows, and instantiated at runtime by a factory design pattern (template methods, e.g).