A Taxonomy of Wrappers

Drew Gross
7 min readJul 14, 2020

Programmers write a lot of wrapper functions. Sometimes the wrappers do a lot, and sometimes only a little. Sometimes they do too much. The goal of this post is to come up with a taxonomy of wrapper functions, with examples of each. I’d also like to suggest that if you are writing a function and it fits into multiple of these categories, if might be doing too much, and you should consider breaking it up into multiple wrappers, which can be called in sequence.

I’ve also noticed that term “thin wrapper” gets a lot of use, and different people use it in very different ways. This post also provides some more specific terminology, in the hopes that we can be more precise in our communication.

We’re going to look at various wrappers you might write around the concept of writing a file to disk, sometimes using an abstract file api that can be constructed from a path and then written to, and sometimes using less-abstract OS APIs.

The Validating Wrapper

A Validating Wrapper is one that does some extra computation to check if the function call will be OK before calling the Wrapped Function. For writing a file, a Validating Wrapper might check to see if the filename contains any invalid characters. If it does, the wrapper will do… something. That something is a key part of the design: When implementing a Validating Wrapper, you must decide what to do when the call will not be OK.

One option in many languages, including C++, is to throw an exception:

file safe_create_file(string path) {
for (auto char : path) {
if (char == '/') {
throw invalid_argument("bad path);
}
}
return file(path);
}

This is a popular choice. But there are other options, such as changing the return type:

optional<file> safe_create_file(string path) {
for (auto char : path) {
if (char == '/') {
return nullopt;
}
}
return file(path);
}

The Validating Wrapper need not have the same signature as the Wrapped Function! It might be nice if it did, and sometimes that’s a hard requirement, but if your codebase or language doesn’t use exceptions this may be a good choice for you. There are still more choices though, such as aborting the program:

file safe_create_file(string path) {
for (auto char : path) {
if (char == '/') {
exit();
}
}
return file(path);
}

This may be the preferred choice if the Wrapped Function may cause utter disaster if called with invalid arguments.

There are other choices as well. If the purpose of the Wrapped Function is to perform an action and not return a result, then simply not performing the action is an option. The errno style error reporting common in the C standard library is an option. If those sound like poor options to you, I would mostly agree. I’d also like to call out another “option” that I suggest you don’t consider: Returning a value that is in the domain of the Wrapped Function’s return type, such as file with some default path:

file safe_create_file(string path) {
for (auto char : path) {
if (char == '/') {
return file("");
}
}
return file(path);
}

With this implementation, it’s impossible to tell when you provided bad arguments! This is rarely the right choice. That may seem obvious, but it’s easy to accidentally create this type of wrapper in dynamic languages like Python. Lets briefly consider a safe_list_read function that returns None when the index is out of range:

def safe_list_read(list, index):
if index >= len(list): return None
return list[index]

Don’t forget that None might be in the list, potentially intentionally, or potentially because of a bug, and the caller of safe_list_read can no longer tell if that’s the case.

The Domain Narrowing Wrapper

A Domain Narrowing Wrapper is similar to the Validating Wrapper: It checks if a call would be invalid before making the call. But there is one major difference: A Validating Wrapper checks if the call would be invalid according to the Wrapped Function, and a Domain Narrowing Wrapper checks if the call would be invalid according to some of your own logic unrelated to the Wrapped Function. Let’s look at our file writing example again, and this time let’s consider writing to a file with a max size. The underlying file writing API can accept any size, but you know that your program is probably going to be writing files onto a small disk and more than 100 bytes will cause problems later on. So you write a function like this, and always use it when writing files:

void safe_write(file f, string data) {
if (data.size() > 100) throw length_error("too much data");
f.write(data);
}

This example uses the exception strategy when the call would not be valid, but all of the other strategies from Validating Wrappers also apply.

The Domain Narrowing Wrapper has the significant disadvantage that it requires everyone contributing to your program to know about the problem the wrapper is solving, and that the wrapper is there to solve them. If it makes sense for your program, consider preventing direct use of the Wrapped Function, via removing it entirely, encapsulating it within private members, linter- or language-integrated deprecation notices, or comments around the function.

While I normally recommend not having one function be multiple types of wrappers, mixing Domain Narrowing Wrappers and Validating Wrappers seems fine.

The Complexity Hiding Wrapper

The Complexity Hiding Wrapper calls the Wrapped Function with some fixed settings. Let’s look at the case of writing a file again, but this type using the operating system API instead of a hypothetical abstract API. Operating system file APIs have a lot of complexity. You have to open the file, decide what you mode are opening it with (definitely not read, but should you truncate? Or append?), whether to create the file if it doesn’t already exist, and dozens of more obscure options. All of those options are necessary for some use cases, but maybe for your use case, you just want to write some data to a file. For this you might use the Complexity Hiding Wrapper:

void simple_write(string path, string data) {
int fd = open(path.c_str(), O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
if (fd == -1) {
return;
}
if (write(fd, data.c_str(), data.size() == -1) {
return;
}
}

This has some superficial similarity with the Domain Narrowing Wrapper and Validating Wrapper, but there is a key difference: No additional error reporting, because no errors are possible except the errors possible in the Wrapped Function. In fact, our example function uses exactly the same error reporting mechanism as the Wrapped Function: errno. I recommend avoiding mixing the Complexity Hiding Wrapper with either the Validating or Domain Narrowing Wrapper, as those wrappers require introducing additional error reporting channels, further increasing the complexity of the wrapper.

The Format Changing Wrapper

We’ve discussed error reporting a lot in our various wrapper types, so here is a wrapper that is very useful for errors: The Format Changing Wrapper. Suppose you are working in a codebase where exceptions are the standard error reporting mechanism, and you want to call a file writing function that reports errors via errno. You want the function to use exceptions so that you can use your other exception based code, so you write a Format Changing Wrapper:

void exception_write(file f, string data) {
if (f.write(data)) {
throw runtime_error(errno);
}
}

The Format Changing Wrapper can also be used to change the format or result data instead of errors. If you are reading a file using an API that returns vector<byte> and you really wanted a string, you can change the format of the result.

You can use the Format Changing Wrapper for inputs too. In fact, my example from the Complexity Hiding Wrapper did this: It changed the format of the input path and data from string to char*. This is fine, but must be done with care: If the source format has a wider domain that the destination, what to do with values outside the domain of the destination must be considered. If the source format has a narrower domain than the destination, some potential calls to the Wrapped Function are impossible to make using the wrapper. I recommend only mixing the Format Changing Wrapper with other types of wrappers if the source and destination format are freely convertible between each other with no loss, and even then, proceeding with caution.

The Mutating Wrapper

The Mutating Wrapper is similar to the Format Changing Wrapper, but instead of changing the type of the data, it changes the contents. For file writing API, one reason you might want to do this is to convert tabs to spaces. Here is a Mutating Wrapper that does that:

void write_with_tabs(file f, string data) {
f.write(data.replace("\t", " "));
}

This wrapper type is powerful, but can easily lead to complex code that is hard to reuse when one wrapper does multiple semi-related mutations, or when combined with other wrapper types like the Domain Narrowing Wrapper. With the Mutating Wrapper, it is important to keep your functions small and open the Single Responsibility Principle to keep a tidy codebase.

The Composing Wrapper

This will be the last wrapper type I discuss, but in some ways the most important. Throughout this article, I’ve generally recommended against implementing multiple wrapper types in a single function. But what happens when you really do want a function that writes a file, but converts tabs to spaces, hides the complexities of the operating system API, and reports errors with an exception? What I do recommend is writing one wrapper for each step, and composing them with another wrapper. Putting it all together:

void fancy_write(string path, string data) {
errors_to_exceptions(simple_write(path, tabs_to_spaces(data)));
}

Of course, this often requires careful design of each of the individual wrappers. The ones I used as examples earlier won’t work. But when you do use this strategy, you are rewarded with a codebase full of functions that are small, easy to understand, easy to use and easy to test. What more could you ask for?

Wrapping Up

After reading this article, I hope you can give a name to the various types of wrapper functions you see. You can use this terminology during code review, and you can identify places to clean up your code. Perhaps you will also see some opportunities to refactor some existing complex wrapper functions into smaller, more composable wrapper functions.

--

--