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

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

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

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

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

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

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

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store