Accidental Reuse

Posted On: 2019-03-18

By Mark

When programming, I occasionally encounter a situation where an existing piece of code solves a new problem. I recently had one such situation occur: I was working on developing a system to navigate a menu and make selections, and I discovered the code I was using for the dialogue system could be used for presenting and navigating the menu as well. On the surface, this may sound like a good thing, after all, reusing code means getting results faster and having less code to maintain. This kind of reuse (I call it "accidental reuse", since it was not intended when designing the original code,) unfortunately, can be harmful if used incorrectly: I have worked on several projects in the past that started off as accidental reuse, but as the functionality drifted apart, the complexity and risk of maintaining the code increased dramatically. I'll go into detail regarding how this happens, as well as a few tips on how to approach accidental reuse to improve the chances of making maintainable code.

Example Scenario

Before diving into an explanation about how accidental reuse can go awry, I will provide a hypothetical example that can be used in all further discussion on this topic. Imagine you are programming a button, and the button needs to change from blue to red after a certain amount of time passes. It just so happens that elsewhere in the same application, you find that there is code to turn a piece of text from blue to red. The requirements look the same, change from blue to red, so this seems like you should be able to reuse the existing code. You may have to make a few minor changes (change the method to public and parameterize the object being modified) but it's a only a few minutes of work and your code works great.

How Things Can Go Wrong

