square / retrofit

A type-safe HTTP client for Android and the JVM
https://square.github.io/retrofit/
Apache License 2.0
43.16k stars 7.31k forks source link

Base url required on builder. Should be in constructor of builder. #1194

Closed clockworkant closed 9 years ago

clockworkant commented 9 years ago

Looking at the Retrofit Builder it has a mandatory field of BaseUrl. With this in mind shouldn't the BaseUrl be part of the constructor or the Builder class?

JakeWharton commented 9 years ago

I have two arguments against this. Neither are particularly strong, but they're based on history of working on Retrofit (and other libs) and the fact that counterarguments were never stronger (or just were never made).

Like I said I'm not married to the current choice, but I at least have some reason for preferring it even if they are relatively weak compared to other API decisions in the library. I know @f2prateek is on board with changing the API, and I had given similar arguments when he sent a PR for it.

I'll leave the burden of deciding on whoever wants to chime in here.

artem-zinnatullin commented 9 years ago

Another option — add separate Builder class with only one method public CompleteBuilder baseUrl(String baseUrl) so it won't be possible to forget baseUrl and call build(). This option allows you keep almost all pros of regular Builder pattern.

We use this in StorIO (yeah, yeah) for all buildable things, also for those that needs several required params. As a result — nobody of users that I know about had any problems with understanding what things are required and what are not. We even have it in our features list: "Convenient builders with compile-time guarantees for required params." because it's very easy to misuse API for DB.

Though I'd say that new Retrofit.Builder()....build() is not a very frequent piece of code for a regular app. So it may not worth to do anything to guarantee that all required params are set at compile time. Regular app will crash pretty early with this problem (it's not another DB query in some "hidden" part of the app).

JakeWharton commented 9 years ago

Some libraries base their entire API on that pattern (like jOOQ). It definitely feels far too much machinery for such a low-leverage part of the API, and one that is always exercised like you said.

On Thu, Oct 15, 2015 at 9:43 PM Artem Zinnatullin notifications@github.com wrote:

Another option — add separate Builder class with only one method public CompleteBuilder baseUrl(String baseUrl) so it won't be possible to forget baseUrl and call build(). This option allows you keep almost all pros of regular Builder pattern.

We use this in StorIO (yeah, yeah) for all buildable things, also for those that needs several required params. As a result — nobody of users that I know about had any problems with understanding what things are required and what are not. We even have it in our features list: "Convenient builders with compile-time guarantees for required params." because it's very easy to misuse API for DB.

Though I'd say that new Retrofit.Builder()....build() is not a very frequent piece of code for a regular app. So it may not worth to do anything to guarantee that all required params are set at compile time. Regular app will crash pretty early with this problem (it's not another DB query in some "hidden" part of the app).

— Reply to this email directly or view it on GitHub https://github.com/square/retrofit/issues/1194#issuecomment-148572053.

clockworkant commented 9 years ago

@JakeWharton :

  1. I agree the base url requirement is unlikely to change. I would argue that if a user changes between versions and the builder has a new signature which breaks at compile time this is a good thing. I would rather break at compile time than break at runtime when the builder does a null check and throws because a method has not been called on it such as .baseUrl()
  2. I hadn't considered that the documentation in the editor would only display a String as the parameter. I don't think this is a huge issue as developer will need to read the documentation either way. Alternatively this could be avoided by strongly typing the input to the builder but its verbose to make a class to encapsulate a string. new Retrofit.Builder(new BaseUrl("...")) but it does have merit.

@artem-zinnatullin Having a CompleteBuilder is an interesting pattern however I am unsure of the need to have both a Builder and a CompleteBuilder. What circumstances would you choose to use the Builder over the CompleteBuilder?

Overall I still feel having the url passed into the constructor of the builder is the best option. Alternatively passing in a typed string like BaseUrl would also be a second best.

artem-zinnatullin commented 9 years ago

@clockworkant

Having a CompleteBuilder is an interesting pattern however I am unsure of the need to have both a Builder and a CompleteBuilder. What circumstances would you choose to use the Builder over the CompleteBuilder?

You didn't get the idea :)

public static class Builder {

  public CompleteBuilder baseUrl(String baseUrl) {
    return new Builder(baseUrl);
  }

  // it does not have build() method
  // the only methods that it may have — overloads of baseUrl()
}

public static class CompleteBuilder {

  private final String baseUrl;

  // Notice that constructor of CompleteBuilder is not accessible for the user
  CompleteBuilder(String baseUrl) {
    this.baseUrl = baseUrl;
  }

  // other builder methods: addConverterFactory(), etc

  public Retrofit build() {
    return Retrofit(baseUrl, ...);
  }
}

So the usage pattern is to create Builder and then work with CompleteBuilder (it's transparent for the user until he/she wants to store reference to the builder)

new Retrofit.Builder()
  .baseUrl("http://somehost/")
  .addConverterFactory(MoshiConverterFactory.create())
  ....
  .build();

You just can't call build() without setting baseUrl().

@clockworkant constructor and factory method are bad choice in next case: imagine baseUrl becomes non-required and instead of baseUrl scheme (http, https, etc) becomes required. With constructor or factory method, you will have the same signature of the method method(String) so it will be impossible to prevent misusing of the API at compile time.

Another problem is that baseUrl can be passed as String, BaseUrl, HttpUrl — 3 overloads of constructor or factory method. If somewhere in future Retrofit will require another mandatory parameter — constructors and factory methods will become a mess of different combinations of the parameters. Builder pattern solves this problem by design.

But again, for a regular app new Retrofit.Builder() usually needed once, and error caused by incorrect usage of the Builder will be detected early.

JakeWharton commented 9 years ago

I would argue that if a user changes between versions and the builder has a new signature which breaks at compile time this is a good thing.

That would require a major version bump. There's a difference between behavior change and API change, and we don't break APIs between non-major releases for binary compatibility reasons.

f2prateek commented 9 years ago

I'd actually be against this change now, since (correct me if I'm wrong) Retrofit doesn't ship a default converter anymore, so technically, a converter is required.

f2prateek commented 9 years ago

Actually I take that back, I realized you can use RequestBody without a converter.

JakeWharton commented 9 years ago

In practice once is usually required, but technically one is not. You can use RequestBody and ResponseBody types.

JakeWharton commented 9 years ago

I still think your argument holds some merit. If we want to get technical, we could omit a base URL and require absolute URLs on every method annotation.

hosamaly commented 9 years ago

I've found the simple, parameterless constructor useful in allowing me to have a factory method that creates a builder with some default settings, and then handing it over (or back) to another method that would set the more specific arguments, such as the base URL.

JakeWharton commented 9 years ago

We're going to leave this as-is for 2.0.