It is common wisdom that opposites attract. In programming, however, it is desirable to keep things that are related together — that’s at least what the “Principle of Proximity” states.
This principle has many manifestations, some of which are well known by most software developers, for instance:
-Keep the documentation (comments) as close as possible to the code;
-Initialize variables as close as possible to the point where you use them;
-Limit the scope of declarations (i. e. use namespaces and don’t make constants public if private is sufficient);
As opposed to opposites, related things not always attract, or — as a matter of fact — attract in a suboptimal way.
Here is an example. Assume that you have to process a list of different objects (let’s call them “boxes”, for the sake of this example) that you have just received, maybe over a socket connection. This list always consists of a blue box, a red box, and a green box, exactly in that order. These boxes are encrypted and protected by an integrity checksum. Before actually processing them, you need to perform decryption and integrity checking. (Also assume that the boxes are completely different. They have different content, different security mechanisms, and require different processing.) Below is one way to go about it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void onReceiveBoxes1(void* boxes) throw(BoxSecurityException) { // Get pointers to boxes. blueBox_t* blueBox = (blueBox_t*)boxes; redBox_t* redBox = (redBox_t*)(blueBox + 1); greenBox_t* greenBox = (greenBox_t*)(redBox + 1); // Check box integrity and decrypt box content. applySecurityToBlueBox(blueBox); applySecurityToRedBox(redBox); applySecurityToGreenBox(greenBox); // Process the actual boxes. processBlueBox(blueBox); processRedBox(redBox); processGreenBox(greenBox); } |
At first glance, this code doesn’t look bad at all. It is grouped in such a way that the three steps are clearly visible: 1. get a box; 2. apply security to box; 3. process box. If you zoom out a little, the structure looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
operation 1 object a object b object c operation 2 object a object b object c operation 3 object a object b object c |
Is this the principle of proximity in action? Are related things close together?
Not really. The things that are close together are the objects under each operation, but the objects themselves have little in common. Contrast this with this approach:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void onReceiveBoxes2(void* boxes) throw(BoxSecurityException) { // Handle blue box. blueBox_t* blueBox = (blueBox_t*)boxes; applySecurityToBlueBox(blueBox); processBlueBox(blueBox); // Handle red box. redBox_t* redBox = (redBox_t*)(blueBox + 1); applySecurityToRedBox(redBox); processRedBox(redBox); // Handle green box. greenBox_t* greenBox = (greenBox_t*)(redBox + 1); applySecurityToGreenBox(greenBox); processGreenBox(greenBox); } |
The structure is now inverted:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
object a operation 1 operation 2 operation 3 object b operation 1 operation 2 operation 3 object c operation 1 operation 2 operation 3 |
The objects and their operations are close together; in fact, they are completely encapsulated. I like to call this ‘encapsulation at runtime’, which is not to be confused with traditional object-oriented encapsulation where you put data and its related operations close together at coding time, in a class. (Which is another instance of the principle of proximity, BTW.)
What I don’t like about onReceiveBoxes1 is that it mixes up things that are unrelated: order of boxes and order of box actions. Just because the boxes are ordered in a particular way, doesn’t mean that we have to perform the box actions in that particular box-order. Unnecessary dependencies are usually bad for maintenance.
Ah, maintainability, that’s where the second implementation really shines! If you have to add a yellow box someday, you just copy and paste the block of an existing box and do some minor modifications. And if the order in which boxes arrive changes, adapting onReceiveBoxes2 is likewise trivial. Better maintainability means that the risk of introducing an error is much lower, which in turn means that you spend less time debugging and have more time for doing code katas.
Honoring the principle of proximity almost always gives you better efficiency, either. Notice that in the first implementation, the pointers to all boxes have a fairly long lifetime and must be kept in memory (or CPU registers) as they are needed until operation 3 has finished. onReceiveBoxes2 only needs a pointer to the box that is currently worked on, which means that the compiler only needs to allocate one pointer.