Tailor Standard Containers to Your Needs

We often use standard containers as members of our classes. In a lot of cases, the semantics we actually need is not equivalent to the semantics the containers provide.

A few days ago in the #learn channel on the Cpplang Slack, someone posted a question about a piece of code that looked similar to this:

void appendToOrder(std::string orderId, OrderData data) {
  auto pos = allOrders.find(orderId);
  if (pos == std::end(allOrders)) {
    throw std::runtime_error("Order does not exist!");
  }
  Order& existingOrder = pos->second;

  existingOrder.appendData(data);
  //some more business logic with the existingOrder...
}

Here, allOrders is some kind of map; for simplicity let’s assume a std::map<std::string, Order>.

First things first

There are several similar issues in this snippet I’d like to point out: We’re dealing with the primitive obsession smell in several places. The orderId should not be a std::string. I am pretty sure that an empty string is not a valid orderId. Neither should be ##%?$ – regardless of how you encode the Unicode character. Use A strong type, with an appropriate name. For example, OrderId.

Something similar goes for the runtime_error – there are so many of those and probably we will want to treat errors like this one differently than e.g. range errors, regex errors or filesystem errors. My suggestion: create a new exception class for this kind of error, probably derived from std::logic_error rather than std::runtime_error.

Dealing with the containers

The first five lines of the function above look like they are doing something completely different than the last lines. The function does some business logic: appending some data to an order. Those first lines, however, are mere logistics – finding the order we want to work on. Both are different concerns, and at a different level of abstraction, too.

Therefore, we should move the container access to its own function:

std::map<OrderId, Order> allOrders; //somewhere...

Order& findExistingOrder(OrderId orderId) {
  auto pos = allOrders.find(orderId);
  if (pos == std::end(allOrders)) {
    throw OrderNotFoundException{orderId};
  }
  return pos->second;
}

void appendToOrder(OrderId orderId, OrderData data) {
  Order& existingOrder = findExistingOrder(orderId);
  existingOrder.appendData(data);
  //some more business logic with the existingOrder...
}

Now, business logic and infrastructure logic are cleanly separated. We also should consider that if we need that kind of lookup logic once, we’ll probably need it again. Of course, we should not generalize prematurely, but in this case, it’s just a byproduct of the separation of concerns that now we have this logic in a handy, reusable function.

Thinking further, where does this new function go? It clearly does not belong into the class that deals with the business logic. We’d need an infrastructure class that bundles all the special logic on how to access Orders. There probably also will be some logic on how to add new Orders and remove existing ones from the pool. While we’re at it, the data this logic works on would belong to that class as well.

In Domain Driven Design, this kind of class is called a Repository. They act like standard containers that provide the access functions needed by the business classes, but usually not with the full standard interface. Usually, there also is some kind of persistence mechanism as a backend, e.g. storing the Orders in a database. Here’s the next step of our refactoring:

class OrderRepository {
  std::map<OrderId, Order> allOrders;
public:
  Order& findExistingOrder(OrderId orderId) {
    auto pos = allOrders.find(orderId);
    if (pos == std::end(allOrders)) {
      throw OrderNotFoundException{orderId};
    }
    return pos->second;
  }

  //... more logic
};

OrderRepository orderRepository; //somewhere...

void appendToOrder(OrderId orderId, OrderData data) {
  Order& existingOrder = orderRepository.findExistingOrder(orderId);
  existingOrder.appendData(data);
  //some more business logic with the existingOrder...
}

Conclusion:

Using plain standard containers can be a lack of strong typing as well. We can, however, use them as an implementation detail for our own container classes that are specialized to our needs.

Previous Post
Next Post

4 Comments


  1. I think in this scenario it makes sense to use a set rather than a map as the OrderId should be integral part of the OrderData type. Then supply a special compare function to the set that compares on OrderId values.

    Reply

    1. That would depend largely on the use cases. Using the map makes the lookup a little bit easier and might well be a good option if the additional memory needed is of no concern. Other options include a multi index container like boost provides, or simply a sorted vector, if the cache locality is needed (probably not).

      Reply

    1. I agree that it probably is not the right error handling strategy in this case. I left it in because the original snippet had it and I concentrated on the other aspects of that snippet in this post.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *