-
-
Notifications
You must be signed in to change notification settings - Fork 407
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
Native Class Roadmap #338
Native Class Roadmap #338
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
- Start Date: 2018-06-14 | ||
- RFC PR: (leave this empty) | ||
- Ember Issue: (leave this empty) | ||
|
||
# Native Class Roadmap | ||
|
||
## Summary | ||
|
||
Now that we’ve (almost) achieved feature parity for using native classes to | ||
extend from EmberObject, the next step for Ember is to move toward using and | ||
recommending native classes by default, with the long term goal of deprecating | ||
EmberObject and removing it from the framework. This RFC seeks to outline where | ||
we want to go (e.g. what Ember looks like without EmberObject), constraints we | ||
face, and potential paths forward. | ||
|
||
## Design Constraints | ||
|
||
In order to figure out the path forward to native classes, we need to first | ||
develop an idea of what the future of Ember looks like once we have fully | ||
adopted them and removed EmberObject entirely. Below are several design | ||
considerations for the new object model: | ||
|
||
- **It should leverage the platform.** We should use and encourage standard | ||
Javascript classes and idioms wherever possible. This means leveraging classes, | ||
class fields, and decorators for most features that the object model provides. | ||
- **Ember should remain as neutral as possible about the implementation details.** | ||
Part of the reason we are going to have migration pains as a community moving | ||
forward is that Ember is tied to the behavior of EmberObject in a myriad of | ||
small ways. In an ideal world, these details would be opaque to Ember, allowing | ||
users to implement and change-out object models at their own convenience. This | ||
would have made the transition much easier, and will provide flexibility in the | ||
community for pursuing new language features and libraries (e.g. mixins) | ||
- **It should be possible to adopt incrementally.** This will be one of the largest | ||
changes the community has ever gone through, on the same level if not larger than | ||
dropping Views and adopting Components. EmberObject is pervasive - it's been | ||
used for every construct imaginable in Ember apps and addons due to its | ||
versatility and simplicity. Ideally users will be able to make incremental | ||
changes toward pure native classes by first rewriting their existing classes | ||
using native class syntax (as is already possible) and then dropping EmberObject | ||
altogether. | ||
- **We should consider the long tail of adoption, particularly for addons.** There | ||
will be a long period where the community will need to support older versions of | ||
Ember at the same time as newer versions. Ideally it should be possible to | ||
support both the new and old model at once without separate code paths, or the | ||
overlap period should be long enough that addons can drop EmberObject without | ||
breaking Ember versions that are still part of their support matrix. | ||
- **If possible, conversion should be automate-able.** This is not a hard | ||
constraint, since it limits us to having very similar behavior to the existing | ||
object model. However, if it is possible to create scripts that speed up | ||
adoption it will help the community tremendously, so if we are at an impasse on | ||
particular behaviors or implementations, we should consider whether one would be | ||
easier to automate towards. | ||
|
||
## Detailed design | ||
|
||
As mentioned above, one of the main reasons this conversion is going to be a | ||
difficult and long process is the fact that both user code and Ember code are | ||
very intertwined with the existing object model. Specifically: | ||
|
||
- They rely on the behavior of static methods such as `create`, `extend`, and | ||
`destroy`. | ||
- They rely on the behavior of EmberObject's constructor | ||
- They rely on a non-trivial amount of functionality provided via inheritance | ||
- They rely on features of EmberObject's inheritance model that do not exist in | ||
native Javascript (Mixins, concatenated/merged properties, nuances of observers | ||
and listeners, etc.) | ||
|
||
The fact that these are so intertwined makes it very difficult (if not | ||
impossible) for us to rewrite the core classes that exist in Ember without | ||
breaking user land code. | ||
|
||
One strategy we can implement to prevent this from happening again in the future | ||
is to make the details of classes opaque to Ember. Rather than providing base | ||
classes that can be extended by users to add functionality and implement their | ||
applications, we can rewrite Ember to treat user classes as delegates. In other | ||
words, Ember will make no assumptions about the behavior of a class, other than | ||
that it fulfills a certain interface. | ||
|
||
For instance, a Component can be any class which receives an `args` hash and | ||
optionally implements any of the component lifecycle hooks: | ||
|
||
```js | ||
export default class FooComponent { | ||
didInsertElement() { | ||
if (this.args.isShowing) { | ||
// do something | ||
} | ||
} | ||
} | ||
``` | ||
|
||
This method allows users to provide base-less classes, classes which do not | ||
extend anything, to Ember as well. Ember could continue to provide empty base | ||
classes to extend from, but it wouldn't have to. | ||
|
||
Following a strategy of delegates and interfaces would disentangle Ember from | ||
the details of the object model as much as possible. Ember would not care about | ||
the implementation of a class, it would simply assume that it operates by the | ||
same rules as standard Javascript classes do. | ||
|
||
This method would also allow us to extract CoreObject from Ember and provide it | ||
as a separate package for users who want to continue using the legacy object | ||
model. So long as their objects match the new interfaces provided by Ember, it | ||
shouldn't matter what class system they are using. | ||
|
||
> Note: Just because we could continue to provide CoreObject as a separate | ||
package doesn't mean we _should_. The point is that this strategy makes us | ||
flexible enough to support it, if we choose to. | ||
|
||
### Implementation and Adoption | ||
|
||
The largest concerns for adopting the delegate pattern are: | ||
|
||
1. Implementing the delegate pattern before removing EmberObject, so app | ||
developers can switch seamlessly. | ||
2. Giving a large enough buffer to addon authors so that they can switch to the | ||
delegate pattern without dropping support for Ember versions that are still in | ||
common use. | ||
|
||
We can solve #1 during the Ember v3 lifecycle by changing Ember's code | ||
internally to treat the existing base classes and native classes as delegates. | ||
However, we cannot count on being able to remove EmberObject for v4, since | ||
it's likely that there will still be users of Ember versions that do not support | ||
delegates. So, the overall roadmap would look like: | ||
|
||
- Ember v3: Implement delegates in Ember, allowing users to adopt the new world. | ||
- Ember v4: Deprecate EmberObject, and prepare the framework for removal. | ||
- Ember v5: Remove EmberObject. | ||
|
||
## How we teach this | ||
|
||
Once implemented, we can begin providing examples of creating base-less classes | ||
for all of Ember's standard constructs. We should update the guides to recommend | ||
this method, make announcments, and move toward officially deprecating | ||
EmberObject. | ||
|
||
We should also write migration guides that help users through the process of | ||
converting to native classes. In particular, non-Ember constructs that use | ||
EmberObject will likely have to change a fair amount. Demonstrating various | ||
conversions in blog posts and guides will help to show people the path forward, | ||
and spread knowledge of best practices when using native classes. | ||
|
||
## Drawbacks | ||
|
||
This approach limits what we can do significantly in framework code (no | ||
inheritance, can't provide defaults, must provide all behavior external to user | ||
code). There are many established software patterns other than inheritance for | ||
providing most behavior, so in practice this shouldn't be an issue. | ||
|
||
## Alternatives | ||
|
||
- **Gradually rewrite current base classes.** We could attempt to gradually rewrite | ||
Ember constructs such as Services, Routes, and Components in place. This would | ||
give us more flexibility for a lot of things, like constructor behavior and | ||
default method implementations, etc. It would probably take about as long to do | ||
this as the delegate pattern, and could get very messy along the way, especially | ||
for removing certain constructs (like Mixins). | ||
- **Provide new base classes at the same time as the old ones.** We could provide | ||
new base classes, exposed at different import paths, that users could gradually | ||
switch to over time. This would give us more flexibility as well, but it would | ||
also be about as much work and the same adoption timeline as the delegate | ||
pattern, since Ember would have to support both the old Ember object model and | ||
the new base classes at the same time until the switch. It would also likely be | ||
fairly confusing to users, since they would have to make sure they were | ||
importing the correct classes. | ||
|
||
## Unresolved questions | ||
|
||
If we go this direction, it means we cannot specify any base class behavior. One | ||
issue that arises is the fact that dependency injections will not be available | ||
to the class _during_ object construction. They can be assigned _after_ only. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this result affect initializers/instance-initializers? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initializers and instance-initializers are (as I understand them) essentially pure functions that run at the beginning off app creation/instantiation. They would still receive the app definition/instance and container as such, and wouldn't be directly impacted by this change. However, and injections which are setup by initializers would be affected. All injections are currently assigned by this mechanism, and assigning after would make them unavailable. It's worth noting that only potential solutions mentioned in the RFC which would work for injections done this way would be the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps in v4 we have init() to support this and in Ember v5 we drop it and make everything explicit? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that would be an option if we decided to go with the other DI options and drop |
||
|
||
There are a few options for how we could resolve this: | ||
|
||
1. **Provide injected properties as named params in the constructor.** This would | ||
allow us to mirror the injection definitions, which is fairly intuitive. | ||
However, accessors and methods which depend on services will not work unless | ||
the user assigns the service, which may be counterintuitive: | ||
|
||
```js | ||
class Foo { | ||
@service time; | ||
|
||
getCurrentTime() { | ||
return this.time.now(); | ||
} | ||
|
||
constructor({ time }) { | ||
this.currentTime = time.now(); // this works | ||
this.currentTime = this.getCurrentTime(); // this does not | ||
} | ||
} | ||
``` | ||
|
||
2. **Provide injected properties as positional params to the constructor, using a | ||
separate decorator.** This would completely separate named injections from | ||
constructor injections entirely. This would likely be confusing to newcomers, | ||
but would be similar to how other DI systems work: | ||
|
||
```js | ||
@inject('time', 'date') | ||
class Foo { | ||
@service time; | ||
|
||
constructor(time, date) { | ||
this.currentTime = time.now(); | ||
this.currentDate = date.today(); | ||
} | ||
} | ||
``` | ||
|
||
|
||
3. **Utilize private global state to provide the container to injections during | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this, but then it should be done for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, this would make the most sense if we pursued this strategy. Will update the RFC to reflect that |
||
construction.** If done correctly, this would allow users to use injected | ||
properties during all phases of construction. This would be the most intuitive | ||
for users since it mirrors the existing behavior, but would be the most | ||
difficult to maintain: | ||
|
||
```js | ||
const constructionContainers = []; | ||
|
||
function service(target, key, desc) { | ||
desc.get = function() { | ||
let owner = getOwner(this) | ||
|| constructionContainers[constructionContainers.length - 1); | ||
|
||
return owner.lookup(`service:${key}`); | ||
} | ||
} | ||
|
||
class Container { | ||
lookup(className) { | ||
let Class = this.registry.lookup(className); | ||
|
||
// We push the current container onto a stack, and pop it off when done | ||
// constructing, because other lookups may occur during the construction | ||
// of this object | ||
constructionContainers.push(this); | ||
let instance; | ||
|
||
try { | ||
instance = new Class(); | ||
} finally { | ||
// remove the current container, even in the case of an error | ||
constructionContainers.pop(); | ||
} | ||
|
||
return instance; | ||
} | ||
} | ||
|
||
class Foo { | ||
@service time; | ||
|
||
constructor() { | ||
this.currentTime = this.time.now(); | ||
} | ||
} | ||
``` | ||
|
||
4. **We could not support injections during object construction.** Instead, we could | ||
formalize the `init` hook as a separate hook from the constructor, which | ||
semantically means “initialized by the container”. This may be a reasonable hook | ||
for the framework to have and could be very useful, even if it seems a bit | ||
confusing (e.g. what’s the difference between `init` and `constructor`?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non important, curious question - Would this in effect reduce lookup time for properties/non existent properties, especially for things are are high up the prototype chain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory yes, though I wouldn't count on it being that huge of a win. In aggregate though, who knows 😄