Część z nas jest mniej lub bardziej leniwa. Części może przeszkadzać taki zapis, a części nie.
Szczególnie część, gdzie powtarzają się różnego rodzaju serwisy i repozytoria (@7-@14) oraz (@21-@27). Co czynić, jak zrobić to samo za mniej? Konwencje i autoskanowanie assembly’ów. Jak? Poniżej prosta ściągawka: Continue reading
wzorce
Zamień bóla na enuma
Dlaczego zamienić? Moim zdaniem czytelniej i jasno sformułowana myśl i łatwiej zrozumieć. Nie chodzi o prosty przypadek, gdzie zamiana polegałaby na zamianie true/false na MyEnum.True/MyEnum.False – nie nie, to byłoby szaleństwem. Ale może od razu do kodu, bo czas nagli dzisiaj.
Pierwszy przypadek, wszystko działa jak należy:
Jakaś tam klasa filtrów, przyjmuje (@3) identyfikator, nazwę i bit, które oznacza że filtr będzie grupą lub nie (w zasadzie nie wiadomo, czym jest nie-grupa)
Teraz wykorzystanie:
Zasadniczo prosty kod, jest jakaś lista filtrów (@5). Następnie dodajemy filtry; pierwsze id, jakaś nazwa i true, potem id, nazwa i false. Osoba nowa w projekcie, lub nowa w tej części kodu na pierwszy rzut oka nie będzie wiedzieć co to oznacza. Następne dwie linijki są trochę bardziej rozgadane, bo jawnie mówią jak interpretować ostatniego bool’a.
Dalej szukamy tylko tych, gdzie IsGroup jest ustawione na true (@12). Jeszcze dalej wołamy metodę, która robi to samo, ale na podstawie parametru wejściowego (@18). Najczęściej gdy się tworzy takie metody nie zwraca się dużej uwagi na nazwę parametru, bo albo to R# wygenerował, a on robi dobrze, albo jest to jedna linijka i nie muszę się nad tym za bardzo skupiać. Rzeczywiście sama metoda jest prosta, trudno się pomylić do czego służy parametr i gdzie go wykorzystać.
Spójrzcie teraz na klienta tej metody, ma przesłać dwa parametry, listę filtrów (to proste), a potem flagę która oznacza group – group co?! Trzeba zajrzeć do ciała metody, żeby sprawdzić co oznacza ten parametr i jak mam go wykorzystać. Można też poprawić nazwę parametru lub (czuję obrzyd jak to piszę) napisać dokumentacja.
A co gdyby napisać kod w taki sposób:
Na scenę wchodzi tajemniczy FilterType:
Jak widać, tajemniczy bohater to enum, które jawnie definiuje jakiego rodzaju może być filtr:
- 0 – nie został określony, nikt nie wie, nikt nie słyszał. Jeśli w ten sposób został zainicjalizowany, to najprawdopodobniej nie zostanie nigdy wykorzystany
- 1- Typ grupowy
- 2- Typ pojedynczy (aha, teraz to trochę jaśniejsze)
Prosty klient klasy Filtr, może wyglądać jakoś tak:
Na początku ponownie tworzymy listę, tym razem nie trzeba zgadywać co oznacza true/false. Nie trzeba także uciekać się do używania nazwanych parametrów aby rozjaśnić intencje.
Następnie gdy wybieramy z kolekcji filtry (@12), znowu w jasny sposób mówimy o typie, który nas interesuje. I wreszcie na koniec metoda filtrująca (@18) także w jawny sposób definiuje parametry których wymaga do działania.
Co jeszcze dobrego płynie z przyjęcia enuma? Jeśli pojawi się nowy rodzaj, nie trzeba będzie wprowadzać kolejnego bool’a który będzie mówić, czy filtr jest czy nie jest danego rodzaju. Domyślnie filtr jest nie znanego typu, więc wszystkie podczas poszukiwać za grupowym/pojedynczym takie znaleziska się nie pojawią. Nawet gdy nazwa parametru będzie do bani, sam tym parametru sprzeda intencje programisty i użytkownik metody (@18) będzie wiedzieć jakiego rodzaju wartość ma przesłać, aby otrzymać interesująco go wynik.
I jeszcze na koniec: oczywiście że ciężko tworzyć enum’y do każdego wykorzystania bool’a. Równie ciężko jest pisać kod który działa i jest czytelny, bezbłędny czy zoptymalizowany – a jednak każdy się stara.
Miłego wieczoru!
Powiedz nie new
Co mają wspólnego ze sobą te dwa obrazki?
Na pewno nie pasują tutaj, to raz. dwa nie są to najładniejsze obrazki, a trzy to obcisłe spodnie nie zawsze wyglądają fajnie, nawet na kobietach. On na szczęście nie ma skarpetek do sandałów.
Ale o czym dzisiaj, dzisiaj o obcisłości po angielsku w programowaniu. Słowo tight będzie jednym z bohaterów wpisu. A nawet tight coupling, czyli coś mocno wiążącego. Co tak mocno wiąże w programowaniu? Moim zdanie new jest temu winny. Wiąże bowiem ono ze sobą klienta, klasę którą korzysta z new aby zaspokoić swoje potrzeby, oraz dostarczyciela usługi, czyli klasę, która jest w stanie zaspokoić tę potrzebę.
Ale hola-hola, pomyślicie, jak to programować bez new? Przecież to hańba, skandal, herezje! Tak się nie da! Jasne, ale może uda się trochę ukrócić to panowanie new w kodzie?
Na pierwszy ogień idzie klasyczna zależność klas wyższych od niższych, w przypadku asp.net mvc przykładem jest tutaj kontroler, który może zależeć od serwisu, który posiada jakieś repozytorium danych, a to korzysta z bazy danych.
Czyli coś takiego
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
1: public class HomeController
2: {
3: private HomeService service;
4:
5: public HomeController()
6: {
7: service = new HomeService();
8: }
9: }
10:
11: public class HomeService
12: {
13: private HomeRepository repository;
14:
15: public HomeService()
16: {
17: repository = new HomeRepository();
18: }
19: }
20:
21: public class HomeRepository
22: {
23: private ApplicationDatabase database;
24:
25: public HomeRepository()
26: {
27: database = new ApplicationDatabase();
28: }
29: }
30:
31: public class ApplicationDatabase
32: {
33: //internalls
34: }
Application database to pewnie EF, nH, albo inna warstwa nad SQL, a może zestaw zapytań SQL zapisanych w stringach – nie jest to ważne w tym przykładzie.
Ten kod zadziała i będzie świetnie, ale te klasy będą działać tylko i wyłącznie ze sobą, nigdy nie pozwolą innej klasie, która może być szybsza i w ogóle – wykonać kodu, albowiem tylko HomeService, tylko HomeRepository, tylko ApplicationDatabase może to zrobić.Taki zapis powinien wam się kojarzyć z hardcodowanymi wartościami, które są tylko odrobinę lepsze niż magic numbers. Bo jaka jest różnica pomiędzy tym co napisałem powyżej a var daysToAdd=49; I tak gdy będę chciał zmieć ilośc dni, muszę przepisać i przekompilować.
A można prościej się, i uwaga prościej nie oznacza mniej kodu, prościej oznacza prościej, ewentualnie możne oznaczać lepiej:
1: public class HomeController
2: {
3: private IHomeService service;
4:
5: public HomeController(IHomeService homeService)
6: {
7: this.service = homeService;
8: }
9: }
10:
11: public class HomeService : IHomeService
12: {
13: private IHomeRepository repository;
14:
15: public HomeService(IHomeRepository homeRepository)
16: {
17: repository = homeRepository;
18: }
19: }
20:
21: public interface IHomeService
22: {
23: }
24:
25: public class HomeRepository : IHomeRepository
26: {
27: private IDatabase database;
28:
29: public HomeRepository(IDatabase applicationDatabase)
30: {
31: database = applicationDatabase;
32: }
33: }
34:
35: public interface IHomeRepository
36: {
37: }
38:
39: public class ApplicationDatabase : IDatabase
40: {
41: //internalls
42: }
43:
44: public interface IDatabase
45: {
46: }
Ponownie, nie wnikam w szczegóły implementacyjne..
Samo ApplicationDatabase się nie zmienia, zależy może od jakichś systemowych dll, ale dla nas będzie ona ostateczną granicą. Co się zmieniło? Otóż klasy teraz mówią coś takiego: “Spoko spoko ziomuś, będę działać, będzie pan zadowolony, ale potrzebuje tego i tego – inaczej mój kolega kompilator, albo runtime (wersja gdy dasz nulla) da Ci popalić – taka jest sytuacja”. W jasny sposób deklarują swoje wymogi pracy. Znika z naszego kodu słówko new, gacie przestały być takie obcisłe. Oczywiście trzeba mieć jakiś system który te klocki dobrze połączy ze sobą, ja korzystam z Autofaca, ale w zasadzie każdy konterer jest dobry. Co najważniejsze kontenery można tak ustawić, aby ich konfiguracja była w zapisana w zewnętrznych plikach, daje do możliwość zamiany zachowania aplikacji bez potrzeby jest przekompilowywania – wystarczy zmiana w konfiguracji i restart.
Inne miejsce gdzie pojawia się new to przejścia pomiędzy warstwami aplikacji, gdy model stają się dto, lub innymi dto, albo się agregują i projektują jakieś dane na i z, lub zupełnie inaczej. Tutaj z pomocą przychodzi Automapper. Całą magię przepisywania nazwy (name) z klasy A na pełną nazwę (fullname) w klasie B można oddelegować do osobnej części i ponownie powiedzieć, że do poprawnej pracy potrzebuje takich i takich rzeczy. Automapper domyślnie jest statyczną klasą, ale można ją owrapować i wstrzykiwać jako serwis, co może ułatwić ewentualne testowanie.
Ostatnie co przychodzi mi do głowy, to sytuacja gdy naprawdę muszę stworzyć obiekt, bo np. nie lubisz automapować go z kilku innych, albo jest to zlepek kilku wywołań jakichś metod albo jeszcze inaczej. Wiadomo, każdy ma wyjątkowy projekt i wyjątkowe sytuacja, które nigdy i nikomu się jeszcze nie powtórzyły. Na takie sytuację przychodzą mi do głowy dwa mechanizmy: konstruktor i/lub fabryki. Konstruktor powinien zapewnić ci to, że jeśli wszystko się udało i programista tworzący klasę, którą właśnie niułujesz (takie słowo), to po zwróceniu sterowania do twojego kodu, klasa ta będzie w stu procentach gotowa do działania. Używania inicjalizatora jest fajnym skrótem, albo gdy zostanie dołożone nowe pole do klasy, nie masz pewności, że w każdym miejscu zostanie ono poprawnie ustawione. Dodając pole do klasy i na listę parametrów konstruktora masz pewność, że kompilator ci tego nie zapomni wytknąć. Jest to jedno rozwiązanie, które nie zmniejsza new w kodzie, ale nie zmniejsza ciasnowatości (znowu takie słowo) spodni. Lepszym podejściem są fabryki, które wezmą tę robotę na siebie. Znowu rozwiązanie, które spowoduje ze kodu nie będzie mniej, ale umożliwi napisanie kodu który nie musi być mocno związany z typem, fabryki mogą zwracać obiekty przez interfejsy, znowu więcej kody, za luźne gacie. Dodatkowo obowiązek posiadania wiedzy o tworzeniu klas wynikowych zostanie oddelegowany do fabryk. Fabryki możecie podawać jako wasze ulubione wzorce projektowe na rozmowach o pracę
Jeśli macie inne pomysły na to jak pozbyć się new, chętnie je poznam. Jednocześnie możecie się zastanawiać czemu to robić? Po pierwsze w luźnych spodniach lepiej się chodzi, ale luźne tak bez przesady. Po drugie, moim zdaniem kod staje prostszy w utrzymaniu. Trudniej w takim kodzie o wycieki, tak w .net można zrobić wycieki pamięci. Po kolejne, stajecie się bardziej pro. Kod jest otwarty na modyfikacje i rozszerzenia, ułatwia testowanie, jest ładny i miły w dotyku. Jeśli tego nie czujecie, to nic na siłę. Ja po prostu musiałem to napisać – jeśli mogę unikam new.
ps. A miał to być krótki wpis.
Opisz bibliotekę wartą poznania i napisz dlaczego to właśnie AutoMapper

