- A Mantra for Development
- The Pathologies of Code Qualities
- Avoid Over- and Under-Design
- Minimize Complexity and Rework
- Never Make Your Code Worse/Only Degrade Your Code Intentionally
- Keep Your Code Easy to Change, Robust, and Safe to Change
- A Strategy for Writing Modifiable Code in a Non-Object-Oriented or Legacy System
- Summary
A Strategy for Writing Modifiable Code in a Non-Object-Oriented or Legacy System
Many of the approaches we've discussed here are often met with this attitude: "That's a great idea, but I can't do it where I work because I'm using C." A variant of this is "That's a great idea, but I can't do it where I work because there is so much monolithic legacy code that I can't take advantage of object-oriented methods." There are other variants as well, but you get the idea. Although it is true that your existing software and the languages you are using provide certain constraints on what you can do, there are certain approaches you can always take. One of these is to consider the separation of concerns in a different way.
The idea is to separate the code that is particular to the application from the code that defines the application's architecture (or even system architecture).
One can think of a program as essentially an overall flow detailing the steps to be undertaken. For example, a sales-order system can have a variety of actions needed to work:
- Select customer.
- Get customer information.
- Select products to be sold.
- Get prices.
- Apply appropriate discounts.
- Total cost of sales order.
- Specify shipping.
Object orientation attempts to simplify this by creating objects that group responsibilities for the different implementing steps. These objects collaborate with each other and avoid coupling by having well-defined interfaces that hide their implementations. Unfortunately, if you can't (properly) use an object-oriented language, how can you get at least some of the value that comes from separating concerns? One way is to have each method in your code deal with only one of the following:
- The system architecture
- The application architecture
- The implementation of a step
For example, let's say you are writing embedded software that takes its input from a special bus in the form of string from which it extracts required parameters via a specialized method. Applications like this often take the following approach:
public function someAction () { string inputString; inputString= getInputFromBus(); if (getParameter(inputString, PARAM1)> SOMEVALUE) { // bunches of code } else { if (getParameter(inputString, PARAM2)< SOMEOTHERVALUE) { // more bunches of code // ... } else { // even more bunches of code // ... } } }
The problem with this is lack of cohesion. As you try to figure out what the code does, you are also confronted with detailed specifics about how the information is obtained. Although this might be clear to the person who first wrote this, this will be difficult to change in the future (not counting the confusion that happens now). This gets much worse if one never makes the distinction between the system one is embedded in (which is determining the input method) and the logic inside the routine. For example, consider what happens when a different method of getting the string is used as well as a different method of extracting the information. In this case, the parameters are returned in an array:
public function someAction () { string inputString; int values[MAX_VALUES]; if (COMMUNICATION_TYPE== TYPE1) { inputString= getInputFromBus(); } else { values= getValues(); } if ( (COMMUNICATION_TYPE== TYPE1 ? getParameter( inputString, PARAM1) : values[PARAMETER1]) > SOMEVALUE) { // bunches of code } else { if ( COMMUNICATIONS_TYPE== TYPE1 ? getParameter( inputString, PARAM2) : values(PARAMETER2]) < SOMEOTHERVALUE) { // more bunches of code // ... } else { // even more bunches of code // ... } } }
Pretty confusing? Well, have no fears, it'll only get worse. If, instead, we separated the "getting of the values" from the "using of the values," things would be much clearer.
public function someAction () { string inputString; int values[MAX_VALUES]; int value1; int value2; if (COMMUNICATION_TYPE== TYPE1) { inputString= getInputFromBus(); } else { values= getValues(); } value1= (COMMUNICATION_TYPE== TYPE1 ? getParameter( inputString, PARAM1) : values[PARAMETER1]); value2= ( COMMUNICATIONS_TYPE== TYPE1 ? getParameter( inputString, PARAM2) : values(PARAMETER2]); someAction2( value1, value2); } public function someAction2 (int value1, int value2) { if ( value1 > SOMEVALUE) { // bunches of code } else { if ( value2 < SOMEOTHERVALUE) { // more bunches of code // ... } else { // even more bunches of code // ... } } }
You must remember that complexity is usually the result of an increase in the communication between the concepts involved, not the concepts themselves. Therefore, complexity can be lowered by separating different aspects of the code. This does not require object orientation. It simply requires putting things in different methods.