Serializacja binarna wewnątrz aplikacji Dietphone « Pabloware + Windows Phone

Pabloware + Windows Phone

Niezależny blog firmy Pabloware o systemie Windows® Phone 7

Serializacja binarna wewnątrz aplikacji Dietphone

Za wyborem ręcznej serializacji binarnej przemawia fakt, że projektowana aplikacja należy do tego rodzaju narzędzi, które uruchamia się na chwilę, ale wielokrotnie w ciągu dnia. W przypadku takich aplikacji bardzo ważne jest szybkie uruchamianie się aplikacji. Należy dodać, że w środowisku mobilnym nie mamy do dyspozycji takiej wydajności sprzętu, jak na komputerze biurkowym. Wybrany model serializacji pozwala oszczędzić czas.

Wadą tego rodzaju serializacji, z punktu widzenia programisty, jest konieczność pisania metod serializacyjnych dla każdej klasy. W przypadku projektowanej aplikacji nie był to jednak problem, ponieważ jej model nie zawiera wielu klas. W przeciwnym przypadku ręczna serializacja byłaby bardzo kłopotliwa i wykorzystalibyśmy, za pewną cenę wydajności, mechanizm refleksji .NET, likwidując powyższą kłopotliwość.

Jest natomiast prawdopodobne, że model będzie rozbudowywany. Tutaj wybór ręcznej serializacji również jest korzystny. Można bowiem dowolnie zdecydować co zrobić, kiedy plik danych pochodzi z wcześniejszej wersji aplikacji.

Automatyczne narzędzia serializacji, które potrafią radzić sobie w takich sytuacjach, czynią to za cenę przechowywania identyfikatorów każdej właściwości obiektu, przez co obniża się ich efektywność. Jeszcze gorszą efektywność, niż gotowe narzędzia do serializacji, oferowały bardziej klasyczne zewnętrzne narzędzia bazodanowe, dostępne na obraną platformę. Przez co nie zostały zastosowane. Należy przypomnieć, że obecnie nie jest dostępna na Windows Phone 7 wbudowana baza danych SQL.


Z wbudowanych w bibliotekę Silverlight mechanizmów serializacji, najszybsza okazała się serializacja przy użyciu mechanizmu DataContract do binarnej reprezentacji XML platformy .NET (nie jest to stricte serializacja binarna, jedynie binarne sformatowanie kodu XML). Brak jest natomiast w bibliotece Silverlight binarnego serializatora BinaryFormatter, znanego z pełnej wersji platformy .NET, więc serializację binarną trzeba zapewnić samemu lub korzystać z gotowych narzędzi zewnętrznych. Zastosowanie w aplikacji własnego mechanizmu serializacji binarnej, pozwoliło wczytać na telefonie komórkowym dane 843 produktów w czasie 320 milisekund. Wobec trwania tej operacji przez 4500 milisekund, podczas użycia wspomnianego mechanizmu DataContract. Oszczędność wyniosła zatem 93% czasu.


Mechanizm serializacji jest odizolowany od reszty aplikacji. Założono możliwość wykorzystania go w innych aplikacjach na Windows Phone 7, więc rdzeń kodu serializacji jest całkowicie niezależny od warstwy Model aplikacji. Konieczne było jedynie dodanie bardzo prostych klas zapewniających metody serializacji konkretnych encji warstwy Model. Te klasy są już zależne od warstwy Model. Oczywiście sama warstwa Model jest całkowicie niezależna od mechanizmu przechowywania danych, w tym od serializacji binarnej. Poniżej znajduje się opis wszystkich elementów odpowiedzialnych za serializację w opisywanej aplikacji.

BinaryFile

Jest to abstrakcyjna klasa generyczna, która implementuje interfejs BinarySerializer. Odpowiada plikowi o określonej nazwie i wersji, w którym przechowywane są obiekty danego typu. Umożliwia zapis i odczyt z pliku listy wszystkich obiektów. Jej podklasy konkretne muszą zapewnić zapis i odczyt pojedynczych obiektów ze strumienia binarnego, są to bowiem metody abstrakcyjne tej klasy.

BinarySerializer i BinarySerializerExtensions

Generyczny interfejs BinarySerializer odpowiada za serializację pojedynczego obiektu.

Natomiast klasa BinarySerializerExtensions wprowadza rozszerzenia do klas BinaryWriter oraz BinaryReader (klasy platformy .NET), o możliwość zapisywania i odczytywania obiektów z tych klas, przy użyciu interfejsu BinarySerializer oraz ułatwia zapis i odczyt niektórych innych typów.

Konstrukty BinarySerializer i BinarySerializerExtensions bazują na bardzo ciekawej pracy Kevina Marshalla, w której rolą obiektów, implementujących interfejs IBinarySerializable, jest serializacja samych siebie. Stosując SRP i programowanie generyczne zmieniono nieznacznie to założenie, wprowadzając zasadę wydzielania serializacji obiektów do zewnętrznych klas, implementujących interfejs BinarySerializer.

