Closed ash2k closed 2 years ago
/hold
Gotta say, I don't love this pattern. Why would we want to use this over just an Options struct?
Down sides:
Gotta say, I don't love this pattern. Why would we want to use this over just an Options struct?
Down sides:
- It makes it more difficult to see which options are available when using an IDE, which currently shows me all the fields in the Option struct.
- It means that anyone can implement a new option struct that satisfies the interface, but Run will just ignore it, right?
- Feels less intuitive and more "magic" you need to know to use it.
According to Dave Cheney when discussing the Config struct pattern:
It has trouble with defaults, especially if the zero value has a well understood meaning.
Here's the full Functional Options
post: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
I have an alternate suggestion that i think would solve both the defaults issues and still make the API discoverable:
opts.WithX(...)
pattern so they can be chained.This has the advantages:
Example usage:
applier.Run(ctx, inv, objs, apply.DefaultOptions().
WithServerSideOptions(r.serverSideOptions).
WithReconcileTimeout(r.reconcileTimeout).
WithDryRunStrategy(common.DryRunNone))
@karlkfi Thanks for your feedback!
It makes it more difficult to see which options are available when using an IDE, which currently shows me all the fields in the Option struct.
I partially agree with this. In practice if we move all/most of the stuff, that shouldn't be public away from the apply
package, it will almost exclusively contain relevant options (and the constructor).
It means that anyone can implement a new option struct that satisfies the interface, but Run will just ignore it, right?
Not really. You cannot implement a function to implement the type because it takes a parameter of a private type - see https://github.com/kubernetes-sigs/cli-utils/pull/454/files#diff-44c477dc998e3b6be3729c3697d977a86de5fcd680dd8cee6a181e2d3f94ce9cR35 and https://github.com/kubernetes-sigs/cli-utils/pull/454/files#diff-ff0604c07cd58dd82246f24045636317c9ce34bb516d74c599511042f5e8313bR60.
Overall, this is a well known and widely used pattern in Go. For example, grpc-go uses it. As a user of that library, I think it's quite good in terms of UX.
Feels less intuitive and more "magic" you need to know to use it.
It depends if you have used it before. As I said above, it's a widely known and used pattern in Go. If you have worked with it before, you'll feel right at home.
I have an alternate suggestion
To me as a user that reminds Java's builders :) I think this is fine, but less commonly used in Go.
defaults are always set, don't need to match the zero value in Go, and don't always need to be specified by the user.
Same as with functional options.
all possible options are discoverable using IDE auto-complete on the options struct.
This is good, but with func. options it's package name (e.g. apply
) vs the private struct. Autocomplete works on both, but with functional options you get a bit of unrelated stuff too (depends on the IDE, I guess).
I think both approaches are better than what we have now. As I user, I personally would prefer functional options because it's more idiomatic in Go, but it's just me. 🤷
How is the functional options pattern more idiomatic than the builder pattern?
I don't really buy that Functional Options is idiomatic just because Dave Cheney made a blog about it. And just because Builder pattern is popular in Java doesn't mean it's bad or not idiomatic Go. These seem like spurious arguments.
Consistency with other big projects is mildly compelling, but would be more compelling if it were something more closely related, like K8s itself. It's still sort of an appeal to authority tho.
I can sort of get on board with functional options being better than the existing options struct, but I see it more as a tradeoff than a solid win. And I'm not convinced it's better than the build pattern.
That said, none of this seems like a huge priority. Even if it's an improvement, it's still an API change that's going to require refactoring all the existing clients, without any obvious return on investment for existing users.
How is the functional options pattern more idiomatic than the builder pattern?
I don't really buy that Functional Options is idiomatic just because Dave Cheney made a blog about it. And just because Builder pattern is popular in Java doesn't mean it's bad or not idiomatic Go. These seem like spurious arguments.
Consistency with other big projects is mildly compelling, but would be more compelling if it were something more closely related, like K8s itself. It's still sort of an appeal to authority tho.
I can sort of get on board with functional options being better than the existing options struct, but I see it more as a tradeoff than a solid win. And I'm not convinced it's better than the build pattern.
That said, none of this seems like a huge priority. Even if it's an improvement, it's still an API change that's going to require refactoring all the existing clients, without any obvious return on investment for existing users.
Either builder, functional options, options struct, or a hybrid, we are going to simplify the API. The return on investment for the two current clients are that they will have a stable API. The two current existing clients are going to have to change no matter what, because we are certainly going to simplify the API to make it more accessible to other clients. The API is not even alpha
yet. It is much better to change it now that there are only two known clients, instead of trying to update it later when there may be more. Specifically, the simplest default API will not assume a custom inventory object. In fact, the simplest case will not activate pruning functionality at all. The simplest case will be to merely activate the wrapped kubectl apply
functionality--no pruning, depends on, or apply-time mutation. And the API will support this as the default. The SIG CLI refactored dependencies literally for years by moving the code base into staging
, which finally allows kubectl
code reuse. And this open source effort in a SIG CLI repository is one of the first known reuses of kubectl apply
. The open source community deserves a simple, accessible API to this library, and it will get it.
[APPROVALNOTIFIER] This PR is NOT APPROVED
This pull-request has been approved by: ash2k
To complete the pull request process, please assign liujingfang1 after the PR has been reviewed.
You can assign the PR to them by writing /assign @liujingfang1
in a comment when ready.
The full list of commands accepted by this bot can be found here.
[APPROVALNOTIFIER] This PR is NOT APPROVED
This pull-request has been approved by: ash2k
To complete the pull request process, please assign liujingfang1 after the PR has been reviewed.
You can assign the PR to them by writing /assign @liujingfang1
in a comment when ready.
The full list of commands accepted by this bot can be found here.
/kind cleanup
I've opened https://github.com/kubernetes-sigs/cli-utils/pull/485 with an alternative approach with the builder pattern.
@ash2k: PR needs rebase.
This is a draft for API refactoring using the functional options pattern. The purpose of this PR is to start the conversation on how this can look.
It's easier to look at with the "hide whitespace" diff option enabled.
I suggest to look at this PR in the following order:
NewApplier()
. See how the new constructor looks.Run()
. See how the new signature looks.TODO:
NewDestroyer()
to use functional options (in a separate PR).Applier.Run()
to use functional options. This is the most interesting part. Ran out of time today.WDYT @karlkfi @mortent @seans3