I've noticed a certain idea recur in different contexts, but before I start calling it "the sandwich pattern", it would be useful to know (in the spirit of other "Is there a name for this pattern?" questions on this site):
whether it is a recognized concept that already has a better-known name, and
whether there are further examples to illustrate this pattern.
What I'm thinking of as "the sandwich pattern" is abstracted from a couple of examples:
The first is Ned Batchelder's "Pragmatic Unicode" from 2012, where he suggests a "Unicode sandwich": [emphasis added]
the data coming into and going out of your program must be bytes. But you don’t need to deal with bytes on the inside of your program. The best strategy is to decode incoming bytes as soon as possible, producing unicode. You use unicode throughout your program, and then when outputting data, encode it to bytes as late as possible. This creates a Unicode sandwich: bytes on the outside, Unicode on the inside.
The name is repeated at the end, as one of the three main take-aways:
Unicode sandwich: keep all text in your program as Unicode, and convert as close to the edges as possible.
The example that motivates this question. At work, we have a system (program) that (among other things) happens to deal with dates, and a date can differ depending on whether it is in:
the customer's timezone, or
what I'll call the "global timezone", for simplicity. (Don't worry about the fact that different machines can differ in their timezone; it's not relevant here and if you prefer you can think of it as UTC… that's not the point of this question.)
The code can get a bit confusing and error-prone for this reason, converting dates between timezones here and there, but it turns out that much of the business logic of this system is concerned with the customer timezone, so I'm thinking about the following proposal:
Establish the convention (enforced with types if necessary; that's a separate discussion) that the "customer timezone" is used everywhere in the code, i.e. a date means the date in the customer timezone by default, except:
at certain boundaries, where it is necessary to interact with the "global timezone", we immediately convert either from the global timezone to customer timezone (when the date comes from the external system), or from customer timezone to the global timezone (when the date needs to be written out to the external system).
That is, a "sandwich" with global timezone on the outside, and customer timezone on the inside.
Both these examples appear to me to be instances of a single, more general pattern, something like [this is not a quotation, just my attempt to put the pattern in words]:
Sometimes, there can be multiple kinds of the same "thing" (like "bytes" and "unicode" in (1), or "global date" and "customer date" in (2)) that tend to be used in similar contexts / have some semantic overlap.
To avoid error, confusion, and too much back-and-forth conversion between the two kinds of "thing", it helps to have a convention where you:
- identify the preferred kind, and
- design your system such that "things" inside the system are of the preferred kind, and conversion happens as close as possible to the boundaries with whichever external systems require the other kind.
(But maybe the pattern is even more general; see the "Aside" later below.)
This pattern seems a "design pattern" at least in the broader sense of "a general, reusable solution to a commonly occurring problem within a given context" (the Wikipedia lede), even if not in the narrow sense of the original object-oriented design patterns from the "Gang of Four" book (e.g. consider that the book Game Programming Patterns includes "patterns" like "data locality"). But in any case, my question is not about whether this qualifies as a "design pattern", but about the "sandwich" software-engineering idea/principle/pattern itself: specifically, whether there are existing names (and other examples) for it.
Aside: That's the question, but it's possible that the two examples above, and the common pattern I abstracted from them, are not at the right level of generality/abstraction, because perhaps the following examples can also be folded under the same pattern:
Also from 2012 is Gary Bernhardt's "Functional Core, Imperative Shell" idea. It is also mentioned in his "Boundaries" talk (YouTube), and searching this site finds several mentions. Roughly, the idea is to have impure functions only on the "outside" (the "shell") and have "pure" functions in the "core" (more links here).
Not an independent example, but an answer on this site by user Theraot, taking inspiration from "the idea of a pure core and an impure shell", suggests a similar idea for
async
functions:interacting with external systems is often
async
[…] The solutions is that the entry point will be impure (andasync
) it will deal with external systems (impure imperative shell) and call into your notasync
code (pure functional core), which returns to the shell for more interoperability. That way you do not have to make everythingasync
, and you do not have to make your whole code impure.
These examples are different in some ways from the "sandwich pattern" as in the actual question above the line: The conversion only goes one way; there's no straightforward way to convert an impure function to pure, or async function to sync. It's not like there's a risk of accidentally using the "incorrect" kind of function (and we're less worried about the reader getting confused which is which), so the problem being solved is entirely different. But what's similar is the idea of consciously choosing to declare one kind as preferable, and carefully designing such that this kind is used everywhere in the "core" except a "shell" where forced otherwise by external boundaries. A supporting coincidence(?) is that a search just before asking this question led me to Mark Seemann's 2020 post "Impureim sandwich", where (apparently independently) he comes up with the term "sandwich" for this pattern.
(process-datum (thing canonical-form))
that does important stuff to a specific form of information. Then you might have methods(process-datum (thing degenerate-form-1))
,(process-datum (thing degenerate-form-2))
, etc., which convert the degenerate forms into the canonical form before handing them to the mainprocess-datum
method. If these auxiliary methods are defined as:before
the main one, then you might also have corresponding:after
methods to convert back to the original form once processing has concluded.@d
before adef f:
is simply shorthand for replacingf
withg(f)
(see this say), so it can be arbitrary code and there's no special marker or convention for before, after, around etc.