BinaryStorage i BinaryStorageCreator

Klasa BinaryStorage pełni rolę adaptera, będąc podklasą BinaryFile i implementując przy jej użyciu interfejs Storage, który należy do warstwy Model aplikacji.

Natomiast klasa BinaryStorageCreator implementuje, należący do warstwy Model aplikacji interfejs StorageCreator. Korzysta przy tym z klasy StorageBuilder również wchodzącej w skład warstwy Model aplikacji. Jest to ciekawy przypadek, który zostanie szerzej omówiony.

Zacznijmy od porównania naiwnej wersji kodu z poprawioną wersją kodu. Obie wersje zostały uproszczone (uwzględniono tylko kategorie i produkty oraz pominięto przypisanie BinaryStreamProvider do BinaryFile), aby czytelnik mógł skupić się na istocie problemu.

Pierwsza wersja:

Naiwna wersja klasy BinaryStorageCreator (kod C#)

Druga wersja:

Poprawiona wersja klasy BinaryStorageCreator korzystająca z klasy StorageBuilder (kod C#)

Zamierzonym efektem było utworzenie instancji odpowiedniej podklasy klasy abstrakcyjnej BinaryStorage<T> na podstawie typu generycznego T. Problem częściowo wziął się stąd, że w danym kontekście programista chciał, żeby typ BinaryStorage<T> był dla kompilatora rodzicem typu CategoryBinaryStorage, który dziedziczy z BinaryStorage<Category>, podczas gdy dla kompilatora typ generyczny T może w tym samym kontekście równać się klasie Product, która poza wspólnym typem bazowym nie ma nic wspólnego z Category, a więc mielibyśmy odmienny typ BinaryStorage<Product> zamiast BinaryStorage<Category>. Jak widać klasa o innym typie generycznym jest po prostu inną klasą i kompilator nie może pozwolić na bezpieczne rzutowanie typów w takiej sytuacji. To na programiście spoczywa obowiązek upewnienia się w czasie wykonania programu, że tworzony jest obiekt właściwego typu generycznego. Choć jak pokażemy, można również w tym przypadku skorzystać z kontroli typów języka.

Obrany efekt utworzenia instancji odpowiedniej podklasy BinaryStorage<T>, na podstawie typu generycznego T, można uzyskać w naiwny sposób sprawdzając typ generyczny i tworząc na tej podstawie odpowiedni typ konkretny (jak w pierwszej wersji). Takie rozwiązanie oznacza jednak rezygnację z silnej kontroli typów języka C# i nie jest bezpieczne, ani przejrzyste. Po refactoringu udało się wypracować bezpieczne rozwiązanie, widoczne w drugiej wersji, które można zredukować do samego użycia operatora as. Użyto jednak dodatkowo operatora typeof w warunku, aby nie tworzyć nadmiarowych obiektów dla operatora as, a całość zamknięto w klasę, która poprzez swą konstrukcję, uniemożliwia utworzenie obiektu niewłaściwego typu. W najgorszym przypadku, kiedy programista rozbudowując program zapomni dodać wywołanie metody ProposeStorageForEntity, zobaczy wyjątek „Nie zaimplementowano” i żaden obiekt nie zostanie utworzony.

Jeszcze bardziej eleganckie i krótsze rozwiązanie polegałoby na przeładowaniu metody CreateStorage<T>, za pomocą wymuszenia minimalnej klasy bazowej typu generycznego T, przy użyciu operatora where. Niestety jednak przeładowanie metod różniących się tylko operatorem where nie działa w języku C#, choć można sądzić, że byłoby przydatne w wielu sytuacjach.

BinaryStreamProvider

Obiekt implementujący interfejs BinaryStreamProvider jest używany przez obiekt BinaryFile, do uzyskania wejściowego i wyjściowego strumienia pliku. Wydzielenie tego interfejsu było motywowane zróżnicowanym na platformach Windows i Windows Phone sposobem uzyskiwania strumienia pliku. Poza tym umożliwiło proste przejście od domyślnych plików danych instalowanych wraz z aplikacją, do własnych danych użytkownika aplikacji, bez konieczności kopiowania plików danych.

CategoryBinaryStorage, MealBinaryStorage, MealNameBinaryStorage i ProductBinaryStorage

Są to klasy dziedziczące po abstrakcyjnej, generycznej klasie BinaryStorage i pośrednio po BinaryFile. Ich rolą jest obsługa zapisu i odczytu pojedynczej encji modelu, odpowiednio: kategorii, posiłku (razem ze składnikami posiłku), nazwy posiłku i produktu spożywczego. Dostarczają także nazwy pliku i wersji, dla obsługiwanego przez siebie typu obiektu.

No Responses to “Serializacja binarna wewnątrz aplikacji Dietphone”

Kanał RSS z komentarzami do tego wpisu. TrackBack URL

Leave a Response