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?