Automapper – czytając kilka komentarzy pod poprzednim postem wywołuje nie małe emocje. Ja jednak nadal uważam go za dobre i warte poznania narzędzie. Dodatkowo obiecałem, że napiszę więcej niż parę słów o nim, także do dzieła!
Automapper autorem jest Jimmy Bogard i jeśli miałbym opisać jego funkcjonalność swoimi słowami to działałby tak:
zapomnijcie o ręcznym przepisywanie wartości z Foo.A do Goo.A – pozwólcie tę pracę wykonać komputerowi.
Jeśli nie wierzycie w czary, to automapper jest jedną z tych bibliotek która przywróci wam wiarę.”
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
1: public class Book
2: {
3: public Book(BookVM source)
4: {
5: this.Id = source.Id;
6: this.Title = source.Title;
7:
8: }
9: public long Id { get; set; }
10: public string Title { get; set; }
11: }
12:
13: public class BookVM
14: {
15: public BookVM(Book source)
16: {
17: this.Id = source.Id;
18: this.Title = source.Title;
19: }
20:
21: public long Id { get; set; }
22: public string Title { get; set; }
23: }
Ten sposób powoduje, ze obie klasy muszą o sobie wiedzieć, a skoro tak to, po co je rozdzielać, po co mapować, po co w ogóle dwie osobne klasy, źle!.
Dzięki AM można rozdzielić te dwie klasy i korzystać z nich zupełnie nie zależnie:
1: namespace AutoMapper
2: {
3: class Program
4: {
5: static void Main(string[] args)
6: {
7: // initialize AM
8: Mapper.CreateMap<Book, BookVM>();
9: Mapper.CreateMap<BookVM, Book>();
10:
11: // somewhere in code
12: var src = new Book { Id = 3, Title = "See sharp in 24" };
13: var dst = Mapper.Map<BookVM>(src);
14:
15: }
16: }
17:
18: public class Book
19: {
20: public long Id { get; set; }
21: public string Title { get; set; }
22: }
23:
24: public class BookVM
25: {
26: public long Id { get; set; }
27: public string Title { get; set; }
28: }
29: }
Jak zauważycie wszędzie podaje mapowanie obu kierunkach, a korzystam tylko z jednego. Nie jest to konieczne, powstało to na wypadek gdybym napisał jakiś kod który będzie z tego korzystać i zostało. W przypadku powyżej linijka 8 jest zbędna i można się jej spokojnie pozbyć. Podobnie będzie w przykładach poniżej.
Efekt działania programu i wartość dst:
Raz zdefiniowane mapowanie można wykorzystać później, jeśli jakaś klasa będzie zawierać nasze książki AM skorzysta już z wcześniej zdefiniowanych reguł:
1: using System.Collections.Generic;
2:
3: namespace AutoMapper
4: {
5: class Program
6: {
7: static void Main(string[] args)
8: {
9: // initialize AM
10: Mapper.CreateMap<Book, BookVM>();
11: Mapper.CreateMap<BookVM, Book>();
12: Mapper.CreateMap<Person, PersonViewModel>();
13: Mapper.CreateMap<PersonViewModel, Person>();
14:
15: //somewhere later in code
16: var p = new Person
17: {
18: Id = 1,
19: FirstName = "Jarek",
20: Books =
21: new List<Book>
22: {
23: new Book { Id = 1, Title = "Automapper in 24" },
24: new Book { Id = 2, Title = "Dependency Injection in 24" },
25: new Book { Id = 3, Title = "See sharp in 24" },
26: }
27: };
28: var vm = Mapper.Map<PersonViewModel>(p);
29: }
30: }
31:
32: public class Person
33: {
34: public long Id { get; set; }
35: public string FirstName { get; set; }
36: public List<Book> Books { get; set; }
37: }
38:
39: public class PersonViewModel
40: {
41: public long Id { get; set; }
42: public string FirstName { get; set; }
43: public List<BookVM> Books { get; set; }
44: }
45:
46: public class Book
47: {
48: public long Id { get; set; }
49: public string Title { get; set; }
50: }
51:
52: public class BookVM
53: {
54: public long Id { get; set; }
55: public string Title { get; set; }
56: }
57: }
Po wykonaniu linii 28 vm będzie wyglądać tak:
Jeśli któreś z pól nazywa się inaczej, można machnąć jedno linijkową instrukcję, która wyjaśni że Name to FirstName, np.
1: public class Book
2: {
3: public long Id { get; set; }
4: public string Title { get; set; }
5: }
6:
7: public class BookVM
8: {
9: public long Id { get; set; }
10: public string Title { get; set; }
11: }
12:
13: public class Person
14: {
15: public long Id { get; set; }
16: public string FirstName { get; set; }
17: public List<Book> Books { get; set; }
18: }
19:
20: public class PersonViewModel
21: {
22: public long Id { get; set; }
23: public string Name { get; set; }
24: public List<BookVM> Books { get; set; }
25: }
26:
27: static void Main(string[] args)
28: {
29: // initialize AM
30: Mapper.CreateMap<Book, BookVM>();
31: Mapper.CreateMap<BookVM, Book>();
32:
33: Mapper.CreateMap<Person, PersonViewModel>()
34: .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName));
35: Mapper.CreateMap<PersonViewModel, Person>()
36: .ForMember(d => d.FirstName, o => o.MapFrom(s => s.Name));
37:
38: //somewhere later in code
39: var p = new Person { FirstName = "jarek", Id = 1 };
40: var x = Mapper.Map<PersonViewModel>(p);
41: }
Zmienna x będzie miała taką wartość (co mam nadzieję już nikogo nie będzie dziwić)
Konwertować możemy także tylko część właściwości, np. z klasy person można wyciągnąć tylko kolekcję książek. Oczywiście można to zrobić na dwa sposoby, ręcznie (bleh) i AM (yeah):
1: static void Main(string[] args)
2: {
3: // initialize AM
4: Mapper.CreateMap<Book, BookVM>();
5: Mapper.CreateMap<BookVM, Book>();
6:
7: Mapper.CreateMap<Person, PersonViewModel>()
8: .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName));
9: Mapper.CreateMap<PersonViewModel, Person>()
10: .ForMember(d => d.FirstName, o => o.MapFrom(s => s.Name));
11:
12: Mapper.CreateMap<Person, List<BookVM>>()
13: .ConvertUsing(x => Mapper.Map<List<BookVM>>(x.Books));
14:
15: //somewhere later in code
16: var p = new Person
17: {
18: Id = 1,
19: FirstName = "Jarek",
20: Books =
21: new List<Book>
22: {
23: new Book { Id = 1, Title = "Automapper in 24" },
24: new Book { Id = 2, Title = "Dependency Injection in 24" },
25: new Book { Id = 3, Title = "See sharp in 24" },
26: }
27: };
28:
29: var books = Mapper.Map<List<BookVM>>(p);
30:
31: }
W book będzie to czego oczywiście oczekujemy:
Najważniejsza część kodu w tym przypadku ukryła się w linijkach 12 i 13.
W przypadku gdy typy są do siebie trochę mniej pasujące? Np. coś co nancy dostarcza “z pudełka”, czyli string rozdzielony separatorami do kolekcji obiektów – hę? Prosta sprawa żeby mieć tak samo bez nancy:
1: using System.Collections.Generic;
2:
3: namespace AutoMapper
4: {
5: using System;
6: using System.Linq;
7:
8: class Program
9: {
10: static void Main(string[] args)
11: {
12: // initialize AM
13: Mapper.CreateMap<string, List<BookVM>>()
14: .ConvertUsing<StringToBookViewModel>();
15:
16: string booksnames = "Lessie, Pinokio, Guliwer";
17: var booksvm = Mapper.Map<List<BookVM>>(booksnames);
18: }
19: }
20:
21: public class StringToBookViewModel : ITypeConverter<string, List<BookVM>>
22: {
23: public List<BookVM> Convert(ResolutionContext context)
24: {
25: var source = context.SourceValue as string;
26: var titles = source.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
27: var result = new List<BookVM>();
28: titles.ToList().ForEach(x => result.Add(new BookVM { Title = x, Id = -1 }));
29: return result;
30: }
31: }
32:
33:
34: public class BookVM
35: {
36: public long Id { get; set; }
37: public string Title { get; set; }
38: }
39: }
Nie będzie dla nikogo zaskoczeniem taki widok:
Jeśli ktoś się zastanawia co daje string na obiekty, to może warto się zastanowić nad np. obsługą tagów. Gdy ktoś dodaje listę tagów do posta, wpisu, książki, etc, dostajemy od użytkownika ciąg stringów, taki konwerter jak powyżej może podmienić pojedyncze stringi na pełnoprawne obiekty z poprawnym ID – jak? W poprzednim wpisie podałem sposób jak połączyć moc DependencyInjection oraz Automappera, wystarczy dla takiego konwertera dostarczyć repozytorium i w trakcie konwersji można sprawdzić czy dany tag istnieje czy nie; i wreszcie ustawić id obiektu na poprawną wartość.
Z dobroci, które oferuje AM jest także możliwość zdefiniowania zachowania dla obiektów które są nullem, dzięki czemu można pozbyć się uciążliwej ifologi z kodu:
1: using System.Collections.Generic;
2:
3: namespace AutoMapper
4: {
5: using System;
6: using System.Linq;
7:
8: class Program
9: {
10: static void Main(string[] args)
11: {
12: // initialize AM
13: Mapper.CreateMap<Book, BookVM>();
14: Mapper.CreateMap<BookVM, Book>();
15:
16: Mapper.CreateMap<Person, PersonViewModel>()
17: .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName))
18: .ForMember(d => d.Books, o => o.NullSubstitute(new List<BookVM>()));
19:
20: Mapper.CreateMap<PersonViewModel, Person>()
21: .ForMember(d => d.FirstName, o => o.MapFrom(s => s.Name));
22:
23: var p = new Person { FirstName = "jarek", Id = 1 };
24: var x = Mapper.Map<PersonViewModel>(p);
25: }
26: }
27:
28:
29: public class Person
30: {
31: public long Id { get; set; }
32: public string FirstName { get; set; }
33: public List<Book> Books { get; set; }
34: }
35:
36: public class PersonViewModel
37: {
38: public long Id { get; set; }
39: public string Name { get; set; }
40: public List<BookVM> Books { get; set; }
41: }
42:
43: public class Book
44: {
45: public long Id { get; set; }
46: public string Title { get; set; }
47: }
48:
49: public class BookVM
50: {
51: public long Id { get; set; }
52: public string Title { get; set; }
53: }
54: }
Dzięki NullSubstitute zamiast nulla w książkach mamy pustą kolekcję po której możemy spokojnie iterować.
Dopiero teraz (wow!) zauważyłem, że AM domyślnie stosuje taką strategię. Propopnuje wrócić do trzeciego obrazka i sprawdzić. Obiekt klasy Person ma null na kolekcji książek, ale już PersonViewModel ma pustą kolekcję książek. Całkiem możliwe że Jimmy też nie lubi ifologi. Tak czy siak, warto znać opcję NullSubstitute.
Przykłady powyżej to najczęściej używane konstrukcje używane przeze mnie w moich projektach domowych jak i komercyjnych, jak do tej pory rozwiązywały one wszystkie nasze problemy. Co więcej jeśli czasem coś nie działało jak powinno zawsze mogliśmy w najgorszym wypadku wrócić do ITypeConverter i napisać własne mapowanie od a do z, jednak nadal zachowując standardy AM.
Mogę się założyć, że są jeszcze cuda, które umożliwia AM a o których nie wiem, ale nawet teraz jest to jedno lepszych narzędzi, z których na pewne nie przestanę korzystać.
Jeśli macie uwagi lub pytania, chętnie wdam się w dyskusje.
RX extensions w przykładach
RxExtension – to biblioteka od Microsoftu ułatwiająca programowanie asynchroniczne. Opiera się na istniejących interfejsach IObservable oraz IObserver.
W RX wiadomości są traktowane jako strumienie danych, do których należy się przypiąć i reagować na pojawienie się nowej wiadomości. Najnowsza wersja ma już cyferkę 2, ale nie jest jeszcze oznaczona jako stabilna. Ja do nauki wykorzystałem wersję 1 oraz książeczkę dostępną na stronie RX – Dev Labs Hands On. Na Channel9 są jakieś filmy na temat RX. Poniżej pokaże kilka przykładów i opis jak korzystać z RX, źródła pochodzą z książki, będą dostępne razem z książką i projektem MSVC. Drobna uwaga co do książki i źródeł: w książce podane są dwa pliki do których należny dodać referencję, aktualna wersja RX dostarcza tylko jednej biblioteki – System.Reactive, natomiast System.CoreEx jest już w systemie. Ja korzystałem z .net 4.0. Podobnie kilka metod wymienionych w książce nie jest już dostępne w związku z czym zostały zamienione na inne dostępne i spełniające te same wymagania.
Zaczynamy od poznania interfejsów:
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
1: static void Main(string[] args)
2: {
3: IObservable<int> source = Observable.Empty<int>();
4: IObserver<int> handler = null;
5:
6: IDisposable subscription = source.Subscribe();
7: Console.WriteLine("Press ENTER to unsubscribe and dispose");
8: Console.ReadLine();
9:
10: subscription.Dispose();
11: }
RX opiera się o dwa interfejsy IObservable oraz IObserver. Klasa Observable pochodzi System.Reactive.Linq. Kod powyżej nie jest zbytnio porywający, ale pokazuje, że po subskrypcji do źródła danych otrzymujemy instancję IDisposable, którą trzeba będzie wywalić do kosza po zakończeniu pracy. Później będzie to zrobione przy użyciu mechanizmu using.
Interfejs IObserver definiuje trzy metody, które muszę zostać zdefiniowane:
1: public interface IObserver<in T>
2: {
3: void OnCompleted();
4: void OnError(Exception error);
5: void OnNext(T value);
6: }
- OnCompleted: źródełko wyschło
- OnNext: nowa informacja
- OnError: coś poszło nie tak
Podczas dopisywania się do obiektu, który ma dostarczać informacji można wykorzystać wyżej wymieniony interfejs lub skorzystać z wyrażeń lambda. To drugie podejście jest prostsze, dodatkowo nie wymagane jest definiowanie dla wszystkich metod z interfejsu.
Na początek pełna deklaracja:
1: static void Main(string[] args)
2: {
3: IObservable<int> source = Observable.Empty<int>();
4:
5: IDisposable subscription = source.Subscribe(
6: x=> Console.WriteLine("Has new value {0}", x),
7: ex=>Console.WriteLine("Exception caught {0}", ex.Message),
8: ()=>Console.WriteLine("No more items")
9: );
10:
11: Console.WriteLine("ENTER to dispose");
12: subscription.Dispose();
13: }
W tym przypadku od razu zostanie wywołana metoda OnCompleted, ponieważ źródło danych jest puste. Po zakończeniu pracy zwalniamy zasoby przez wywołanie Dispose na obiekcie zwróconym po subskrypcji. Co ważne, aby móc korzystać z rozszerzeń dla RX należy dodać referencję oraz using do System.Reactive.Linq.
Jednoelementowa kolekcja intów:
1: static void Main(string[] args)
2: {
3: IObservable<int> source = Observable.Return(42);
4:
5: var subscriber = source.Subscribe(
6: x=>Console.WriteLine("Value: {0}",x),
7: ex=>Console.WriteLine("Exception: {0}", ex.Message),
8: ()=>Console.WriteLine("end!")
9: );
10:
11: Console.WriteLine("ENTER to dispose");
12: subscriber.Dispose();
13: }
Kod powyżej zwróci raz wartość 42 (sens życia), a następnie zakończy poprzez wywołanie OnCompleted. Następnie zwolnienie zasobów. Ponownie wykorzystanie Reactive.Linq do stworzenia jednoelementowej kolekcji.
Aby zrobić coś ciekawszego można podmienić kod definiujący źródło na coś takiego:
1: IObservable<int> source = Observable.Range(5, 7);
Spowoduje do wygenerowanie cyfr od 5 do 11, następnie zakończy przez OnCompleted.
Aby nie zanudzać prostymi przykładami, RX umożliwia stworzenie obserwowanej pętli for, robi się to w taki sposób:
1: static void Main(string[] args)
2: {
3: IObservable<int> source = Observable
4: .Generate(0, // initial state
5: i => i < 10, // condition
6: i => i + 1, // iteration step
7: i => i * i); // iteration operation
8:
9: using (var s = source.Subscribe(
10: x => Console.WriteLine(x) // only the working stuff will be handled
11: // no errors and no exceptions
12: // no information about sequence finish either
13: )) { };
14: }
Coraz ciekawiej co nie?
Zaczynamy od 0, następnie warunkiem jest i mniejsze od 10, w każdym kroku i będzie zwiększane co 1, a wynikiem operacji ma być i*i, wynik mnożenia nie jest zapisywany do i, tylko zwracany jako wynik. W przeciwnym wypadku pętla skończyła by się za wcześnie.
Podczas dopinania się do źródła definiujemy tylko metodę OnNext, która przyjmuje jeden parametr, w ten sposób nie zostaniemy powiadomieni o skończeniu się danych lub o wystąpieniu błędu. Dodatkowo wykorzystany zostanie mechanizm using, na który spadnie odpowiedzialność zwolnienia zasobów z IDisposable.
Jeśli zastanawialiście się jak to możliwe że pętla nie zakończy skoro w klamrach using nic nie ma, nie dziwcie się, dla mnie na początku to także było dziwne. Ale jeśli odpalić debuggera i sprawdzić wątki, to wszystkie wywołania są dokonywane z głównego wątku. Dopiero po skończeniu się zasobów w IObservable, zostanie wykonany kod z klamerek.
Żeby nie było nudno, teraz przykład z normalniejszym kodem, takim który jest w klamerkach:
1: static void Main(string[] args)
2: {
3: var source = Observable.Generate(
4: 0,
5: i => i < 10,
6: i => i + 1,
7: i => i * i,
8: i => TimeSpan.FromSeconds(i)
9: );
10:
11: using (var s = source.Subscribe(
12: x => Console.WriteLine("next: {0}", x),
13: ex => Console.WriteLine("exception: {0}", ex.Message),
14: () => Console.WriteLine("no more")))
15: {
16: Console.WriteLine("ENTER");
17: Console.ReadLine();
18: }
19: }
Dwie zmiany zostały wprowadzone; dodałem 1 sekundowe opóźnienie w generowaniu wartości i, co powoduje że wywoływania zaczynają wreszcie być prawdziwie asynchroniczne. To powoduje, potrzebę umieszczenie Console.ReadLine w ciele using, w przeciwnym wypadku, nie zdążymy odczytać nawet pierwszej wartości. Walnięcie ENTER w trakcie działania dema, spowoduje jego zakończenie.
Okno wątków w debuggerze, pokazują że podczas wołania OnNext, wykorzystywany jest dodatkowy Worker Thread, stworzony przez RX.
Teraz drobna zmiana, do naszej konsoli dołożona zostanie prosta forma (WinForms – przykłady z książki są na tym oparte, więc i ja z tego skorzystałem). Będzie służyć za pośrednika do źródła danych, załóżmy sobie że jest taki kod:
1: static void Main(string[] args)
2: {
3: var label = new Label();
4: var form = new Form
5: {
6: Controls = { label }
7: };
8:
9: var moves = Observable.FromEventPattern<MouseEventArgs>(form, "MouseMove");
10:
11: using (moves
12: .Subscribe(
13: x => label.Text = x.EventArgs.Location.ToString(),
14: ex => label.Text = ex.Message,
15: () => label.Text = "Mouse is over?!" ) )
16: {
17: Application.Run(form);
18:
19: }
20: }
Pojawia się tutaj prosta forma z tekstem do którego będziemy zapisywać informacje o aktualnym położeniu myszy. To co interesujące znajduje się w linijce 9, gdzie tworzymy źródło danych na podstawie przychodzących zdarzeń z formy, następnie zapisujemy się do nich. Ponieważ w tym przykładnie nie stosowane są żadne specjalne opóźnienia (o nich poniżej), nie potrzeba tutaj wykorzystywać Dispatchera czy obiekty synchronizacji pomiędzy wątkami. W okienku wątków widać, że każde wywołanie metody OnNext realizowane jest w głównym wątku UI, dlatego bezpieczna jest modyfikacja labelki.
1: static void Main(string[] args)
2: {
3: var label = new Label();
4: var form = new Form
5: {
6: Controls = { label }
7: };
8:
9: var moves = Observable.FromEventPattern<MouseEventArgs>(form, "MouseMove");
10:
11: using (moves
12: .Subscribe(
13: x => label.Text = x.EventArgs.Location.ToString(),
14: ex => label.Text = ex.Message,
15: () => label.Text = "Mouse is over?!" ) )
16: {
17: Application.Run(form);
18: }
19: }
Najciekawsza jest linia 9, gdzie wydarzenia generowane przez formę/mysz są definiowane jako źródło danych, a następnie w linii 13 wartość ta zostaje wyłuskana z argumentów oraz zapisana w labelce na formatce.
Kolejny przykład będzie rozwinięciem tego powyżej:
1: static void Main(string[] args)
2: {
3: var textbox = new TextBox();
4: var form = new Form
5: {
6: Controls = { textbox }
7: };
8:
9: var moves = Observable
10: .FromEventPattern<MouseEventArgs>(form, "MouseMove")
11: .Select(e => e.EventArgs.Location);
12:
13: var texts = Observable
14: .FromEventPattern<EventArgs>(textbox, "TextChanged")
15: .Select(e => (e.Sender as TextBox).Text);
16:
17: var msubs = moves.Subscribe(x => Console.WriteLine("mouse position: {0}", x));
18: var ksubs = texts.Subscribe(x => Console.WriteLine("Textbox text: {0}", x));
19:
20: using (new CompositeDisposable(msubs, ksubs))
21: {
22: Application.Run(form);
23: }
24: }
Jak widać tutaj ponownie nasłuchujemy na wiadomości generowane przez ruch myszy, ale dodatkowo oczekujemy na wpisywany przez użytkownika tekst w formatce. Warto zauważyć, że w przypadku tekstu, źródłem danych nie jest formatka, ale sama kontrolka. W tym przykładzie pod uwagę bierzemy tylko pozytywne wywołania OnNext. Dodatkowo subskrypcja wykorzystuje projekcję (Select) i ostatecznej rozgrywce (17 i 18) otrzymujemy tylko takie wartości, które są dla nas najbardziej interesujące. Na sam koniec przykładu, wykorzystujemy klasę CompositeDisposable, która umożliwia wykorzystanie mechanizmu using na więcej niż jednym obiekcie IDisposable.
Kolejnym ciekawy rozszerzenie dla RX jest mechanizm Distinct, który wyśle powiadomienie tylko wtedy, gdy jest ono różne od poprzedniego.
1: var texts = Observable
2: .FromEventPattern<EventArgs>(textbox, "TextChanged")
3: .Select(e => (e.Sender as TextBox).Text)
4: .Do(e => Console.WriteLine("Before DistinctUntilChanged: {0}", e))
5: .DistinctUntilChanged();
Cały kod jest taki sam jak poprzednio, zmienia się tylko definicja źródła danych dla tekstu. Linia 4 pokazywać będzie, że wiadomości są przesyłane – metoda Do. Natomiast linia 5 zatrzyma ich dalszą propagację, gdy będę one takie same jak poprzednio otrzymane. Należy zwrócić uwagę podczas używania DistinctUntilChanged na kolejność jego użycia. Jeśli zostanie wywołany za wcześnie, kolejne eventy mogą nie zostać przesłane. W ramach ćwiczeń można wstawić go przed Select i zobaczyć co się dzieje. W tym przykładzie, pod uwagę brana będzie zawartości kontrolki w textboxie. A więc, gdy zaznaczona zostanie literka i zostanie zastąpiona przez taką samą, obiekt nasłuchujący nie zostanie powiadomiony o takie zmianie.
Następne rozszerzenie dla RX to Throttle, który powoduje że powiadomienia są przesyłane z pewnym opóźnieniem, każda nowa aktualizacja na źródle resetuje ten licznik. Dla przykładu, gdy chcemy udostępnić mechanizm który działa jak słownik z podpowiedziami, aby nie włączać wyszukiwania dla każdej wpisanej literki, można dodać małe opóźnienie rzędu kilkudziesięciu milisekund, które dla użytkownika będzie nie zauważalne, a jego wykorzystanie spowoduje zmniejszenie wykorzystania zasobów, ponieważ ilość generowanych zapytać będzie mniejsza. Poniżej przykład definicji takiego źródła danych:
1: var moves = Observable
2: .FromEventPattern<MouseEventArgs>(form, "MouseMove")
3: .Select(e => e.EventArgs.Location)
4: .Throttle(TimeSpan.FromMilliseconds(100));
5:
6: var texts = Observable
7: .FromEventPattern<EventArgs>(textbox, "TextChanged")
8: .Select(e => (e.Sender as TextBox).Text)
9: .Throttle(TimeSpan.FromMilliseconds(500))
10: .DistinctUntilChanged();
W pierwszy przypadku, informację o pozycji myszy otrzymamy gdy użytkownik okres około 100 ms nie będzie poruszać myszą. Należy być świadomym, że nie dostaniemy wszystkich wartości pośrednich, tylko ostatnią pozycję myszy. W drugim przypadku, jest dodatkowe założenie, użytkownik nie może zmieniać wartości tekstu przez około 500ms, a dodatkowo wartość musi być inna niż poprzednia – modyfikator DistinctUntilChanged. W bardzo prosty sposób ograniczyć można ilość powiadomień, które obsłużyć musi obiekt obsługujący kontrolkę.
Wcześniej mówiłem o tym, że będzie o pełniejszej asynchroniczności oraz o kontrolkach, czas najwyższy na rozwiązanie tej tajemnicy. Jeśli wiemy, że obsługa zdarzeń będzie wymagała interakcji z kontrolkami (lub kolekcjami – np. ObservableCollection w WPF) wymagającymi synchronizami z głównym wątkiem UI należy wykorzystać odpowiednie rozszerzenie, które umożliwia obserwację zdarzeń wykonywaną na głównym wątku aplikacji – zamieszane? Kod was oświeci:
1: static void Main(string[] args)
2: {
3: TextBox t1 = new TextBox();
4: Label l1 = new Label { Left = t1.Width + 20 };
5: Form f1 = new Form
6: {
7: Controls = { t1, l1 }
8: };
9:
10: var source = Observable
11: .FromEventPattern(t1, "TextChanged")
12: .Throttle(TimeSpan.FromMilliseconds(250))
13: .Select(x=>(x.Sender as TextBox).Text)
14: .DistinctUntilChanged();
15:
16: using (source
17: .ObserveOn(WindowsFormsSynchronizationContext.Current)
18: .Subscribe(x => l1.Text = x))
19: {
20: Application.Run(f1);
21: }
22: }
Jak widać, źródło danych działa teraz prawdziwie asynchronicznie (wykorzystanie Throttle), aby móc wykonać kod zdefiniowany w OnNext (linia 18) należy wcześniej powiedzieć że obsługa (obserwacja) ma nastąpić w WindowsFormsSynchronizationContext.Current (dla WPF to będzie Application.Current.Dispatcher lub Dispatcher.Current) – lub w implementacji wykorzystać mechanizm synchronizacji, jak zawsze to zależy od potrzeb implementacji. W takiej sytuacji źródło działa w osobnym wątku, ale powiadomienia i ich obsługa nastąpi w wątku głównym, gdzie można modyfikować UI.
Następny przykład jest trochę dłuższy, pokazuje jak wykorzystać RX w połączeniu z serwisami implementowanymi w WCF, dokładniejsze omówienie poniżej:
1: private static void Main()
2: {
3: var service = new DictServiceSoapClient("DictServiceSoap");
4: Func<string, string, string, IObservable<DictionaryWord[]>> match = Observable
5: .FromAsyncPattern<string, string, string, DictionaryWord[]>(
6: service.BeginMatchInDict, service.EndMatchInDict);
7:
8: Func<string, IObservable<DictionaryWord[]>> matchInWordNetByPrefix = term => match("wn", term, "prefix");
9:
10:
11: var t1 = new TextBox();
12: var f1 = new Form
13: {
14: Controls = {t1}
15: };
16:
17:
18: IObservable<string> textSource = Observable
19: .FromEventPattern<EventArgs>(t1, "TextChanged")
20: .Throttle(TimeSpan.FromMilliseconds(250))
21: .Select(x => ((TextBox) x.Sender).Text)
22: .Where(x => x.Length > 0)
23: .DistinctUntilChanged();
24:
25: IDisposable formRequest = textSource
26: .Finally(()=>Console.WriteLine("Finalized"))
27: .Subscribe(x => matchInWordNetByPrefix(x)
28: .Finally(() => Console.WriteLine("Finalized: {0}", x))
29: .Subscribe(words =>
30: {
31: Console.WriteLine("{0} - {1}", x, words.Count());
32: foreach (DictionaryWord w in words)
33: {
34: Console.Write("{0},", w.Word);
35: }
36: Console.WriteLine("n*******************************************");
37: },
38: ex =>
39: {
40: Console.WriteLine("n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
41: Console.WriteLine(ex.Message);
42: Console.WriteLine("n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
43: }));
44:
45:
46: using (formRequest)
47: {
48: Application.Run(f1);
49: }
50: }
Zacznijmy od początku:
Linia 3 definiuje nowe proxy do serwisu WCF, nie istotne z punktu widzenia RX. Następnie linia 4 definiuje zmienną typu Func, będzie to delegat przyjmujący jako dane wejściowe trzy stringi, a zwracający tablicę wyrazów słownikowych (ach ten mój angielski). Dalej (ciągle w linii 4) przypisujemy do tego delegata metodę z RX wykorzystując wcześniej zdeklarowany serwis WCF, a dokładniej dwie jego metody BeginMatchInDict oraz EndMatchInDict. Są to dwie metody, które umożliwiają asynchroniczne wykorzystanie serwisu. Podczas definiowania dostępu do serwisu WCF zaznaczona została opcja generowania zapytań asynchronicznych. Dalej, linia 8 to ponowne zdefiniowanie delegata, tym razem będzie on przyjmować tylko jeden argument – string, który będzie zawierać właściwe dla nas zapytanie, zwracany typ nie ulegnie zmianie. Pozostałe dwa stringi zostają przypisane na stałe. Ich wartość nie jest istotna dla RX. Teraz możemy korzystać asynchronicznie z WCF podając tylko jednego stringa podczas zapytania. Potem “normalna” definicja formatki, później źródełko z danymi; pierwsze to textbox, drugie to serwis WCF który na podstawie wysłanej części słowa, zwraca tablicę wyrazów, który mogą je dokończyć (np. “yellow a“: yellow adder’s tongue,yellow ageratum,yellow asphodel,yellow avens). Metoda Finally posłuży aby wykonać jakiś kod w momencie zwalniania zasobów zwróconych przez Subscribe. Kod z linijki 28 zostanie wykonany po tym, gdy wszystkie pasujące słowa zostaną wypisane na ekran. Linijka 26 zostanie wywołana po zamknięciu formatki, czyli zakończeniu using z linii 46.
Jeśli podczas zabawy z tym przykładem zauważycie, że możliwe jest zdefiniowanie nowego pytania do serwisu WCF zanim, przyjdzie odpowiedź na wcześniejsze – to macie racje (za .net rocks – golfclap for you). Występuje tu taka sytuacja. Na rozwiązanie tego problemu zaproponowano dwa podejścia, dla mnie Switch jest (będzie poniżej) czytelniejszy i zrozumiały. W udostępnionym projekcie są oba rozwiązania.
1: static void Main(string[] args)
2: {
3: var t1 = new TextBox();
4: var l1 = new ListBox { Top = t1.Height + 10, Height = 250, Width = 150 };
5: var f1 = new Form
6: {
7: Controls = { t1, l1 }
8: };
9:
10: var textSource = Observable
11: .FromEventPattern<EventArgs>(t1, "TextChanged")
12: .Throttle(TimeSpan.FromMilliseconds(50))
13: .Select(x => (x.Sender as TextBox).Text)
14: .Where(x => x.Length >= 3)
15: .DistinctUntilChanged()
16: .Do(Console.WriteLine);
17:
18: var service = new DictServiceSoapClient("DictServiceSoap");
19: var dictSource = Observable
20: .FromAsyncPattern<string, string, string, DictionaryWord[]>(service.BeginMatchInDict, service.EndMatchInDict);
21:
22: Func<string, IObservable<DictionaryWord[]>> matchInWordNetByPrefix = term => dictSource("wn", term, "prefix");
23:
24: var data = textSource
25: .Select(x => matchInWordNetByPrefix(x))
26: .Switch();
27:
28: using (data
29: .ObserveOn(WindowsFormsSynchronizationContext.Current)
30: .Subscribe(w =>
31: {
32: l1.Items.Clear();
33: l1.Items.AddRange(w.Select(word => word.Word).ToArray());
34: },
35: ex =>
36: {
37: MessageBox.Show(ex.Message);
38: }))
39:
40: Application.Run(f1);
41: }
Początek bez zmian, dopiero podczas wykorzystywania tego co wpisze użytkownik i przesyłania tego do serwisu WCF, pojawiają się pierwsze zmiany. Na początku (linia 24) czekamy aż, użytkownik coś wpisze, a następnie wysyłamy to do serwisu. Jeśli w między czasie użytkownik zmieni coś w kontrolce, ponownie wyślemy zapytanie, ale z nową wartością. Aby nie obsługiwać wyników dla poprzedniego zapytania wykorzystana zostaje metoda Switch, której działanie polega na zwróceniu wyniku tylko z ostatniego zapytania, wszystkie poprzednie zostają anulowane. Reszta zmian to tylko kosmetyka, w tym przykładnie wyniki pojawiać się będą w kontrolce na formatce.
Na moje oko RX wydaje się być interesującym rozszerzeniem, umożliwiającym uproszczenie i kodu, a już na pewno metody jak Throttle czy DistinctUntilChanged uczynią aplikacje mniej zasobożernymi. Jak z każdą nowo poznaną technologią trzeba trochę czasu, aby poznać wszystkie za i przeciw. Ja na razie jestem na etapie wow. Poprawcie mnie jeśli się mylę, ale RX jest wyczesany.
Projekt, źródła i książka dostępne pod następującym adresem:
Projekt: https://bitbucket.org/jstadnicki/rx-examples
Git: https://bitbucket.org/jstadnicki/rx-examples.git