Bad APIs

I write a lot of C++ code at my day job, so often I don’t want to do that for my hobby projects. There’s a lot of complexity in C++, no doubt. However, I think many of troubles of C++ are simply bad API designs.

As just an example, let’s look at the std::map API. I mean, really?

auto nameit = req.parameters.find("name");
if (nameit == req.parameters.end()) {
// parameter not found
}
else {
// yay, get the name out!
auto name = nameit->first;
}
if (req.parameters.count("name") == 1) {
// there is an item here
}
else {
// there is not
}

view raw
stdmap.cpp
hosted with ❤ by GitHub

OK… so there are some things we know about a map, but the most important is that the key values are unique. That is, if “name” exists, there will be one and only one of them.

So what the flip is up with the iterator stuff? And why on earth is there a count() function instead of just a contains() function?

I’m sure there is some explanation for it, but I’m not terribly interested in it. As a consumer of the API, I don’t really care why you make my life more challenging. I want to work with nice, clean APIs.

Swift vs. C++

Part of what make Swift look more appealing is the clean syntax and sometimes better (though, there are many examples where this isn’t the case today) API interfaces. Let’s take a look at a snippet of a Vapor project I’m toying around with:

let drop = Droplet()
drop.get("/v1/api/spells/:name") { req in
let name = try req.parameters.extract("name") as String
return SpellRouter.api(name: name).json
}
drop.get("/spells/:name") { req in
let name = try req.parameters.extract("name") as String
let partial = req.query?["partial"]?.bool ?? false
let res = SpellRouter.get(name: name)
return partial ? res.partialHtml : res.html
}
drop.run()

view raw
vapor.swift
hosted with ❤ by GitHub

That’s some pretty reasonable code. Maybe I’d change a few things, but there’s nothing that makes me go, “what the heck is wrong with you?”

Compare that to what it might look like in straight up C++:

fws::service service;
service.get("/v1/api/spells/:name", [](fws::http_request req) {
auto nameit = req.parameters.find("name");
if (nameit == req.parameters.end()) {
// bad request
return fws::http_response();
}
else {
auto name = nameit->first;
return spells::api(name);
}
});
service.get("/spells/:name", [](fws::http_request req) {
auto nameit = req.parameters.find("name");
auto partialit = req.query.find("partial");
if (nameit == req.parameters.end()) {
// bad request
return fws::http_response();
}
bool partial = false;
if (partialit != req.query.end()) {
auto value = partialit->first;
partial = (value == "true" || value == "yes");
}
return spells::view(nameit->first, partial);
});
service.run();

view raw
server1.cpp
hosted with ❤ by GitHub

I mean… it’s not that terrible, but there is a lot of WTF? in there. The biggest problem is that these APIs cause the code to really switch context from the get() handling to the details of parsing out data. That’s not cool.

Better APIs

We can fix this though and create something that is much more easier to follow and use:

fws::service service;
service.get("/v1/api/spells/:name", [](fws::http_request req) {
auto name = req.parameter("name");
if (!name) return fws::http_response();
return spells::api(*name);
});
service.get("/spells/:name", [](fws::http_request req) {
auto name = req.parameter("name");
auto partial = fws::asbool(req.query("partial"));
if (!name) return fws::http_response();
return spells::view(*name, partial);
});
service.run();

view raw
server2.cpp
hosted with ❤ by GitHub

This would be the same exact number of lines of code as the Swift version if I change the Swift version to not throw on an error trying to get the parameter. But more importantly, we didn’t lose anything in the C++ version. However, we did gain code that is much easier to read and maintain.

Build better APIs, your code and coworkers will thank you for it.

Bad APIs

2 thoughts on “Bad APIs

  1. “let’s look at the std::map API. I mean, really?”

    I agree the API to containers in the C++ STL isn’t the prettiest, but at least I understand the method behind the madness. The designers decided that all containers should have the same API, and this allows for some pretty powerful reuse of algorithms using the “magic” of templates.

    So if you were writing a map on its own, its API probably wouldn’t look like this. But if you had a sizable collection of containers then it makes sense to have the same API on all of them. Whenever I write a specialized container class in C++ (which actually happens a few times a year) I try to make it STL compatible, ugly API and all.

    Like

    1. So this could have been achieved by lifting the type with a conform() function or what not. The STL containers and generic algorithms don’t provide the best performance anyhow, so this lifting should come at little cost.

      By doing this, you could have still achieved an API surface that worked well for the container and a hook into the shared STL algorithms.

      Like

Comments are closed.