Статьи

Уникальный подход к наблюдателю / наблюдаемой модели на Цейлоне

Суть известного наблюдателя / наблюдаемого паттерна состоит в том, что у вас есть наблюдаемый объект, который производит события различного рода, и один или несколько объектов- наблюдателей, которые регистрируют себя как заинтересованные в уведомлении, когда происходят эти события.

Конечно, мы представляем каждый тип события как тип, обычно класс, хотя ничто не мешает нам использовать тип интерфейса в качестве типа события.

Например:

1
2
class Started() {}
class Stopped() {}

Тип события может быть даже общим:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class Created<out Entity>
        (shared Entity entity)
        given Entity satisfies Object {
    string => "Created[``entity``]";
}
 
class Updated<out Entity>
        (shared Entity entity)
        given Entity satisfies Object {
    string => "Updated[``entity``]";
}
 
class Deleted<out Entity>
        (shared Entity entity)
        given Entity satisfies Object {
    string => "Deleted[``entity``]";
}

Конечно, у нас есть мощные механизмы для абстрагирования типов событий, например:

1
2
3
4
5
alias Lifecycle<Entity>
        given Entity satisfies Object
        => Created<Entity> |
           Updated<Entity> |
           Deleted<Entity>;

Наблюдатель, как правило, по сути является не чем иным, как функцией, которая принимает определенный тип события в качестве параметра.

Например, эта анонимная функция наблюдает за созданием User s:

1
2
(Created<User> userCreated)
        => print("new user created: " + userCreated.entity.name)

Эта анонимная функция наблюдает события жизненного цикла любого вида объекта:

1
2
(Lifecycle<Object> event)
        => print("something happened: " + event)

Типы объединения и пересечения дают нам хороший способ выразить соединение и дизъюнкцию типов событий:

1
2
3
4
5
6
7
8
9
void (Created<User>|Deleted<User> userEvent) {
    switch (userEvent)
    case (is Created<User>) {
        print("user created: " + userEvent.entity.name);
    }
    case (is Deleted<User>) {
        print("user deleted: " + userEvent.entity.name);
    }
}

Теперь здесь мы можем сделать что-то действительно милое. Как правило, в других языках наблюдаемый объект предоставляет различные операции регистрации наблюдателя, по одной для каждого типа события, которое производит объект. Мы собираемся определить универсальный класс Observable который работает для любого типа события и использует усовершенствованные обобщенные типы для отображения событий в функции наблюдателя.

1
2
3
4
shared class Observable<in Event>()
        given Event satisfies Object {
    ...
}

Параметр типа Event захватывает различные виды событий, которые создает этот объект, например, Observable<Lifecycle<User>> создает события типа Created<User> , Updated<User> и Deleted<User> .

Нам нужен список для хранения наблюдателей в:

1
value listeners = ArrayList<Anything(Nothing)>();

Здесь Anything(Nothing) является супертипом любой функции с одним параметром.

Метод addObserver() регистрирует функцию наблюдателя в Observable :

1
2
3
4
shared void addObserver<ObservedEvent>
        (void handle(ObservedEvent event))
        given ObservedEvent satisfies Event
        => listeners.add(handle);

Этот метод принимает функции наблюдателя только для некоторого подмножества событий, фактически произведенных Observable . Это ограничение обеспечивается верхней границей given ObservedEvent satisfies Event .

Метод raise() производит событие:

1
2
3
4
shared void raise<RaisedEvent>(RaisedEvent event)
        given RaisedEvent satisfies Event
        => listeners.narrow<Anything(RaisedEvent)>()
            .each((handle) => handle(event));

Опять же, верхняя граница обеспечивает, что этот метод принимает только те объекты событий, которые имеют тип события, созданный Observable .

Этот метод использует новый метод narrow() Iterable в Цейлоне 1.2, чтобы отфильтровать функции-наблюдатели, которые не принимают возбужденный тип события. Этот метод реализован с использованием усовершенствованных обобщений. Вот его определение в Iterable<Element> :

1
2
shared default {Element&Type*} narrow<Type>()
        => { for (elem in this) if (is Type elem) elem };

То есть, если у нас есть поток Element s, и мы вызываем narrow<Type>() , явно передавая произвольный тип Type , то мы возвращаем поток всех элементов исходного потока, которые являются экземплярами Type . Это, естественно, поток Element&Type s.

Теперь, наконец, если мы определим экземпляр Observable :

01
02
03
04
05
06
07
08
09
10
11
object userPersistence
        extends Observable<Lifecycle<User>>() {
 
    shared void create(User user) {
        ...
        //raise an event
        raise(Created(user));
    }
 
    ...
}

Затем мы можем зарегистрировать наблюдателей для этого объекта следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
//observe User creation events
userPersistence.addObserver(
        (Created<User> userCreated)
        => print("new user created: " + userCreated.entity.name));
 
//observe User creation and deletion events
userPersistence.addObserver(
        void (Created<User>|Deleted<User> userEvent) {
    switch (userEvent)
    case (is Created<User>) {
        print("user created: " + userEvent.entity.name);
    }
    case (is Deleted<User>) {
        print("user deleted: " + userEvent.entity.name);
    }
});

Обратите внимание, что с типами объединения и пересечения, подтипами и дисперсией мы находимся с мощным языком выражений, позволяющим точно определить, какие именно типы событий нас интересуют, с учетом безопасности типов, прямо в списке параметров функции наблюдателя.

Для записи вот полный код Observable :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
shared class Observable<in Event>()
        given Event satisfies Object {
    value listeners = ArrayList<Anything(Nothing)>();
 
    shared void addObserver<ObservedEvent>
            (void handle(ObservedEvent event))
            given ObservedEvent satisfies Event
            => listeners.add(handle);
 
    shared void raise<RaisedEvent>(RaisedEvent event)
            given RaisedEvent satisfies Event
            => listeners.narrow<Anything(RaisedEvent)>()
                .each((handle) => handle(event));
 
}

Наконец, предостережение: точный код, приведенный выше, не компилируется в Ceylon 1.1, потому что метод narrow() является новым и из-за исправленной ошибки в проверке типов. Но это будет работать в предстоящем выпуске Цейлона 1.2.