News, Praca w IT

Await event, czyli jak śledzić zdarzenia z użyciem TaskCompletionSource

Mówi się, że pierwszą programistką była hrabina Lovelace. Osobiście uważam, iż pierwszą programistką była Matka Natura, a jednym z pierwszych wzorców projektowych przez nią stworzonych jest popularny „obserwator”. Jak śledzić dowolne zdarzenia wykorzystując do tego TaskCompletionSource? Tego dowiecie się z poniższego artykułu.


Dominik Przywara. Mobile Solutions Center Developer w Billennium tworzący rozwiązania przy pomocy technologii Microsoft — od Azure po Xamarin. Współzałożyciel Śląskiej Grupy Microsoft, członek Billennium Inspiration Team. Okazjonalnie prowadzący szkolenia z tematyki Azure i SOLID/TDD. Prywatnie: fan dobrych seriali, koder, gamer.


 

 

Zanim to nastąpi, spójrzmy, jak brzmi definicja „obserwatora”: „Relacja jeden do wielu pomiędzy obiektami stworzona w taki sposób, że kiedy jeden obiekt zmienia swój stan, wszyscy jego obserwatorzy są o tym fakcie informowani i mogą na te zmiany zareagować, w sposób dla siebie odpowiedni.”

Prosty przykład: kiedy uderzymy kolanem o coś z odpowiednią siłą, nasz ośrodkowy układ nerwowy poinformuje wszystkich swoich subskrybentów o tym zdarzeniu. Kto może być takim subskrybentem? Np. gruczoł łzowy, który przy odpowiedniej wartości siły uderzenia spowoduje, że po naszym policzku spłynie łza. Może być nim też nasz mózg, który poinformuje mięśnie, żebyśmy się chwycili za kolano czy wypowiedzieli pewne słowa. Idea wzorca jest więc banalnie prosta.

Jeżeli wiesz, czym są eventy i jak sobie z nimi radzić, to możesz przeskoczyć prosto do rozdziału 2., by dowiedzieć się, jak być na bieżąco ze zdarzeniami.

Podstawy

Praca ze zdarzeniami w .NET jest na pierwszy rzut oka bardzo łatwa. Tak naprawdę musimy znać tylko dwa operatory += i -=, przy czym ten drugi bardzo często jest przez nas, programistów zapominany. Ale po kolei. Wiemy już, że musimy mieć obiekt informujący i jakiegoś subskrybenta. Niech naszym obserwowanym (ang. observable, subject) będzie przycisk, który będzie posiadał pole Clicked typu EventHandler opatrzone słowem kluczowym event.

public class Button
{
  public event EventHandler Clicked;
  /*
  * reszta implementacji
  */
}

Aby nasz przycisk informował nas, że został kliknięty musimy zasubskrybować się do naszego zdarzenia Clicked w sposób, w jaki zostało to uczynione w metodzie Initialize().

private void Initialize()
{
  var playButton = new Button();
  playButton.Clicked += PlayButton_Clicked;
}
private void PlayButton_Clicked(object sender, EventArgs e)
{
  //Implemantacja naszego reagowania na kliknięcie przycisku "Play"
}

Metoda PlayButton_Clicked będzie uruchamiana za każdym razem, kiedy ktoś kliknie w przycisk w naszej formatce. Musimy pamiętać, że Garbage Collector nie „posprząta” nam po obiekcie playButton, jeśli nie przestaniemy obserwować jego zdarzeń – możemy to osiągnąć w następujący sposób:

playButton.Clicked -= PlayButton_Clicked;

W tym wszystkim jest jeszcze jeden problem – kiedy i w jaki sposób usunąć subskrypcję, ale to nie jest tematem dzisiejszego artykułu.

Async over events

Model pracy ze zdarzeniami wygląda więc następująco – wiemy, że w przyszłości może wydarzyć się określona rzecz, ale nie wiemy dokładnie kiedy. Pierwszą rzeczą, która przychodzi mi na myśl, gdy zastanawiałem się nad przykładem wykorzystania TaskCompletionSource, było pobieranie danych. Uruchamiamy pobieranie jakiegoś zestawu danych i subskrybujemy się do zdarzeń typu DownloadCompleted, DownloadFailed i DownloadCancelled. Scenariusz jest bardzo prosty, bo dane pobieramy tylko z jednego źródła i tylko te dane będziemy chcieli wyświetlić w naszym widoku. Wiele obsłużonych zdarzeń też nie powinno przysporzyć nam bólu głowy, o ile nasza klasa nie będzie wyglądała jak spaghetti. A co w przypadku nieco bardziej skomplikowanego procesu?

