5

I'm developing a client library. I'd like to provide both Sync and Async interfaces to endpoints on a server. They would be rather easy to implement as completely separate entities, but I would like to follow DRY principles and other best practices. How could one implement such clients, and are there any established patterns of achieving this behavior? Endpoints need to do three things: pre-process arguments to request attributes, send the request and post-process request contents.

An example of both implementations:

def get_resource(id, parameter):
    """
    Docstring with explanations and parameter descriptions.
    """
    params = pre_process(id, parameter)
    response = sync_request(params)
    return post_process(response)

async def get_resource(id, parameter):
    """
    Docstring.
    """
    params = pre_process(id, parameter)
    response = await async_request(params)
    return post_process(response)

This is of course a simplification. Pre-processing often involves logic which I wouldn't want to duplicate, along with the call signature and docstring. So there could be a number of solutions:

Duplicate everything

I don't consider this a good option. It would require maintaining two pieces of the same logic.

Refactor logic into functions

This would mean that pre_process and post_process would quite literally be functions, but the overall call would be implemented twice in two classes or modules.

Refactor into one function

Suppose we had a way of knowing whether we are in async or sync mode. Synchronous functions can then return "early" to provide async functionality through a truly asynchronous function that duplicates the latter part of the call.

def get_resource(id, parameter):
    """
    Docstring with explanations and parameter descriptions.
    """
    params = pre_process(id, parameter)

    if async_mode:
        return async_get_resource(params)
    else:
        response = sync_request(params)
        return post_process(response)

async def async_get_resource(params):
    response = await async_request(params)
    return post_process(response)

I don't know if it is an advantage that the same function can now be both sync and async. It could even be confusing to use. Depends on the mechanism to provide async_mode I guess.

Refactor into one function with a decorator

This is taking it a bit far, but could work. We can decorate a synchronous function that returns the request parameters. In that decorator we can then decide whether async should be used. If so, return an awaitable, if not, use synchronous calls. A callable function for post-processing the request would be passed into the decorator.

def decorate_call(post_process: callable):
    def decorator(function):
        async def async_send(params):
            response = await async_request(params)
            return post_process(response)

        def wrapper(*args, **kwargs):
            params = function(*args, **kwargs)
            if async_mode:
                return async_send(params)
            else:
                response = sync_request(params)
                return post_process(response)

        return wrapper
    return decorator

@decorate_call(post_process)
def get_resource(id, parameter):
    """
    Docstring with explanations and parameter descriptions.
    """
    return pre_process(id, parameter)

Which of these would be the best solution? Or are there some other, more appropriate methods?

11
  • I would much appreciate a comment on the downvote. Is this question not appropriate for SE? Perhaps it could be posted somewhere else. But it is not really a Code Review question either, definitely not Stack Overflow.
    – Felix
    Commented Jan 18, 2020 at 16:05
  • why do you want to provide a non async wrapper for an async method?
    – Ewan
    Commented Jan 18, 2020 at 16:05
  • @Ewan What do you mean? To my knowledge, async awaitables can be returned from synchronous functions. I simply want to implement both async and sync interfaces with minimal effort and maintenance.
    – Felix
    Commented Jan 18, 2020 at 16:07
  • @Ewan Makes total sense if you want to call the method from a background thread. Synchronous calls are easier to handle.Of course the reason for "why do you want to provide X" is often "because customers demand X".
    – gnasher729
    Commented Jan 18, 2020 at 16:08
  • I mean the underlying function is either async or not, the calling code gets to decide if it wants to block until the result is returned or not. Wrapping an async within your library is just a bit pointless
    – Ewan
    Commented Jan 18, 2020 at 16:12

1 Answer 1

1

Why do we use DRY? Because if we have the same code in two places, in totally unrelated files, and we change one, we will forget to change the other one and may have very hard to find bug.

Your first approach is nice and simple but repeats itself. But the repetition is a very minimal problem, because it is in two places just ten lines apart. If you need to make a change in one place, it is very easy to make it in the other place.

So approach 1 is KISS, and it is DRY enough.

1
  • 2
    Fair enough for that dummy example. But given a more complex client and set of modules it would require the functions or methods to be in similar places. Which is a reasonable way to go. So in your opinion not even the second suggestion of refactoring logic into functions?
    – Felix
    Commented Jan 18, 2020 at 16:10

Not the answer you're looking for? Browse other questions tagged or ask your own question.