Если вас интересует ответ на этот вопрос, альтернативная реализация на TypeScript и не пугает чтение кода, то прошу под кат.
Все просто — все существующие Javascript фреймворки на сегодняшний день абсолютно не поддер-живают одно из главных преимуществ TypeScript – статическую типизацию и ее следствие — кон-троль типов на стадии компиляции. JS фреймворки в этом винить нет никакого смысла. В языке просто нет средств для этого.
Однако, это далеко не единственна проблема. Что гораздо хуже, в JS очень часто используются примеси, в частности весь Backbone построен на них. Нет, в них нет ничего плохого в самих по себе. Это вполне естественная и жизнеспособная практика в контексте чистого прототипного динамического JS и небольших проектов. В случае TS она приводит к ряду неприятных последствий, особенно при попытке создать приложение хоть сколько-нибудь серьезного размера:
- Аналогом примесей в классическом ООП является множественное наследование. Не хотелось бы вступать в «священную войну», но на мой взгляд множественное наследование всегда плохо. Особенно в условиях динамической типизации, без возможности контролировать поведение объектов хотя бы через явную или неявную реализацию интерфейсов а-ля C#. Естественно в JS об этом можно даже не мечтать, поэтому отладка, поддержка и рефакторинг подобного кода — полный кошмар.
- Если отвлечься от высоких материй, то TS просто не поддерживает подобное на уровне язы-ка. Бэкбоновский extend это полный аналог наследования в TS и это вполне работает для Model и View, но абсолютно не подходит для событий. Нет, мы конечно можем унаследовать все классы в приложении от Backbone.Event или его аналога в зависимости от фреймворка и добиться результата, но это не решает 3-ей проблемы:
- События Backbone или любого другого JS фреймворка не типизированы. Прощай статический анализ и все преимущества TS.
Что вообще такое события и что вообще от них нужно
Если не вдаваться в дебри, то событие это некоторый сигнал, получаемый приложением извне или генерируемый внутри, по получении которого приложение может что-либо делать, а может и не делать. Лично я не знаю, что еще можно сказать о событиях, если меня поднять в 2 часа ночи, направить фонарик в лицо и начать допрашивать)
Но все меняется, если появляется некоторый контекст. В нашем случае контекстом является JavaScript, который без любых надстроек в виде TS уже является 100% ООП языком. В частности все сущности в JS это объекты. Так же объектом являются и DOMEvent, создаваемые браузером. Т.е., если продолжить аналогию, то любое событие является объектом.
Допустим, что в случае Backbone событие это тоже объект. Вопрос — а какой? По сути, у нас есть коллекция callback’ов, которые вызываются по тем или иным правилам. Коллекция универсальна. Она способна принять любые функции. Т.е., опять я на этом остановлюсь, у нас нет типизации.
Но постойте. Какова наша цель? Получить статический анализ кода. Значит, событие должно быть объектом и иметь тип — класс. Это первое требование, которое я выдвигаю. События должны быть описаны классами, чтобы их можно было типизировать.
Отсюда вытекает второе требование — события должны обрабатываться и работать однотипно, т.е. наследоваться от базового класса. Если события наследуются, то даже не вникая в дебри SOLID и т.п., ясно, что наследоваться от них совсем плохая идея.
Третье требование — минимальный необходимый функционал. На событие можно подписаться, отписаться от него, а также его вызвать. Все прочее — не критично. Естественно, событие может иметь ни одного или несколько обработчиков.
Четвертое соображение — мы говорим о событиях в контексте асинхронной загрузки модулей, которые строго типизированы, что контролируется на этапе компиляции. Т.е. У нас есть ситуация позднего связывания и строгой типизации, т.е. подписчики всегда знают о том, на какое событие подписываются, а управление зависимостями не их проблема.
Пятое — я хочу, чтобы события могли быть частью любого объекта, независимо от иерархии наследования.
Собрав мысли в кучу и включив KMFDM я приступаю к решению созданных самому себе проблем.
Исходники, по-прежнему на Codeplex: https://tsasyncmodulesexampleapp.codeplex.com
Первые мысли
И так, любой объект, событие это класс и т.д. означают 2 вещи: во-первых у нас есть базовый класс Event:
export class Event { //Реализацию временно опускаю Add(callback: any): void { /* Делаем полезную работу */ } Remove(callback: any): void { /* Делаем полезную работу */ } Trigger(): void { /* Делаем полезную работу */ } }
Во-вторых использовать мы его будем примерно так, аккуратно украдкой посмотрев в сторону C# и вдохновившись его примером:
/// <reference path="Events.ts" /> import Events = require('Framework/Events'); export class MessagesRepo { public MessagesLoaded: Events.Event = new Events.Event(); } class SomeEventSubscriber { //Не пинайте. Это просто пример private MessagesRepo = new MessagesRepo(); public Foo() { this.MessagesRepo.MessagesLoaded.Add(function () { alert('MessagesLoaded'); }); } }
Т.е. событие это просто публичный член класса. Не более и не менее. Что нам это дает:
- События известны на стадии компиляции
- События могут быть объявлены в любом классе
- У нас есть минимально необходимый функционал, он сосредоточен в одном месте и легко модифицируется.
- Все события реализуются одним классом или его наследниками, т.е. мы легко можем поме-нять логику их работы, например создав потомка SecureEvent, унаследованного от Event, выполняющего callback’и только при определенных условиях.
- Нет типичного геморроя JS фреймворков с контекстом, который теперь строго зависит от эк-земпляров объектов, опечатками в названиях событий и т.д.
Чего у нас по-прежнему нет:
1. Строгой типизации
2. Из-за отсутствия контекста, невозможно выполнить отписку от события callback’а, заданного анонимной функцией, т.е. любой callback мы должны где-то сохранять, что неудобно.
3. Не типизированные параметры события
Строгая типизация
Разберемся с первой проблемой. Используем нововведение TypeScript 0.9 — обобщения (generics):
export class Event<Callback extends Function> { //Реализацию все еще опускаю Add(callback: Callback): void { /* Делаем полезную работу */ } Remove(callback: Callback): void { /* Делаем полезную работу */ } Trigger(): void { /* Делаем полезную работу */ } }
И посмотрим на применение:
/// <reference path="Events.ts" /> import Events = require('Framework/Events'); export class MessagesRepo { public MessagesLoaded: Events.Event<{ (messages: string[]): void }> = new Events.Event<{ (messages: string[]): void }>(); } class SomeEventSubscriber { //Не пинайте. Это просто пример private MessagesRepo = new MessagesRepo(); public Foo() { this.MessagesRepo.MessagesLoaded.Add(function (messages: string[]) { alert('MessagesLoaded'); }); } }
При этом, следующий код:
public Foo() { this.MessagesRepo.MessagesLoaded.Add(function (message: string) { alert('MessagesLoaded'); }); }
Выдаст ошибку:
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[]'
А callback без параметров (ну не нужны они нам), скомпилируется спокойно:
public Foo() { this.MessagesRepo.MessagesLoaded.Add(function () { alert('MessagesLoaded'); }); }
Конструкция Callback extends Function
необходима для корректной компиляции, т.к. TS должен знать, что Callback можно вызвать.
Анонимные callback’и и возврат состояний подписки
Как я уже писал выше, при данной реализации мы не можем отписать анонимные callback’и, что приводит к абсолютно несвойственной для лаконичного JS с его анонимными функциями многословности и объявлению лишних пременных. Например:
private FooMessagesLoadedCallback = function () { alert('MessagesLoaded'); } public Foo() { this.MessagesRepo.MessagesLoaded.Add(this.FooMessagesLoadedCallback); }
На мой взгляд это полный энтерпрайз головного мозга и убийство всех функциональных черт JS/TS.
Тем не менее, без отписки от событий не обойтись в любом более-менее сложном приложении, т.к. без этого невозможно корректно уничтожать сложные объекты и управлять поведением объектов, участвующих во взаимодействии через события. Например, у нас есть некоторый базовый класс формы FormBase, от которого унаследованы все формы в нашем приложении. Предположим, что у него есть некоторый метод Destroy, который очищает все ненужные ресурсы, отвязывает события и т.д. Классы-потомки переопределяют его при необходимости. Если все функции сохранены в переменных, то нет никакой проблемы передать их событию, а у события через равенство ссылок не никакjй проблемы определить callback и удалить его из коллекции. Данный сценарий невозможен при использовании анонимных функций.
Я предлагаю решать вторую проблему следующим путем:
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 }; } /** Типизированная версия. Включает ссылки на событие и callback */ export interface ITypedSubscription<Callback, Event> extends ISubscription { Callback: Callback; Event: Event; }
Т.е просто возвращаем в методе Add ссылку на событие, callback и обертку для метода Remove. После этого остается реализовать элементарный «финализатор» у подписчика:
/** Кстати, такие комментарии опознаются IntelliSense ;) */ class SomeEventSubscriber { private MessagesRepo = new MessagesRepo(); /** Тут будем хранить все подписки нашего класса */ private Subscriptions: Events.ISubscription[] = []; public Foo() { //Одним движение регистрируем подписку одного события this.Subscriptions.push(this.MessagesRepo.MessagesLoaded.Add(function () { alert('MessagesLoaded'); })); //И совершенно другого this.Subscriptions.push(this.MessagesRepo.ErrorHappened.Add(function (error: any) { alert(error); })); } /** Просто проходит по массиву подписок и отписывает все события независимо от типа */ public Destroy() { for (var i = 0; i < this.Subscriptions.length; i++) { this.Subscriptions[i].Unsubscribe(); } this.Subscriptions = []; } }
Типизация параметров события
Все очень просто. Опять используем обобщения:
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 { /* Делаем полезную работу */ } }
Класс-издатель теперь будет выглядеть так:
export interface ErrorHappenedOptions { Error: any; } export class MessagesRepo { public MessagesLoaded: Events.Event< { (messages: string[]): void } //Callback , string[]> //Options = new Events.Event<{ (messages: string[]): void }, string[]>(); public ErrorHappened: Events.Event< { (error: any): void }, //Callback ErrorHappenedOptions> //Options = new Events.Event<{ (error: any): void }, ErrorHappenedOptions>(); }
А вызов события так:
var subscriber: Messages.SomeEventSubscriber = new Messages.SomeEventSubscriber(); subscriber.MessagesRepo.MessagesLoaded.Trigger(['Test message 1']); subscriber.MessagesRepo.ErrorHappened.Trigger({ Error: 'Test error 1' });
На этом мои хотелки к событиям заканчиваются. За полными исходными кодами и действующим примером прошу на Codeplex.
Всем спасибо за положительную оценку первой части.
В зависимости от интереса к статье и тематики комментариев буду выбирать тему третьей части. Пока планирую написать свой взгляд на виджеты/формы, их загрузку и централизованное «управление памятью» в приложении.
ссылка на оригинал статьи http://habrahabr.ru/post/185160/
Добавить комментарий