Wyobraźmy sobie, że jesteśmy w troszkę starszym projekcie. Do informowania o stanie pobierania wykorzystujemy eventy i dane pobieramy z kilku różnych serwisów – zawsze niezależnie. Biznes przychodzi i nagle chce pobrać dane z serwisu B na podstawie tych pobranych z serwisu A – np. wyliczenie kosztów wysyłki na podstawie wagi zamówionych artykułów. Wyglądałoby to przynajmniej tak brzydko jak poniżej. A to przypadek, gdzie tylko jeden serwis jest zależny od drugiego!

public void DownloadDeliveryPriceDependingOnWeight()
{
  productsService.DownloadCompleted += ProductsService_WeightCalculation_DownloadCompleted;
  productsService.DownloadAsync(basket);
}
private void ProductsService_WeightCalculation_DownloadCompleted(object sender, ProductsServiceResult productsServiceResult)
{
  productsService.DownloadCompleted -= ProductsService_WeightCalculation_DownloadCompleted;
  deliveryService.DownloadCompleted += DeliveryService_PriceDependingOnWeight_DownloadCompleted;
  deliveryService.DownloadAsync(productsServiceResult);
}
  private void DeliveryService_PriceDependingOnWeight_DownloadCompleted(object sender, DeliveryServiceResult deliveryServiceResult)
{
//Obsługujemy pobrane dane
}

Pójdźmy krok dalej i postawmy sobie nowe wymaganie — pobieranie danych z wielu serwisów i złożenie ich w jeden obiekt oraz wyświetlenie go użytkownikowi w formularzu. Można to zrobić w sposób bardzo łatwy i uzupełniać pola naszego elementu całkowicie asynchronicznie, co przełożyłoby się na tragiczne doznania z użytkowania naszej aplikacji. A gdyby jeszcze jedno z pól wyliczało się na podstawie innych? Jak zmieszany byłby użytkownik, gdyby kwota zmieniała się w losowych odstępach czasu bez większego wytłumaczenia dlaczego?

Na pewno najlepszym rozwiązaniem w tej sytuacji byłoby przepisanie sposobu pobierania i używania dobrodziejstwa typu Task i słowa kluczowego await. Niestety takie rozwiązania nie zawsze są możliwe i musimy sobie radzić z tego typu problemami w inny sposób.

Wraz z premierą .NET 4.0 dano nam możliwość korzystania z ogromnej biblioteki o nazwie Task Parallel Library (TPL) i sławnych już async i await. W przestrzeni nazw System.Threading.Tasks znajduje się rozwiązanie naszych powyższych problemów — klasa o nazwie TaskCompletionSource<TResult>, która to jest „producentem” obiektu typu Task<TResult> bez potrzeby podawania do niego delegaty. Rozwiążmy więc nasz pierwszy przypadek z użyciem TaskCompletionSource!

Zaczynamy refactor! Na pierwszy ogień metoda DownloadDeliveryPriceDependingOnWeight(). Po pierwsze słówko kluczowe async, bo skorzystamy z słowa kluczowego await i typ zwracany – Task<decimal>, bo zwrócimy pobraną wartość. Dodatkowo, zamiast wywołania metody serwisu produktów DownloadAsync() i podpięcia się pod event, wywołujemy dwie nowo utworzone metody i awaitujemy je.

public async Task<decimal> DownloadDeliveryPriceDependingOnWeight()
{
  var productsServiceResult = await DownloadProductsWeightCalculation();
  var deliveryServiceResult = await DownloadDeliveryPriceDependingOnWeight(productsServiceResult);
  return deliveryServiceResult.Price;
}

Następne dwie utworzone metody DownloadProductsWeightCalculation() i DownloadDeliveryPriceDependingOnWeight() będą się delikatnie różniły, lecz skorzystają z tego samego mechanizmu. Ja pozwolę sobie opisać tylko pierwszą z metod, ponieważ różnice są wytłumaczone w komentarzach metody drugiej.

Tworzymy obiekt tcs typu TaskCompletionSource<ProductsServiceResult>. Obiekt ten będzie producentem naszego Task’a, który zwraca wartość typu ProductsServiceResult. Tworzymy EventHandler<ProductsServiceResult>, który zostanie przypisany do productsService.DownloadCompleted. Nie tworzymy nowej metody, ponieważ chcemy uniknąć przekazywania obiektu tcs, co mogłoby być problematyczne. W ciele wyrażenia lambda downloadCompleted na samym początku odpinamy nasz EventHandler, dzięki czemu mamy jeden wyciek mniej – brawo my! Ponadto wywołujemy metodę TrySetResult(), która to zmienia stan naszego Task’a na Completed i zwraca pobraną wartość.

