1

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);
   }
}
SecretAgentMan
  • 2,856
  • 7
  • 21
  • 41
  • 3
    i wouldn't use callbacks. just do `async-await` – Daniel A. White Nov 25 '19 at 19:00
  • 1
    @DanielA.White Well my methods are already async, I just didn't add it for brevity, but the question is more about should IWebRequestWrapper provide a standard set of return types or should it let the caller (Client) decide what it wants to get. If I return the HttpResponseMessage as it is I will force the caller to wrap it around a using statement and I think that adds too much bloat in the code. – Erotokritos Vallianatos Nov 25 '19 at 19:08
  • 1
    Minor phrasing nitpick - you do not return a callback, you return a value produced by applying that callback. – Eugene Podskal Nov 25 '19 at 19:11
  • @EugenePodskal Yep you are right, I will edit the question – Erotokritos Vallianatos Nov 25 '19 at 19:14
  • @ΕρωτόκριτοςΒαλλιανάτος Forcing the use of another (usually anonymous) function is really just a different kind of bloat... – Gabriel Luci Nov 25 '19 at 20:03
  • 1
    If you aren't expecting this to be used for file downloads, you could read the body of the response in `DoRequest` and return a `string`. That would solve the concern around disposing of the response object. – Gabriel Luci Nov 25 '19 at 20:05
  • @GabrielLuci I did think of that but I have some calls that want specific headers and some calls that don't. So I thought I can have a WithHeaders response and one without but then I will get more than I need from the response (getting all of the headers when I only need 1 or 2) – Erotokritos Vallianatos Nov 25 '19 at 20:40

1 Answers1

1

The question basically boils to

  1. Should any consumer of this interface need such degree of freedom as to be able to produce any value from HttpResponse
  2. Will it lead to easy-to-maintain-design, or will it be a proverbial callback hell

Should any consumer of that interface need such degree of freedom as to be able to produce any value from 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

  1. It is composable (you can easily reuse it anywhere)
  2. It makes consumer classes testable (that non-trivial HttpResponse processing logic that produces your actual domain objects)
    • Well, not with the real HttpResponse but I assume that it was not meant literally
    • In the worst case we will just have use some one-to-one wrapper MyHttpResponse that can be easily created/mocked.
  3. It is concise and easy to use

Basically IEnumerable.Select for WebAPI calls.

Then what are the disadvantages?

  1. Well, it is yet another abstraction layer. You gain some composability and testability, but also have to maintain one more class and interface. Overall not a bad deal, but still, unless you really need/will use that composability and testability, its a price paid for little-to-nothing.
  2. It protects against someone forgetting to dispose WebResponse, but it doesn't protect the same person from forgetting that it will be disposed, so response should not/cannot be cached and value cannot be lazy initialized. Though you may say that it is obvious and the problem stands basically the same for inlined using (...HttpClient.Get, any abstraction makes things more difficult to spot.
    • Again, if we really want to avoid such risks, we can use some wrapper class that is created as a full snapshot of the received HttpResponse. Though it will lead to a small (though usually irrelevant) performance loss.

Will it lead to easy-to-maintain-design, or will it be a proverbial callback hell

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.


Overall

As the second question is a non-issue, we are left the first one.

So if

  1. There is very little logic in callbacks (no need to test it)
  2. And/Or there are very few distinct callbacks
  3. And/Or callbacks are often reused (code duplication)

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.

about the testable part I really thought it gets harder to test this way instead of returning a specific dto

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.

Eugene Podskal
  • 10,270
  • 5
  • 31
  • 53
  • Thanks a lot for your reply. For your first point, the abstraction layer already is there, the question is should it have a solid/concrete data model that returns or should the caller handle what he needs. For your second point, that is the purpose of this class, holding all the bloating of making a web request in it. About the callback hell, I didn't intend to nest callbacks inside there, just get what I need from the response (body parsed as json, some headers maybe etc). Last, about the testable part I really thought it gets harder to test this way instead of returning a specific dto. – Erotokritos Vallianatos Nov 25 '19 at 20:47
  • @ΕρωτόκριτοςΒαλλιανάτος I have addressed some of of the question in the last edit. – Eugene Podskal Nov 25 '19 at 21:47
  • Thank you for the extra explanation, I understand your reasoning and I think that is a valid approach. – Erotokritos Vallianatos Nov 26 '19 at 10:02