In the
first part of the article, I talked about asynchronous loading of modules using Require.js and standard TypeScript language tools. Inadvertently, I preoccupied the topic of organizing work with abstract events, which I was very quickly reminded of in the comments. In particular, the question was asked, why invent your own bike, if Backbone.Events, proven and well-working and other analogues, have existed for a long time?
If you are interested in the answer to this question, an alternative implementation on TypeScript and is not afraid of reading the code, then I ask under the cat.
Everything is simple - all existing Javascript frameworks today absolutely do not support one of the main advantages of TypeScript - static typing and its consequence - type control at the compilation stage. JS frameworks have no reason to blame for this. There is simply no means in the language for this.
')
However, this is not the only problem. What's worse, JS uses impurities very often, in particular, the whole Backbone is built on them. No, there is nothing wrong with them in themselves. This is a completely natural and viable practice in the context of pure prototype dynamic JS and small projects. In the case of TS, it leads to a number of unpleasant consequences, especially when trying to create an application of at least some serious size:
- The analogue of impurities in classical OOP is multiple inheritance. I would not want to enter the "holy war", but in my opinion, multiple inheritance is always bad. Especially in the conditions of dynamic typing, without the ability to control the behavior of objects at least through an explicit or implicit implementation of interfaces a la C #. Naturally in JS you can not even dream about it, so debugging, support and refactoring of such a code is a complete nightmare.
- Apart from high matters, TS simply does not support this at the language level. Bekbonovsky extend is a complete analogue of inheritance in TS and it works fine for Model and View, but is absolutely not suitable for events. No, we can of course inherit all classes in the application from Backbone.Event or its analogue depending on the framework and achieve the result, but this does not solve the 3rd problem:
- The events of Backbone or any other JS framework are not typed. Goodbye static analysis and all the benefits of TS.
What are such events and what is needed from them?
If you do not go into the wilds, then the event is a signal received by the application from the outside or generated internally, upon receipt of which the application can or may not do anything. Personally, I don’t know what else can be said about events, if I get up at 2 am, send a flashlight to my face and start interrogating)
But everything changes if some context appears. In our case, the context is JavaScript, which, without any add-ons in the form of TS, is already a 100% OOP language. In particular, all entities in JS are objects. DOMEvent objects created by the browser are also objects. That is, if we continue the analogy, then any event is an object.
Suppose that in the case of Backbone, an event is also an object. The question is - what? In fact, we have a collection of callbacks that are called according to some rules. The collection is universal. She is able to take any function. Ie, again I will stop at this, we do not have typification.
But wait. What is our goal? Get static code analysis. Hence, the event must be an object and have type - class. This is the first requirement that is necessary to achieve a result. Events must be described by classes so that they can be typed.
This implies the second requirement - events must be processed and work in the same type, i.e. inherit from base class. If events are inherited, then even without delving into the jungle of SOLID, etc., it is clear that to inherit from them is a completely bad idea.
The third requirement is the minimum required functionality. You can subscribe to an event, unsubscribe from it, as well as call it. Everything else is not critical. Naturally, an event can have one or several handlers.
The fourth consideration is that we are talking about events in the context of asynchronous loading of modules that are strongly typed, which is controlled at the compilation stage. Those. We have a late binding and strong typing situation, i.e. subscribers always know which event they are subscribing to, and dependency management is not their problem.
Fifth, I want events to be part of any object, regardless of the inheritance hierarchy.
Gathering thoughts in a bunch and turning on KMFDM, I proceed to the solution of self-created problems.
Sources, still on Codeplex:
https://tsasyncmodulesexampleapp.codeplex.comFirst thoughts
And so, any object, event is a class, etc. This means 2 things: first, we have a base Event class:
export class Event {
Secondly, we’ll use it like this, carefully sneaking around looking at C # and inspired by its example:
Those. an event is just a public member of a class. No more and no less. What it gives us:
- Events are known at compile time.
- Events can be announced in any class.
- We have the minimum necessary functionality, it is concentrated in one place and easily modified.
- All events are realized by one class or its successors, i.e. we can easily change the logic of their work, for example, by creating a descendant of SecureEvent, inherited from the Event, performing a callback only under certain conditions.
- There is no typical hemorrhoids of JS frameworks with a context that now strictly depends on instances of objects, typos in the names of events, etc.
What we still do not have:
1. Strong typing
2. Due to the lack of context, it is impossible to unsubscribe from a callback event specified by an anonymous function, i.e. any callback we have to save somewhere, which is inconvenient.
3. Non-typed event parameters
Strong typing
We will deal with the first problem. We use the innovation TypeScript 0.9 - generics:
export class Event<Callback extends Function> {
And look at the application:
With this, the following code:
public Foo() { this.MessagesRepo.MessagesLoaded.Add(function (message: string) { alert('MessagesLoaded'); }); }
Will give an error:
Supplied parameters do not match any signature of call target:
Call signatures of types '(message: string) => void' and '(messages: string[]) => void' are incompatible:
Type 'String' is missing property 'join' from type 'string[]'
A callback with no parameters (well, we don’t need them), compiles calmly:
public Foo() { this.MessagesRepo.MessagesLoaded.Add(function () { alert('MessagesLoaded'); }); }
The design of
Callback extends Function
necessary for correct compilation, since TS should know that
Callback
can be called.
Anonymous callbacks and return subscription states
As I wrote above, with this implementation, we cannot unsubscribe anonymous callbacks, which leads to an absolutely unusual for laconic JS with its anonymous verbosity functions and declaring unnecessary variables. For example:
private FooMessagesLoadedCallback = function () { alert('MessagesLoaded'); } public Foo() { this.MessagesRepo.MessagesLoaded.Add(this.FooMessagesLoadedCallback); }
In my opinion, this is a complete enterprise of the brain and the killing of all the functional features of JS / TS.
Nevertheless, it is impossible to do without unsubscribing from events in any more or less complex application, since without this, it is impossible to correctly destroy complex objects and control the behavior of objects participating in interaction through events. For example, we have a certain base class of the FormBase form, from which all forms in our application are inherited. Suppose that he has some method Destroy, which clears all unnecessary resources, unbinds events, etc. Descendant classes override it if necessary. If all functions are stored in variables, then there is no problem to transfer them to the event, and the event through the equality of types has no problem to determine the callback and remove it from the collection. This scenario is not possible using anonymous functions.
I propose to solve the second problem in the following way:
export class Event<Callback extends Function> { public Add(callback: Callback): ITypedSubscription<Callback, Event<Callback>> { var that = this; var res: ITypedSubscription<Callback, Event<Callback>> = { Callback: callback, Event: that, Unsubscribe: function () { that.Remove(callback); } } return res; } public Remove(callback: Callback): void { } public Trigger(): void { } } export interface ISubscription { Unsubscribe: { (): void }; } export interface ITypedSubscription<Callback, Event> extends ISubscription { Callback: Callback; Event: Event; }
Ie, we simply return in the Add method a reference to the event, callback and wrapper for the Remove method. After that, it remains to implement an elementary "finalizer" with the subscriber:
class SomeEventSubscriber { private MessagesRepo = new MessagesRepo(); private Subscriptions: Events.ISubscription[] = []; public Foo() {
Typing of event parameters
Everything is very simple. Again we use generalizations:
export class Event<Callback extends Function, Options> { public Add(callback: Callback): ITypedSubscription<Callback, Event<Callback, Options>> { var that = this; var res: ITypedSubscription<Callback, Event<Callback, Options>> = { Callback: callback, Event: that, Unsubscribe: function () { that.Remove(callback); } } return res; } public Remove(callback: Callback): void { } public Trigger(options: Options): void { } }
The class publisher will now look like this:
export interface ErrorHappenedOptions { Error: any; } export class MessagesRepo { public MessagesLoaded: Events.Event< { (messages: string[]): void }
And the event call is:
var subscriber: Messages.SomeEventSubscriber = new Messages.SomeEventSubscriber(); subscriber.MessagesRepo.MessagesLoaded.Trigger(['Test message 1']); subscriber.MessagesRepo.ErrorHappened.Trigger({ Error: 'Test error 1' });
At this my Wishlist for events end. For the complete source codes and the current example, I ask for
Codeplex .
Thank you all for the positive assessment of the first part.
Depending on the interest in the article and the subject of comments I will choose the theme of the third part. For now, I plan to write my view on widgets / forms, their loading and centralized “memory management” in the application.
Work on the bugs according to the comments:
Part 2.5:
Building Scalable Applications on TypeScript - Part 2.5. Bug fixes and delegates