Reputation: 45
After searching for a while I did not find any answers to this question so here I go.
My general question is: Should interface methods return values produced by callbacks that are passed to them as parameters?
To demonstrate what I mean, I will give an example (the language in use is C# and the problem-domain is web but I think the question applies to many languages and domains).
Lets say you have a class that handles Http requests. It can be configured to accept parameters, http method and url and returns the response. The way I see it this method could either:
A) Provide a set of standard return types (maybe an error/success model and have a generic overload)
B) Accept a callback that gets the HttpResponse as parameter and lets the caller return whatever data he needs from it.
I realize that method A follows best practice principles but I think method B is applicable as well in some circumstances (for example you have a Services module that uses this Http handler class internally)
So my domain specific question is: Is it bad practice to have an interface method return a value produced by a callback in this specific situation?
To demonstrate my example with code:
public interface IWebRequestWrapper
{
T DoRequest<T>(params... , Func<HttpResponse,T> callback);
}
public class WebRequestWrapper : IWebRequestWrapper
{
public T DoRequest<T>(params... , Func<HttpResponse,T> callback)
{
using (var response = httpClient.GetResponse(params))
{
return callback(response);
}
}
}
public class Client
{
public SomeModel MakeServiceRequestX()
{
return iWebRequestWrapper.DoRequest(params, (i) => //construct your model through i and return it);
}
}
Upvotes: 1
Views: 390
Reputation: 10401
The question basically boils to
HttpResponse
HttpResponse
If we assume that we need to create many WebApiClients like
public class FooApiClient: IFooApiClient
{
private readonly IWebRequestWrapper _webRequestWrapper;
public FooApiClient(IWebRequestWrapper webRequestWrapper) =>
_webRequestWrapper = webRequestWrapper ?? throw new ArgumentNullException();
public FooBaseData GetBaseData(string id) =>
_webRequestWrapper.DoRequest(id, response => new FooBaseData...);
public FooAdvancedData GetAdvancedData(string id) =>
_webRequestWrapper.DoRequest(id, response => new FooAdvancedData...);
}
Though unless we really need some custom HttpResponseProcessing, I'd rather expected that API to have swagger to allow those API clients to be generated automatically.
and our HttpResponse processing logic is at least slightly non-trivial (I mean not just get Json, parse if HttpStatusCode.Success/throw if anything else)
or if we have some generic(defined at run-time) endpoints
var result = _webRequestWrapper.DoRequest(
TextBox1.Text,
response => Deserializer.Deserializer(GetType(ComboxBox1.Type), ...)
then it is a pretty reasonable design
Basically IEnumerable.Select for WebAPI calls.
Then what are the disadvantages?
using (...HttpClient.Get
, any abstraction makes things more difficult to spot.
As the first comment probably meant by its vehemence is that callbacks lead to callback hell. And that's true - those should be avoided at any cost.
But in this case it is a synchronous callback, not the dreaded asynchronous one from JavaScript.
Unless you nest another callbacks inside it, it is not much different from the regular Enumerable.Select
that can be misused just as well.
As the second question is a non-issue, we are left the first one.
So if
Then it probably makes little sense to have such interface. Strongly typed API clients are the way to go (especially if they can be just generated with NSwagStudio or the likes).
But if none of those points apply (or apply very weakly), then such abstraction is a perfectly viable solution.
As always there are no absolute answers - everything depends on the exact situation.
I meant cases when
our HttpResponse processing logic is at least slightly non-trivial (I mean not just get Json, parse if HttpStatusCode.Success/throw if anything else)
For example,
DoRequest("PeculiarApi", response =>
{
if ((response.StatusCode == HttpStatusCode.Success) ||
(response.StatusCode == HttpStatusCode.Conflict))
{
if (response.Headers.TryGet(Headers.ContentType, out var contentType) &&
(contentType == ContentType.ApplicationJson))
{
return JsonConvert.Deserialize<TModel>(response.Content);
}
throw new Exception("No content type");
}
if (response.StatusCode == (HttpStatusCode)599) // Yes, that's what they return
{
return XmlConvert.Deserialize<T599Model>(response.Content).Errors[2];
}
...
}
is a piece of non-trivial logic.
This logic can be easily tested through mocked IWebRequestWrapper
.
Can it be tested with regular HttpClient? Yes, but that will require some boilerplate that is not needed with IWebRequestWrapper
.
By itself it is not worth much (not a deal-breaker), but if we already have to use IWebRequestWrapper
it is at least a minor simplification.
Upvotes: 1