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
Make Service objects plain ES classes #600
Comments
|
And if they're going to be plain classes, I wonder if we need anything other than some utils to register and inject these classes correctly. E.g. |
|
Service-specific core code is pretty thin already, but if Ember can go in this direction, I'd want to make it an optional feature, similar to #587. |
|
I am very much in favor of this! It'd be a great step into the direction of being able to fully pull out business logic into pure vanilla TypeScript / JavaScript packages.
AFAIK it does not. However, many core service implementations, like the I think the ability to apply mixins in the first place, is also only possible with
The only thing that's needed in a class to be instantiable by the resolver / container / DI system is a static Then there's also the "destroy" lifecycle hooks interface that needs (?) to be implemented, but #580 would nicely replace that with a utility functions based approach (like |
|
I did a quick Ember Twiddle to implement a plain import { inject as service } from '@ember/service';
export default class FooService {
@service bar;
constructor(injections) {
Object.assign(this, injections);
}
static create(injections) {
return new this(injections);
}
}
|
|
Just thinking, we probably don't want to have to explain to users the mechanics of how to make their service work with Ember. Here are some approaches we can take:
There may be other approaches I haven't thought of. |
|
The lightest-weight move is to have a trivial base class and have the constructor receive (Adding magic into the system is effectively going backwards here. What we want is something that is pleasant to use but also sufficiently explicit that it's easy for users to understand when they want to, and which retains the benefits of static analyzability.) We could basically implement the lightweight base class approach by making something very close to @buschtoens's be the base class (it could probably be tweaked slightly to match the One of the upsides to the approach @buschtoens outlined, too, is that users who want to build their own base class or just implement it directly per-service can: as long as it matches a well-defined interface, you're off to the races.
|
|
@chriskrycho I was enumerating options, not weighing in. I think your idea is a good one. The slight downside is that there will be duplicated logic between |
|
Yep, I was just enumerating the actual trade offs around the options you enumerated! As regards a base class, I don’t think that level of duplicated functionality is something to worry about. The specifics are different (including arguments vs. not) and an overemphasis on eliminating every point of repetition of any sort is its own problem. The key for this kind of design is to provide good primitives (a simple and clear way to hook into the DI system), and then a nice abstraction on top of that (e.g. thin base classes which get rid of boilerplate), while maximizing flexibility for each type as appropriate. Only share what is actually shared—which in this case is the primitives, not the (base) classes built with those primitives. I’m mulling on some delegate-based and manual-registration based approaches here as well, will share if I land on anything I think is actually good. |
|
I'm super much in favor of going into this direction! Apparently, I think @mehulkar is right in saying it has some connection to the
@buschtoens I think I have to disappoint you. The inheritance chain is: I think many addons and still many code relies on On the other hand if the new base class is |
@chriskrycho lighter still is to have a constructor interface and not even consume the user's one base class for services. |
|
@gossi Evented gets mixed into RouterService further down the page, so I think @buschtoens may be right that it’s not a default. https://github.com/emberjs/ember.js/blob/23d3ff0436e03dff172efa3360ad09cbec98f80f/packages/%40ember/-internals/routing/lib/services/router.ts#L410 @chriskrycho im not convinced a base class is needed. The thing that provides the owner to the constructor is whatever resolved the class and understands that it’s a Service class. Which is really a proxy for saying “this should be injectable and this should be a singleton”. I don’t know what the right abstraction is here, but I keep coming back to the idea of explicit registrations and injections. That wouldn’t quite work out of box for lazy instantiations, as services are, but an app boot hook that includes some lines of code that do these registrations could easily adapted to do the same thing for ES classes that don’t extend from anything. |
|
FWIW, I prefer base classes. That way I can open a file and see what it is supposed to be, rather than relying on file location to determine type. Additionally I would think a base class would be better for third party tools inspecting files to then know what they're exporting. |
|
I think there are two things we can do to approach this problem in parallel:
Doing 1 will set us up for the long term, and 2 will help us migrate in the short term. The |
@pzuraq I would rather have a different base class. Optional features are already making things more difficult for addon authors that can't know if they are turned on or off (there is no public api, and even if one is added they have to test for both scenarios). |
|
@Gaurav0 we've discussed this a bit on the core team, and there isn't consensus on that point yet. Either we would want a new base class, or we would want to remove base classes altogether (via the DI manager). I believe the lean was toward removing the base class altogether last time we discussed it. The DI manager would unblock both options, and would allow addon authors to use a stable solution for their services that won't change with the optional feature flag for As an aside, I think this points to us needing better solutions for addon authors and optional features. |
|
For anyone that wants to experiment with this today, this is a workaround (which is what I do in ember-swappable-service): export default class Service {
static isServiceFactory = true;
static create(props) {
new this(getOwner(props));
}
constructor(owner) {
setOwner(this, owner);
}
}
It probably has some crossover with other framework objects like routes, so given this workaround exists, we probably want to consider those questions together with the routing update to ensure we have a consistent "factory" protocol rather than just standardizing around what we have now. |
|
@chancancode what's the path forward to RFC for this? |
|
An example from the Java -- gasp -- world is the springframework. You created a plain object (very similar to JS class) and declarativative marked the properties you needed injected. Injection can be performed via constructor-injection or property injection (e.g., If the latter, there is a lifecycle associated with the service because you can't rely on anything to exist until the container has finished injecting the properties. This could be called After properties are set and lifecycle is set on the service the object is usuable. In general, the basic features of the springframework are required by any DI system. There's really not a reason to "innovate" in this space. There are other examples of great DI systems that can be studied and emulated. At a high level, users want:
$0.02 |
The main purpose of services is to be an injectable singleton. It does not seem to be gaining much by extending from
FrameworkObjectwhich inherits from CoreObject. The main benefit of this hierarchy is the Observable mixin, which provides things likethis.getand friends. I think somewhere,Servicealso mixes inEvented, but I couldn't find it.The text was updated successfully, but these errors were encountered: