-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Singleton Pattern Considered Harmful #119
Labels
Comments
Thanks for this detailed outline @ryneeverett! Will need some time to reflect on the suggestion and its nuances, as well as the impact on the team here at ITHAKA who is likely the most extensive user base. |
daneah
added
✨ enhancement
New feature or request
❓ question
Further information is requested
labels
Oct 13, 2022
ryneeverett
added a commit
to ryneeverett/apiron
that referenced
this issue
Jan 26, 2023
This introduces an option to enable instantiated services while maintaining backwards compatibility with the singleton pattern. Resolve ithaka#119. While I think the singleton pattern should be deprecated, put under a feature flag, and discouraged, the first step is probably to give instantiated services some field experience as an optional feature.
11 tasks
ryneeverett
added a commit
to ryneeverett/apiron
that referenced
this issue
Jan 26, 2023
This introduces an option to enable instantiated services while maintaining backwards compatibility with the singleton pattern. It also allows passing caller arguments into the `Endpoint` declaration. Resolve ithaka#119. While I think the singleton pattern should be deprecated, put under a feature flag, and discouraged, the first step is probably to give instantiated services some field experience as an optional feature.
ryneeverett
added a commit
to ryneeverett/apiron
that referenced
this issue
Jan 26, 2023
This introduces an option to enable instantiated services while maintaining backwards compatibility with the singleton pattern. It also allows passing caller arguments into the `Endpoint` declaration. Resolve ithaka#119. While I think the singleton pattern should be deprecated, put under a feature flag, and discouraged, the first step is probably to give instantiated services some field experience as an optional feature.
ryneeverett
added a commit
to ryneeverett/apiron
that referenced
this issue
Jan 27, 2023
This introduces an option to enable instantiated services while maintaining backwards compatibility with the singleton pattern. It also allows passing caller arguments into the `Endpoint` declaration. Resolve ithaka#119. While I think the singleton pattern should be deprecated, put under a feature flag, and discouraged, the first step is probably to give instantiated services some field experience as an optional feature.
ryneeverett
added a commit
to ryneeverett/apiron
that referenced
this issue
Jan 27, 2023
This introduces an option to enable instantiated services while maintaining backwards compatibility with the singleton pattern. It also allows passing caller arguments into the `Endpoint` declaration. Resolve ithaka#119. While I think the singleton pattern should be deprecated, put under a feature flag, and discouraged, the first step is probably to give instantiated services some field experience as an optional feature.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
Apologies for ignoring the issue templates but this would fall more closely into a [question] or [design] category.
To me, the primary appeal of declarative clients can be summed up as encapsulation of the logic of interacting with endpoints and improved ergonomics in terms of readability and making it easy to implement correctly while making it difficult to make errors.
My thesis is that when apiron is pushed beyond simple use cases, these twin virtues are often threatened by apiron's use of the singleton pattern.
If this is correct, I would further propose that this limitation is unnecessary, could be addressed with fairly minimal changes, and could likely be eliminated without breaking backwards compatibility.
However, first I believe I should try to establish that there is room for improvement by going through a few use cases. All code examples I will be discussing can be found on my repo https://github.com/ryneeverett/quasi-apiron, which contains a series of example modules implementing the same functionality using both
apiron
andquasi_apiron
. Thequasi_apiron.py
module does not attempt to replicate all apiron functionality but implements the bare minimum to illustrate what a non-singleton design might look like. All the modules in that repo should run successfully with the exception of the*_example_multiple_caller.py
modules which reference fictional infrastructure in order to illustrate a point.Singleton Encourages Global Variables
I believe a common use case would be to pass arguments like
auth
,session
, andheaders
to any and all endpoints of a given service. (Side note: I believe most of thecall
arguments are undocumented at the moment.) Inapiron_example_caller_endpoint_args.py
we have an example of how one might do this:You could imagine us reusing this
SESSION
variable in subsequent requests and the global variable wouldn't be a big deal. However, it becomes cumbersome if you imagine this in the context of a program that, say, defines the service in one module and calls it from several other modules. These modules would have to also import theSESSION
variable and now we're dragging this throughout our program when it really just wants to be owned by this one class.In
quasi_example_caller_endpoint_args.py
we have an example of what this api could look like with an instantiatedService
:Alternatives: We could eliminate the need for global variables by allowing callers to overwrite a class attribute to pass in default arguments so, for example, you could write
GitHub.endpoint_kwargs = {'session': SESSION}
.Endpoint Customization is Unintuitive and Cumbersome
The most obvious way to write an endpoint that wraps the default functionality would be to subclass
Endpoint
and write a__call__
method that callssuper()
, but not so fast!You really have to dig into the code to figure out what's going on before you can do something like we find in
apiron_example_pagination.py
:Contrast this with
quasi_example_pagination.py
, in which we can use__call__
and can just do what we want without wrapping it in a function:Alternative: Maybe we could simulate this behavior by having the caller call the
__call__
method of subclasses?Multiple Callers must Reset Class State
This is the only case in which the examples don't actually run and just serve as an illustration. It may not be the strongest point, but I believe it's the only one with no viable alternatives.
I would suggest that a valid use case would be using a single declarative client to access multiple services and have tried to illustrate what this might look like in a realistic scenario in
apiron_example_multiple_callers.py
:Because the service is a singleton, the caller must reset the domain before every request. And because our user may want to support multithreading, they need to take extra measures to make sure another thread doesn't change the domain before the request. With instantiated services, the author of
quasi_example_multiple_callers.py
doesn't need to worry about such things:Summary
Thanks for your consideration. Comments, critiques, and corrections welcome.
The text was updated successfully, but these errors were encountered: