Builder pattern question


#1

Hello,

I have a question about whether I’m building an API call in a way that makes sense.

For now, I have something like:

let request = Request::new()
    .command(Command::AgencyList)
    .route("one")
    .create_request()
    .unwrap();

let mut res = request.send().unwrap();

The intermediate type from create_request is:

pub fn create_request(&self) -> Result<ApiCall>

where ApiCall is a simple wrapper around Url which validates that it’s a correct API call.

An alternative would look more like:

let res = Request::new()
    .command(Command::AgencyList)
    .route("one")
    .send()
    .unwrap();

Which has the advantage of being more compact. And perhaps makes more sense? I would perform validation that the API call is correct in .send(). One downside I noticed is that I lost the unit tests I was writing to make sure that the correct Url was being created, since this goes straight to sending an HTTP request.

Any thoughts on which way is better? Or if there’s an even better way? Also, let me know if I wasn’t clear in explaining the question.


Request for Builder macro review / critique
#2

What are users of your API likely to prefer? Is there a case for doing things in between initially creating a request and then sending it? For example, could a user want to ship that request object off to another thread to execute?


#3

I would endorse the second approach, not because it’s more compact per se, but because having fewer public types will reduce cognitive load. As long as the only thing you can meaningfully do with an ApiCall is send it, there’s not much point in having the type. If you expect to have a lot of code that needs to manipulate notional API calls as data, then it would make sense to add the type (but probably worth keeping the ‘simple’ API for ergonomics).


#4

I had not thought of this possibility. But, I think it’s unlikely that a request would be created outside a thread but executed inside it. I’m not sure there’s a case to be made for any other separation either.

This makes a lot of sense to me.

Thanks for the advice!


#5

I have a follow-up question!

As I fiddle around with the user API, I realize that the most important thing is to return a struct that deserializes the XML response. (and if I want to build something higher level, it will be easier to work with these structs than trying to parse the API responses ad-hoc). So, each command e.g.

AgencyList
RouteList
RouteConfig
...

would be an individual API call, and would have its own struct deserializing the response.

I would like to use the builder pattern for each command, because some of the commands have multiple options. However, I’d also like to “hide” the builder. So Hyper, for example, has the Client struct which has a method get() which returns a RequestBuilder which, when completed, returns Response.

My thought is to have a Command struct, which would have methods like agency_list() and route_list, each of which would return their own type of builder, which would finally return the structs containing the parsed response (AgencyList and RouteList).

An example:

let agencies = Command::new().agency_list().get().unwrap(); // returns AgencyList
let predictions = Command::new() // returns PredictionsForMultiStops
    .predictions_for_multi_stops()
    .agency("MBTA")
    .route("1")
    .stop("354")
    .stop("253")
    .get()
    .unwrap();

I think the Command struct would just be empty (in contrast to hyper’s Client, which contains some config). But it makes sense to me as an entry point. Does this make sense to others? Is there a better way to handle multiple builders?


#6

If by “hiding” you mean that users do not instantiate the builder directly, I am in favour of it.

This has some disadvantages and advantages, though.

The disadvantage would be the hardcoding (?) in your Command.

The advantage would be the unified struct which also makes it possible for others to extend the Command with traits of their own in a seperate crate. To encourage users to do this, if this is even planned for your application, you can use traits for the Commands abilities from your library.

This would also enable some sort of only bringing into scope what the user needs, both preventing errors, but also forcing the user to use the traits, which can be a hassle, unless they have their own module.