Question

For code maintainability I understand that it is generally better to have code structured like this:

void abc() {
    a();
    b();
    c();
    d();
}

Instead of like:

void abcd() {
    a();
    bcd();
}
void bcd() {
    b();
    cd();
}
void cd() {
    c();
    d();
}

As it adheres to the single responsibility principle and generally makes things more readable. However when I am trying to using asynchronous functions which call some handler when it completes (more specifically netcode with boost::asio) so I am forced to have it look closer to the latter example.

void abcd() {
    a();
    // async_read(file, handler)
    async_read("foo.txt", bcd);
}
void bcd() {
    b();
    async_write("bar.txt", cd);
}
void cd() {
    c();
    async_write("baz.txt", d);
}

Basically is there a better way to format async code to have it follow a pattern closer to the first example?

Was it helpful?

Solution

You want to apply the clean code principle, that a function should do one thing only and do it well. In both snippets, a(), b(), c() and d() comply with these principles.

Clean code also has a single level of abstraction principle that tells that the code of a function work at the same level of abstraction. So the first snippet is ok only if a(), b(), c() and d() are all at the same level of abstraction. Depending on their levels of abstraction, the second snippet could be a valid or even better alternative.

But these principles are all about readability and maintainability and they should not be used dogmatically:

  • If abcd(), bcd(), and cd() are used for asynchronous "scheduling", then they could be considered to do one thing: they split the sequence of tasks according to asynchronous requirements.
  • If you don't like it, because it distracts from the real structure, you could envisage a lambda as read or write handler (see this SO question about requirements). But it will not necessarily improve the readability and maintainability, especially with nested lambdas, even if it has the advantage to regroup in one place the gluing.
  • If you do not have any statement following your async_read() or async_write(), this code is equivalent to waiting the read/write to be finished before to continue. You may then as well use asio's read() and write(), with the benefit of increased readability, unless you have other asynchronous or threading constraints.
  • If you have a lot of function decompositions like that, you could consider to implement a pipeline object, that processes a sequence of "commands". You may then have a helper class that is used as read/write handler and that keeps track the pipeline object and its current step (i.e.somewhat like a pipeline iterator), to launch in due time the asynchronous operations continuing at the following steps. It's a first idea and would need to be designed more in detail. However, this would be a complete restructuring of your code and might not necessary make it easier to understand.

So keep it as simple as possible but not more ;-)

Terminological remark: the Single Responsibility Principle (SRP) is about classes, and more precisely about reasons to change (as explained by its inventor). For the functions, it's do-one-thing-and-do-it-well. We intuitively feel that there is something in common between the two concept but it's nevertheless different ideas and it might confuse to use the same term (unfortunately wikipedia promotes this confusion).

Licensed under: CC-BY-SA with attribution
scroll top