wtorek, 29 listopada 2011

Prism cz. 2: Moduły

Czym tak naprawdę jest moduł? Moduł jest to zbiór widoków i funkcji aplikacji, które można odseparować od reszty. Moduł może być równie dobrze jedynie serwisem bez graficznego interfejsu użytkownika. Poszczególne moduły powinny móc komunikować się pomiędzy sobą, jednak komunikacja nie powinna naruszać ich niezależności. Sam użytkownik nie powinien mieć świadomości modularności aplikacji, która jest dla niego spójna i łatwa w użyciu.
No dobra, ale po co pisać aplikacje modułowe? Jeżeli stworzyliśmy aplikację modułową zgodnie ze sztuką, wówczas jest ona łatwa do testowania, utrzymania i rozwijania w przyszłości. Można bezproblemu dodawać nowe funkcjonalności nie ingerując w istniejący kod i rozwiązania. Każdy moduł może być osobno testowany wdrażany i rozwijany, ponieważ jest niezależną jednostką.

Bardzo prostym przykładem aplikacji modułowej może być program .NetTurnGames. Jest to aplikacja, która gromadzi w sobie różnego rodzaju gry turowe. Użytkownik po odpaleniu programu wybiera którą grę chce uruchomić. Każda mała gra turowa jest oddzielnym modułem. Wówczas użytkownik w łatwy sposób mógłby dodawać kolejne moduły np. kopiując pliki z nową grą do odpowiedniego katalogu.
Gdybyśmy chcieli ograniczyć się jedynie do gier karcianych, wówczas możemy interfejs użytkownika zamknąć w osobnym module natomiast zasady i logika każdej gry były by osobnym modułem. Podział aplikacji na moduły nie jest trywialnym zadaniem i leży po stronie projektanta aplikacji. Z reguły nie ma sensu wyodrębniać oddzielnego modułu, który byłby osobnym projektem i zawierał tylko jeden plik, a w nim jedną klasę z jedną metodą. Takie postępowanie z reguły prowadzi do nadmiaru projektów i małej czytelności aplikacji.
Powyższy przykład programu .NetTurnGames, powinien mieć wczytywane poszczególne gry w czasie działanie programu (runtime). Można to rozwiązać za pomocą reflekcji - sprawdzać czy odpowiedni moduł implementuje niezbędny interfejs. Jest to przykład wzorca Plugin.
Jednak moduły nie muszą dzielić jedynie aplikacji wg funkcjonalności, równie dobrze można podzielić aplikację warstwową ze względu na warstwy czyli otrzymalibyśmy mniej więcej takie moduły: UIModule, BuisinessLogicModule, DataAccessModule. Duże aplikacje mogą łączyć w sobie oba podejścia czyli dzielić moduły wg funkcjonalności i warstw jednocześnie.
W kontekście .NET doskonałym framework'iem, który ułatwia pisanie takich aplikacji jest Prism. Prism wręcz wymusza na nas, abyśmy pisali aplikacje modułowe.
Zazwyczaj przyjmuje się, że każdy moduł jest oddzielnym projektem (nic jednak nie stoi na przeszkodzie, aby moduł składał się z wielu projektów, lub jeden projekt z wielu modułów).

Każdy moduł powinien zawierać klasę implementującą interfejs IModule, dzięki czemu możemy go zainicjalizować.
public class MyModule : IModule
{
public void Initialize(){}
}

Podczas inicjalizacji, tworzymy wszystkie obiekty niezbędne do poprawnego działania modułu oraz rejestrujemy widoki.
Wszystkie informacje nt. modułów trzymane są w obiekcie klasy ModuleCatalog, który znajduje się w bootstrapperze. Również w bootstrapperze jest metoda, którą musimy przeciążyć, aby dodać przy starcie aplikacji wszystkie potrzebne moduły.
protected override IModuleCatalog CreateModuleCatalog()
{
IModuleCatalog moduleCatalog = new ModuleCatalog();
moduleCatalog.AddModule(typeof(CardGameModule.CardGameModule));
return moduleCatalog;
}

W obiekcie typu ModuleCatalog każdy moduł reprezentowany jest przez obiekt klasy ModuleInfo.
Moduły można wczytywać od razu lub doczytywać je na żądanie. Domyślnie każdy wczytywany jest tak szybko jak to tylko możliwe, natomiast można to zmienić dodając do metody AddModule argument:
moduleCatalog.AddModule(typeof(CardGameModule.CardGameModule), InitializationMode.OnDemand);


Wówczas w momencie gdy chcemy załadować moduł musimy na obiekcie typu IModuleManager wywołać metodę LoadModule
moduleManager.LoadModule("CardGameModule");


Moduły można dodawać również za pomocą pliku xaml. Taka implementacja wygląda wówczas następująco:
<Modularity:ModuleCatalog
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:Modularity="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism">
<Modularity:ModuleInfoGroup Ref="CardGameModule.xap" InitializationMode="WhenAvailable">
<Modularity:ModuleInfo ModuleName="CardGameModule" ModuleType="CardGameModule.CardGameModule, CardGameModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</Modularity:ModuleInfoGroup>
</Modularity:ModuleCatalog>

protected override IModuleCatalog CreateModuleCatalog()
{
var moduleCatalog = Microsoft.Practices.Prism.Modularity.ModuleCatalog.CreateFromXaml(new Uri("/MVVMPrismSilverlightv3;component/ModulesCatalog.xaml", UriKind.Relative));
return moduleCatalog;
}

Ponieważ każdy moduł jest niezależną jednostką, zazwyczaj zachodzi potrzeba komunikacji pomiędzy modułami. Jeżeli moduły mają dostęp do tego samego interfejsu (typu) wówczas możemy wstrzyknąć do wielu różnych modułów ten sam obiekt danego typu. Proces wstrzykiwania opisuje pattern Dependency Injection (DI). W prismie możemy domyślnie korzystać z dwóch bibliotek wspierających DI: Unity oraz MEF, jednak nic nie stoi na przeszkodzie, aby podpiąć inną bibliotekę np StructureMap i wykorzystywać jej możliwości w projekcie opartym na PRISM (wykracza to poza temat tego posta). PRISM dostarcza nam konkretnego rozwiązania problemu komunikacji, opartego na patencie DI. Jest to EventAggregator. Jeżeli korzystamy z domyślnych klas PRISMA nie musimy rejestrować EventAggregator'a ponieważ jest on rejestrowany w bazowej metodzie ConfigureContainer() naszego bootstrappera.

Źródło wiedzy:

Developer Guide to Microsoft Prism 4

1 komentarze:

Oskar Dudycz pisze...

Fajnie i prosto opisane. Czekam na drugą część o wykorzystaniu modułów w regionach :)

Prześlij komentarz

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Online Project management