For the first example of how this can go wrong, imagine that you get feedback that the color change on the button is too sudden. The text changing color at that rate still looks fine, so only the button should be slowed down. Since the code is shared, the quickest fix is to add a new parameter, perhaps a speed multiplier, that can allow changing the speed of the transition. As you continue to get feedback and changes (the button colors should be less saturated, the text alone shouldn't have a drop-shadow, but the button text should, etc.) you keep modifying this one method, each change in reaction to the feedback. With each change, you have to test both the color-changing text and the color-changing button, since each modification introduces risk on code shared by both of them. Later, after many changes, you discover a new field (let's say it's a text-box) that also needs to change from red to blue. As you approach reusing the same color-changing code, you discover that you need to specify eight different parameters, each one representing a variation on the original implementation's design, as well as add several else if statements to account for features that behave differently between all three instances (like the text changing its drop-shadow, the button not changing, and the text-box not having any drop-shadow to begin with.) Then, as new changes for the text-box flood in (change the size when the color changes, hide the cursor after it completes changing, etc.) you find it harder and harder to change the method, as the code is becoming an increasingly tangled mess of unrelated features. At this point, it takes longer (and introduces more risk) than if you had three completely separate implementations. In this example, the primary cause of issues was a lack of intention: since the reuse was entirely incidental, it was unclear which functionality belonged in the shared code, so it quickly became overwhelmed with unrelated features. Unfortunately, establishing intention can also come with its own pitfalls, as we'll see in the second example.

For the second example, imagine that, instead of making the minimal amount of changes to the shared code, you painstakingly separated each possible permutation of how the code could behave, to maximize the reuse of this particular piece of code. You expect that the particular objects, the speed of the transition, the colors involved, and any possible "juice" effects (like bounce or wiggle animations) are all possible permutations on how this code works, so you add them upfront as parameters on the method, and implement them inside the method body. Doing all of that may take a long time, and result in a much larger method, but it leaves you with confidence that any feature changes can be easily accommodated by a simple parameter change. Unfortunately, after taking the time to implement all of that, you discover that the button is being cut: it wasn't adding enough value to justify it's cost. Now you have a method with dozens of parameters that is only ever called in one place, to do one thing (plus you have low morale, as you feel all that work was wasted.) The pain of this particular example does not end there, however. Future maintainers of the code dread feature changes to the color-changing text, as it requires modifying an incomprehensibly complex code, and any changes to it frequently causes bugs or undesirable side-effects. In this example, we saw how fully descibing all the possible reuse cases can be harmful, both because it takes a long time, and also because it can leave you with code that is far more complex than is actually required.

For the third, and final example, imagine that you instead chose to copy-paste the code. Sure it's not really reuse, but the similarity is tenuous enough that any kind of formal reuse doesn't seem like a good fit. You merrily go along, able to quickly react to feedback and feature requests, as you don't have the burden of designing or testing for any other use-case. Unfortunately, there were bugs in the original color-changing text that were discovered and resolved late (such as the colors flickering for users with a certain model of computer), and, since you copied the code, you also have those bugs. Usually you remember to implement it in both places, but over time, the differences between the two make it increasingly difficult to do so. Eventually, you are faced with a new problem: now you need to implement a color-changing text box. You could try to reuse the code, or could copy-paste existing code. Either way, however, you have two very different examples of color-changing code, and picking which one should be used here is no simple task. This example shows the dangers of avoiding reuse altogether: code duplication leads to multiple places to fix bugs (which gets progressively more difficult as differences pile up over time). When you (inevitably) run into another situation that could benefit from shared code, having two competing methods can make it difficult to determine which one is the "right" way to approach that problem.

Lessons Learned

The lessons from each of these examples can be distilled into some (slightly simplistic) guidance:

  1. Code should be shared intentionally. Even if the similarity is accidental, it is important to design for a specific case of reuse. For every change, you should be able to answer whether or not a particular change is relevant to the intent of the reuse. Otherwise, incidental similarities and differences will mar the code, making future reuse progressively harder over time.
  2. Like any form of reuse, it is important to limit the scope of how much variation should be supported in early implementations. Although change is generally cheaper earlier in development, trying to accommodate all possible change up-front can lead to lengthy development and unneeded complexity.
  3. Choosing to defer the decision of reuse to a later time can be beneficial, but it comes with extra maintenance costs (like any code duplication) and future decisions about sharing code will have more implementations to keep in mind (which is a double-edged sword, as it can provide more context about what is and is not in common between two use-cases, but it can just as easily contain unintended differences, which may be mistaken for intentional.)
Generally, I've heard that the third example is often used as a proactive way to pursue the rule of three, which claims that code should be refactored once you have three instances of the same thing. This can be viable (and may even be optimal), provided that you can handle the unintentional differences that inevitably emerge from two (or more) implementations. For myself, I tend to prefer designing for reuse immediately upon encountering the first instance of reuse, but I do so by approaching it like a prototype, or minimum viable product. Two instances is too few to understand what will be in common between three (or more) instances, but I find that attempting an implementation of intentional shared code can provide insights that you might not get from looking at two independent, non-shared implementations.

Lessons in Practice

To provide a real-world example of how to use this guidance, I will go back to the example I provided in my opening paragraph (potential reuse between dialogue system and menu system) and describe how I used the guidance provided above to decide how to approach it. The first thing I did when I encountered this potential to share code, is I slowed down and asked "why?" I specifically asked myself "what is it about these two systems that makes them have seem to have something in common?" After some thought, I realized that the same visual model (branching tree) can be used to represent the structure of a menu and the structure of a non-linear dialogue. Furthermore, both systems prominently feature user choice, as this defines which paths in the tree are being followed. So, from an intentional standpoint, I knew I could ask the question "is this related to navigating a tree of choices?" in order to determine whether or not a particular feature belonged.

Knowing that this was the first attempt at shared code, I didn't set out to create something from scratch. Instead, I aimed to make minimally invasive changes to the existing code (in the dialogue system) in order to separate aspects that were definitely not about navigating a tree of choices. Fortunately, this was relatively low cost - largely thanks to how I'd broken up the original implementation: any feature that was more than a few lines of code was turned into a separate component, and the engine simply delegated to those components to perform their various tasks. Using a stripped-down version of the engine for the Menu system was primarily just making new implementations for the parts that needed to change (the UI and command processing) and removing components that were not needed (controlling the lighting.) Of course, some changes were required, but I tried to implement these as code cleanup, that way it would be useful even if I didn't keep this shared code approach (for example, the lighting component was previously referenced in multiple places, but only actually used in one, so I consolidated that to a single place that only called the lighting if an implementation was provided.)

After working with the chosen abstraction, I discovered that the UI navigation could provide value as well. Specifically, moving the cursor and picking a choice was not trivial to implement, so sharing it between the menu and the dialogue would be helpful. As such, I included that exact same component both in the dialogue and the menu. Fortunately, again, prior design worked in my favor: I had implemented the dialogue to support any UI (by setting any RectTransform as the current choice's cursor) so I was able to swap out the cursor used in the dialogue for the cursor used in the menu. It is worth mentioning, if my design hadn't worked out quite so well, I was prepared to copy-paste the necessary code, as the choice component was not composed of any further components, and it would not be worthwhile to completely rework it's design just for this first instance of reuse.

All told, implementing the menu system to share components with the dialogue system went quite smoothly. Although I did end up duplicating some code (mostly in the variable management component) overall I was able to reuse most of the code at low cost. While it may seem that I was fortunate that my design (dialogue system composed of components that could be swapped out) happened to work well for this particular case of reuse, I personally think that any well-designed system would have some element of modularity, and a prototype of a new reuse situation would benefit from using that existing modularity where it makes sense, and skipping it whenever it doesn't. Finally, like with any prototype, this approach is intended to set me up to succeed in the future, rather than the present, since, when I run into a third situation that involves users navigating a tree of choices, I should have a solid understanding of what worked and what didn't in the current reuse prototype.

Conclusion

In summary, accidental reuse can be of value, if you give it the time and attention that any form of reuse deserves. Approaching it slowly does not, however, necessarily mean that it will take a long time. The first iteration shouldn't be perfect, rather, it (in my opinion) should be focused on being good in ways that provide value for the next iteration, without sacrificing the current health of the project. I hope this exploration of accidental reuse has been helpful to you. As always, if you have any thoughts or feedback, please let me know.