CC-Archived / promise-as3

Promises/A+ compliant implementation in ActionScript 3.0
167 stars 52 forks source link

Separate Promise interface, implementation and utility methods #32

Closed fwienber closed 10 years ago

fwienber commented 10 years ago

According to the spec, a Promise is just a "thenable", i.e. an object with a then() method with a certain signature. In ActionScript, this could best be expressed with an interface. Your implementation provides a Promise class that aggregates (at least) three aspects:

  1. the (extended) Promise interface: then, otherwise, always, done
  2. the actual implementation
  3. static utility methods: when, isThenable, all, ...

For separation of concerns, a clear, concise public API, and to simplify building adapters to foreign promises, these aspect should be separated into three entities:

  1. an interface IPromise that only specifies one method: then
  2. the implementation PromiseImpl that should not be used by client code directly, only through Deferred (maybe this is really the Resolver?)
  3. a class Promises containing all static utility methods plus static convenience methods for otherwise, always and done, using only the IPromise interface in its API

Deferred#promise would change its type from Promise to IPromise.

To stay compatible, class Promise could be [Deprecated] and

If you definitely want to keep the additional convenience methods otherwise, always, done, instead of adding them as static convenience methods to Promises, you would add them to IPromise and provide an AbstractPromise class that implements the convenience methods, but not then. (Technically, since ActionScript does not offer abstract classes or methods, it would have to implement then, but it could for example throw an "abstract method" error.) All IPromise implementations (PromiseImpl, foreign promise adapters) would then extend AbstractPromise and only have to implement then.

johnyanarella commented 10 years ago

No.

A Promise is not "just" a thenable. A thenable is a thenable. There can be other kinds of thenables that are not Promises.

The presence of a then() function is the sole defining characteristic of a thenable.

A Promise is a thenable with additional functionality and behavior, some of which is standardized in the Promises/A+ specification.

If the ActionScript core framework had defined a IThenable interface, and all potential participants implemented that interface, part of what you describe might be reasonable.

The definition of thenable originates from a philosophy of duck typing, not strong typing. If it has a then() function, it's a thenable.

As for the IPromise suggestion:

There is no guarantee that any other future Promise implementation for ActionScript would implement Promise-AS3's IPromise interface even if it existed. In fact, it would be highly unlikely. So, that would mean writing adapters for any new Promise implementation as they emerge (unlikely as that is, given ActionScript's dim future).

Further, it assumes that all IPromise implementations would be equal, and trustworthy.

One of the major pain points discovered as more and more JavaScript implementations of Promises emerged was the incompatibility of seemingly minor differences in their behavior as it relates to timing (current tick vs next tick execution) and behavior (handling of return values and thrown errors to support recovery and propagation). These subtle differences have a huge impact on the outcome of any non-trivial promise chain.

(Anyone who has developed a Promise library implementation and attempted to pass the current suite of Promise/A+ tests knows just how subtle those differences can be.)

Thankfully, this is part of what the specification solves. The standards codified in the Promises/A+ specification define a method for interoperability that provides safe interaction between different Promise implementations. Promises implementations vary wildly, but they share one thing - the then() method. So, the specification defines the then() method as the sole integration point.

This allows a library like Promise-AS3 to adapt and integrate even with badly behaving Promises, so long as they have a then() function that calls the specified handlers. With this approach, there is also no need to create adapters to make things conform to an arbitrary IPromise interface.

A Promise-AS3 Deferred is never going to return anything other than a trusted instance of a Promise-AS3 Promise class. The resolver logic will always adapt external Promises into trusted Promises. Under no circumstances do I want to have to directly consume some random Promise implementation that claims to be an IPromise, that behaves subtly differently and breaks the behavior deep in a nested Promise chain. Nor, as an end-user developer, would I ever want to attempt to debug such a thing.

My stance is that the suggestions you outline above are hostile to end-user developers of this framework. They require the developer to engage in unnecessary additional ceremony to interact with the library. They eliminate the fluent nature of the API and cofound the convenience of chaining. They make it much more foreign to developers who have learned popular implementations of Promises in JavaScript.

I understand why you made this suggestion and I appreciate you spending the time to write up your case for it. The classical interface and implementation approach is powerful and useful in many contexts. Taken an extreme, it results in APIs that are unnecessarily complex and developer hostile. The core concern of interoperability between different Promise implementations can and has been solved in a different (and safer) manner here.

fwienber commented 10 years ago

It seems I didn't get across the main intention of the refactoring I propose. The intention is not to create an interface any thenable must implement. I understand that promises use duck typing. Other promise implementations will not know of your interface at all, so they cannot implement it. Only your framework should provide an implementation of the interface. Client code should only use the interface (API), not implement it (SPI). Only extensions of the framework (custom adapters) may want to provide alternative implementations, but most likely, they use Deferred to produce safe promises and there remains only one (internal) implementation. In any case, it is still an advantage to use an interface (see below). The interface is intended to mean "a safe promise generated by this library". An interface specifies more than method signatures, it also specifies a contract (in the ASDoc), and your contract would be "Promises/A+ spec". The main point is that client code must not instantiate the class Promise. In ActionScript, the only way to keep a client from instantiating a public type is to make the type an interface. This does not make the API any more complicated, because in exchange, the implementing class would not be part of the API (but an internal class). I also still think it is a good idea to put all the static utility methods into a separate class (you cannot define them in an interface, anyway). You can continue making up many new utility methods (just look at Q.js!), but the core promise interface is not likely to be extended at the same speed, if at all. Thus, you separate the (stable) core API from the (growing) utility API. If you like chaining and really think the additional methods will be used very often, my second proposal was to keep the convenience methods in the promise interface. If you think there is no need for an AbstractPromise as a base class for different internal implementations (because there is only one?), just leave it out. Thus, an alternative refactoring would be to make Promise an interface, move its implementation to an internal class PromiseImpl, and move its static methods to a new class Promises or even directly inside the package (like you define spread). Using this approach, the name of the type Promise stays the same, but the downside is this would be a breaking API change, because the static methods cannot be defined in the interface to forward to the new utility functions. This is why I originally proposed the new name IPromise for the interface, so that Promise can remain backwards-compatible ([Deprecated]) and become an internal class later. Does that make more sense?