Wrap or include? Thoughts about a common structural issue in code and templates

We know very well that any code duplication is a bad thing, but sometimes it can be unclear what to do with the repeated content. If function or method or template 'A' contains something that is also in function / method / template 'B', should we move the common content into a sub-template that is called by both A and B, or should we create a common parent that contains the duplicated code, and calls A or B based, for example, on a parameter? In this post, I'll investigate the consequences of each option. For the sake of brevity, I will only talk about templates, but the arguments here equally apply to functions and methods.

This dilemma may be familiar to anyone who has dealt with web templates. Basically, the question is this: if we have, say, different web pages, should all of these pages call another template to include the common header and footer (multi-entry-point model), or should there be a single wrapper template that outputs the header, the footer, and calls other individual templates to include the appropriate content (single-entry-point model)?

The first figure shows the multi-entry-point case, where the common content is in a sub-template (circle with lines) called by two top-level templates (other two circles). The decision (represented by the triangle) which one of these to call is made beforehand, based on some information (represented by the blue arrow). Both top-level templates contain a call (the small circles) to the sub-template that, in this example, contains the header and the footer (the grey lines).



We see that although the common content is only present once, the way it is called, its interface, determines the code at multiple points. Now if this interface is to change (which can happen quite often with templates: a new parameter may be introduced to control something in the header, for example), one would either need to modify the code everywhere the common template is called, or be forced to implement all changes in a backward-compatible manner. Based on personal experience, this can cause serious issues in templating.

The second figure shows the single-entry-point case, where the common code is called first; in this example, in the form of a wrapper template. The decision which second-level template to call is made inside this wrapper. The piece of information this decision is based on (blue arrow) needs to be passed to the wrapper. Here, there is only one point where something is called, which should solve the issue above.



One can argue that the interface is a two-way thing: if it changes, one needs to change not only the code that calls a template, but the called template as well. That is, in this latter model, if the signature of the second-level templates change, one still needs to do the same amount of work, as all of them must be modified to respect the new interface.

In real life, however, the second arrangement may still be simpler to maintain. The reason I see for this is that it is easier to disregard a parameter than to include one. In the single-entry-point model, if one of the second-level templates needs a new parameter, it can be added centrally, in the wrapper, and the rest of the templates will almost automatically ignore it (for example, if named arguments are used). However, in the multi-entry-point model, if the wrapper needs a new parameter, all top-level templates may need to be changed to pass it on. (One can, naturally, find workarounds to this problem, like passing arbitrary data structures or objects instead of parameters. Still, this issue seems to raise its head often enough on various levels.)

Notice however, that the single-entry-point model above also suffers from its own issues. If applied to templates, the current arrangement puts logic (control) as well as content (view) into the wrapper, which may be acceptable in certain templating frameworks, but less so in a strict MVC approach. Also, the decision-making process in the wrapper may get too complicated if it is difficult to figure out what content we should actually return. This, in turn, might signal that some refactoring is needed in the system.

All in all, both arrangements have their own advantages and disadvantages. Keeping in mind some of the considerations above can help to choose the best option in each individual case.

Popular Posts