|
| 1 | +- Start Date: 2018-08-30 |
| 2 | +- RFC PR: (leave this empty) |
| 3 | +- Ember Issue: (leave this empty) |
| 4 | + |
| 5 | +# Summary |
| 6 | + |
| 7 | +Deprecate computed overridability and `computed().readOnly()` in favor of |
| 8 | +read-only computeds as the default. |
| 9 | + |
| 10 | +# Motivation |
| 11 | + |
| 12 | +Computed properties have existed in Ember long before class syntax and native |
| 13 | +accessors (getters and setters) were readily available, and as such they have a |
| 14 | +few notable behavioral differences. As we move toward adopting native class |
| 15 | +syntax and using a decorator-based form of computeds, it makes sense to |
| 16 | +reconcile these differences so that users can expect them to work the same as |
| 17 | +their native counterparts. |
| 18 | + |
| 19 | +The main and most notable difference this RFC seeks to deprecate is computed |
| 20 | +overridability (colloquially known as "clobbering"). There are some other |
| 21 | +notable differences, including the caching behavior of the `return` value of |
| 22 | +setter functions, which may be addressed in future RFCs. |
| 23 | + |
| 24 | +## Overridability |
| 25 | + |
| 26 | +When defining a native getter without a setter, attempting to set the value will |
| 27 | +throw a hard error (in strict mode): |
| 28 | + |
| 29 | +```js |
| 30 | +function makeFoo() { |
| 31 | + 'use strict'; |
| 32 | + |
| 33 | + class Foo { |
| 34 | + get bar() { |
| 35 | + return this._value; |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + let foo = new Foo(); |
| 40 | + |
| 41 | + foo.bar; // undefined |
| 42 | + foo.bar = 'baz'; // throws an error in strict mode |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +By constrast, computed properties without setters will be overridden when they |
| 47 | +are set, meaning the computed property is removed from the object and replaced |
| 48 | +with the set value: |
| 49 | + |
| 50 | +```js |
| 51 | +const Foo = EmberObject.extend({ |
| 52 | + bar: computed('_value', { |
| 53 | + get() { |
| 54 | + return this._value; |
| 55 | + }, |
| 56 | + }), |
| 57 | +}); |
| 58 | + |
| 59 | +let foo = Foo.create(); |
| 60 | + |
| 61 | +foo.bar; // undefined |
| 62 | +foo.set('bar', 'baz'); // Overwrites the getter |
| 63 | +foo.bar; // 'baz' |
| 64 | +foo.set('_value', 123); |
| 65 | +foo.bar; // 'baz' |
| 66 | +``` |
| 67 | + |
| 68 | +This behavior is confusing to newcomers, and oftentimes unexpected. Common best |
| 69 | +practice is to opt-out of it by declaring the property as `readOnly`, which |
| 70 | +prevents this overridability. |
| 71 | + |
| 72 | +# Transition Path |
| 73 | + |
| 74 | +This RFC proposes that `readOnly` properties become the default, and that in |
| 75 | +order to override users must opt in by defining their own setters: |
| 76 | + |
| 77 | +```js |
| 78 | +class Foo { |
| 79 | + get bar() { |
| 80 | + if (this._bar) { |
| 81 | + return this._bar; |
| 82 | + } |
| 83 | + |
| 84 | + return this._value |
| 85 | + } |
| 86 | + |
| 87 | + set bar(value) { |
| 88 | + this._bar = value |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +## Macros |
| 94 | + |
| 95 | +Most computed macros are overridable by default, the exception being `readOnly`. |
| 96 | +This RFC proposes that all computed macros with the exception of `reads` would |
| 97 | +become read only by default. The purpose of `reads` is to _be_ overridable, so |
| 98 | +its behavior would remain the same. |
| 99 | + |
| 100 | +## Decorator Interop |
| 101 | + |
| 102 | +It may be somewhat cumbersome to write overriding functionality or add proxy |
| 103 | +properties when overriding is needed. In an ideal world, computed properties |
| 104 | +would modify accessors transparently so that they could be composed with other |
| 105 | +decorators, such as an `@overridable` decorator: |
| 106 | + |
| 107 | +```js |
| 108 | +class Foo { |
| 109 | + @overridable |
| 110 | + @computed('_value') |
| 111 | + get bar() { |
| 112 | + return this._value; |
| 113 | + } |
| 114 | + |
| 115 | + @overridable |
| 116 | + @and('baz', 'qux') |
| 117 | + quux; |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +Currently this is not possible as computed properties store their getter/setter |
| 122 | +functions elsewhere and replace them with a proxy getter and the mandatory |
| 123 | +setter assertion, respectively. In the long term, making computeds more |
| 124 | +transparent in this way would be ideal, but it is out of scope for this RFC. |
| 125 | + |
| 126 | +## Deprecation Timeline |
| 127 | + |
| 128 | +This change will be a breaking change, which means we will not be able to change |
| 129 | +the behavior of `computed` until Ember v4.0.0. Additionally, users will likely |
| 130 | +want to continue using `.readOnly()` up until overriding has been fully removed |
| 131 | +to ensure they are using properties safely. With that in mind, the ordering of |
| 132 | +events should be: |
| 133 | + |
| 134 | +1. Ember v3 |
| 135 | + * Deprecate the default override-setter behavior immediately. This means that |
| 136 | + a deprecation warning will be thrown if a user attempts to set a |
| 137 | + non-`readOnly` property which does not have a setter. User's will still be |
| 138 | + able to declare a property is `readOnly` without a deprecation warning. |
| 139 | + * Add optional feature to change the deprecation to an assertion after the |
| 140 | + deprecation has been released, and to show a deprecation when using |
| 141 | + the `.readOnly()` modifier. |
| 142 | + * After the deprecation and optional feature have been available for a |
| 143 | + reasonable amount of time, enable the optional feature by default in new |
| 144 | + apps and addons. The main reason we want to delay this is to give _addons_ |
| 145 | + a chance to address deprecations, since enabling this feature will affect |
| 146 | + both apps and the addons they consume. |
| 147 | +2. Ember v4 |
| 148 | + * Remove the override-setter entirely, making non-overrideable properties the |
| 149 | + default. |
| 150 | + * Make the `readOnly` modifier a no-op, and show a deprecation warning when it |
| 151 | + is used. |
| 152 | + |
| 153 | +The warnings should explain the deprecation, and recommend that users do not |
| 154 | +rely on setter behavior or opting-in to read only behavior. |
| 155 | + |
| 156 | +# How We Teach This |
| 157 | + |
| 158 | +In general, we can teach that computed properties are essentially cached native |
| 159 | +getters/setters (with a few more bells and whistles). Once we have official |
| 160 | +decorators in the framework, we can make this connection even more solid. |
| 161 | + |
| 162 | +We should add notes on overridability, and we should scrub the guides of any |
| 163 | +examples that make use of overriding directly and indirectly via `.readOnly()`. |
| 164 | + |
| 165 | +# Drawbacks |
| 166 | + |
| 167 | +Overriding is not a completely uncommonly used feature, and developers who have |
| 168 | +become used to it may feel like it makes their code more complicated, especially |
| 169 | +without any easy way to opt back in. |
| 170 | + |
| 171 | +# Alternatives |
| 172 | + |
| 173 | +We could convert `.readOnly()` into `.overridable()`, forcing users to opt-in |
| 174 | +to overriding. Given the long timeline of this deprecation, it would likely be |
| 175 | +better to work on making getters/setters transparent to decoration, and provide |
| 176 | +a `@overridable` decorator either in Ember or as an independent package. |
0 commit comments