wtorek, 3 stycznia 2012

[RX 8] Zdarzenia w .NET niby pożyteczne, łatwe, ale … obserwowanie w Reactive Extensions jest lepsze [PL]

W ramach kontynuacji cyklu o Reactive Extensions dla .NET ([RX 1], [RX 2], [RX 3], [RX 4], [RX 5], [RX 6], [RX 7]) chciałbym przejść teraz do zdarzeń, dla których Rx dostarcza szczególnego wsparcia.

Programowanie reaktywne, to również wykorzystanie zdarzeń (z j. ang. event), do których podłączamy się, przekazując delegat do funkcji, która ma obsłużyć zdarzenie (z j. ang. event handler), następnie czekamy na nachodzące zdarzenia. Dla przykładu rozważmy obsługę zdarzeń związanych z ruchem myszy. Wyobraźmy sobie okno aplikacji, które udostępnia zdarzenie MouseMove, do którego podłączamy delegat w postaci wyrażenia lambda, w którym podjęta jest akcja po zajściu zdarzenia. Można to przestawić w postaci niniejszego kodu źródłowego:

this.MouseMove += (sender, args) => { //MouseMove to ukryte źródło danych
    if ( args.Location.X < this.Width / 2 ) // Jak filtrować?
      {}//... A może kolejne zdarzenie? Co z kompozycją?
  };

//a po wszystkim:
this.MouseMove -= // a co tutaj?

W takim podejściu można jednak wskazać szereg problemów i elementów wymagających rozważenia:

  • Łatwo użyć wyrażenie lambda, by „podłączyć się do zdarzenia”, ale co powinniśmy wykonać przy odłączeniu? Czy należy przechowywać referencję do wyrażenia lambda (lub funkcji anonimowej), by później ją wykorzystać przy odłączaniu? Przecież zwalnianie zasobów jest bardzo ważne!

  • Jak filtrować takie zdarzenia? Wyobraźmy sobie sytuację, w której jesteśmy zainteresowani ruchem myszki, ale tylko w danym obszarze. Czy dobrym podejściem jest umieszczanie wielu instrukcji warunkowych w obsłudze zdarzenia? Czy tak napisany kod będzie czytelny i łatwo go będzie później utrzymywać, zmieniać i poprawiać?

  • Co w przypadku gdy chcemy komponować zdarzenia (np. jesteśmy zainteresowani ruchem myszką, ale tylko w przypadku naciśnięcia jakiegoś przycisku na klawiaturze)? Czy dobrym podejściem w obsłudze zdarzenia jest sprawdzanie zewnętrznych stanów lub wyzwalanie innych zdarzeń?

  • Jak obsługiwać błędy związane ze zdarzeniem (np. jesteśmy zainteresowani zdarzeniami ruchu myszą, ale tylko jeżeli odstęp pomiędzy nimi jest nie dłuższy niż 1s)? Jak obsłużyć informację o tym, że było to już ostatnie zdarzenie i inne się już nie pojawi. A co w przypadku awarii po stronie źródła danych?

  • Zauważmy również że w tym przypadku mysz to ukryte źródło danych, ale czy da się je przekazywać? Jak napisać testy jednostkowe testujące obsługę tego zdarzenia?

Jak widać, w tym prostym podejściu widać szereg potencjalnych problemów. Można jednak zrobić to lepiej, a z pomocą przychodzą nam rozszerzenia Reactive Extensions.

Zdarzenia w Rx

Jak pomoże Rx we wspomnianym przykładzie obsługi zdarzeń ruchu myszą? Otóż wspomniane rozszerzenia pozwalają na zamianę niejawnego źródła danych związanego ze zdarzeniami ruchu myszy w obserwowalne kolekcje, które można subskrybować i otrzymywać powiadomienia o zdarzeniach, np.:

IObservable<Point> mouseMove =
  Observable.FromEventPattern<MouseEventArgs>( this, "MouseMove" )
  .Select( ev => ev.EventArgs.Location );

Jeśli komuś wadzi "magiczny" łańcuch tekstowy "MouseMove" (który może sprawić problem po zmianie nazwy zdarzenia), to może napisać również:

IObservable<Point> mouseMove =
  Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>( h => this.MouseMove += h, h => this.MouseMove -= h )
  .Select( ev => ev.EventArgs.Location );

Każde podejście z powyższych tworzy obserwowalną i "ciepłą" kolekcję punktów wyznaczonych przez ruch myszą. To które wybrać, Rx pozostawia już wyborze programisty. W przypadku zdarzeń z .NET framework, gdzie nazwy zdarzeń raczej nie ulegną zmianie, łatwiej pewnie napisać kod według pierwszego przykładu. W przypadku zdarzeń, które my tworzymy, chyba lepiej zastosować drugie podejście, które choć odrobinę mniej czytelne, to bezpieczniejsze za względu na to, że ew. błędy wykryje już kompilator, a po zmianie nazwy zdarzenia kompilator sam wykryje problem.

Oczywiście samo utworzenie kolekcji nie wystarczy trzeba ją subskrybować, np. przez:

var subscription = mouseMove.ObserveOn( this )
  .Subscribe( p => { this.label1.Text = String.Format( "{0},{1}", p.X, p.Y ); } );

Natomiast, gdy subskrypcja już jest niepotrzebna, wystarczy zwolnić zasoby przez wywołanie Dispose:

subscription.Dispose();

Dzięki takiemu podejściu, opartego o kolekcje, mamy jawne źródło danych (które np. można przekazywać, czy wykorzystywać w testach jednostkowych), wspomniane kolekcje można filtrować lub dokonywać ich kompozycji (np. przy użyciu LINQ), a po wszystkim łatwo jest zwolnić zasoby, przez proste wykorzystanie interfejsu IDisposable.

Promuj

Brak komentarzy:

Prześlij komentarz

Posty powiązane / Related posts