private Task<ProductsServiceResult> DownloadProductsWeightCalculation()
{
  var tcs = new TaskCompletionSource<ProductsServiceResult>();
  //Bez wcześniejszego przypisania jakiejkolwiek wartości nie możemy odpiąć EventHandler'a
  //(dla kompilatora jest to unassigned value)
  EventHandler<ProductsServiceResult> downloadCompleted = null;
  downloadCompleted = (object sender, ProductsServiceResult productsServiceResult) =>
  {
    //Odpinamy EventHandler == jeden wyciek mniej!
    productsService.DownloadCompleted -= downloadCompleted;
    //Ustawiamy wartość zwracaną z taska i await zostaje "odpalany"
    tcs.TrySetResult(productsServiceResult);
  };
  productsService.DownloadCompleted += downloadCompleted;
  productsService.DownloadAsync(basket);
  return tcs.Task;
}
private async Task<DeliveryServiceResult> DownloadDeliveryPriceDependingOnWeight(ProductsServiceResult productsServiceResult)
{
  var tcs = new TaskCompletionSource<DeliveryServiceResult>();
  EventHandler<DeliveryServiceResult> downloadCompleted = (object sender, DeliveryServiceResult deliveryServiceResult) =>
  {
    tcs.TrySetResult(deliveryServiceResult);
  };
  deliveryService.DownloadCompleted += downloadCompleted;
  deliveryService.DownloadAsync(productsServiceResult);
  //Tym razem nie zwracamy tcs.Task tylko awaitujemy - chcemy wykonać więcej operacji w ramach metody
  var result = await tcs.Task;
  //Inny sposób na odpinanie eventu. Użyteczne, kiedy mamy więcej niż jedną subskrypcję
  //na zdarzenia
  deliveryService.DownloadCompleted -= downloadCompleted;
  //np. deliveryService.DownloadFailed -= downloadFailed;
  return result;
}

Tylko tyle i aż tyle jest potrzebne do tego, aby uprościć potencjalnie zagmatwany kod. Jest jeszcze jeden gratis, który dostajemy przy użyciu tego wzorca. W miejscu, w którym wywołujemy pobieranie, jesteśmy w stanie użyć słowa kluczowego await i śledzić moment zakończenia pobierania (może to mieć znaczenie, kiedy będziemy chcieli np. zbudować większy proces biznesowy).

A co jeśli chodzi o drugi przedstawiony wcześniej problem? Pozwolę sobie pominąć próbę pokazania, jak brzydkie i wręcz niemożliwe do utrzymywania byłoby takie rozwiązanie i przejdę prosto do możliwego rozwiązania. Tutaj też nie będę zagłębiał się w szczegóły, ponieważ implementacja każdego serwisu wyglądałaby bardzo podobnie jak w poprzednim przykładzie. Polecam jedynie zwrócić uwagę na metodę GetVeryImportantObject(). Nie czekaliśmy na wyniki po kolei, tylko wykorzystaliśmy agregację Tasków i nie zrobimy niczego, dopóki wszystkie operacje nie zostaną zakończone – jest to bardzo ważne, ponieważ cały obiekt chcemy zwrócić w jednym momencie.

public Task<FirstServiceResult> GetDataFromXService()
{
  //implementacja jak we wcześniejszym przykładzie dla każdego serwisu
}
public async Task<VeryImportantObject> GetVeryImportantObject()
{
  var firstServiceResult = GetDataFromFirstService();
  var secondServiceResult = GetDataFromSecondService();
  var thirdServiceResult = GetDataFromThirdService();
  var fourthServiceResult = GetDataFromFourthService();
  var fifthServiceResult = GetDataFromFifthService();
  await Task.WhenAll(
    firstServiceResult,
    secondServiceResult,
    thirdServiceResult,
    fourthServiceResult,
    fifthServiceResult);
return VeryImportantObject.Parse(
    firstServiceResult.Result,
    secondServiceResult.Result,
    thirdServiceResult.Result,
    fourthServiceResult.Result,
    fifthServiceResult.Result);
}

To by było na tyle. Jeśli chcesz dowiedzieć się trochę więcej w trochę innej formie, to zapraszam Cię 22.05.2018 o godzinie 11:00 na WEBinarium, które będę miał przyjemność prowadzić.

najwięcej ofert html

Zdjęcie główne artykułu pochodzi z unsplash.com.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/await-event-czyli-sledzic-zdarzenia-uzyciem-taskcompletionsource/" order_type="social" width="100%" count_of_comments="8" ]