06:12:07
Podczas tworzenia stron i aplikacji w Action Script.
głównie w środowisku Flash, zawsze mamy do wykonania te same zadania. Najczęściej jest to zapewnienie łatwości nawigacji, skalowalności,
łatwości rozbudowy strony, obsługa danych zewnętrznych, obsługa wielu języków. W tym tutorialu skupię się na tym, jak
zaplanować projekt, żeby zapewnić sobie łatwość obsługi (pobierania i wyświetlania) danych zewnętrznych w taki sposób, aby
zwiększenie ilości wymagań dla projektu i rozbudowa struktury nie wpływało na zwiększenie złożoności aplikacji.
Sam artykuł nie jest tutorialem w pełnym tego słowa znaczeniu - jest to zbiór pewnych proponowanych i sprawdzonych
dobrych rozwiązań popartych fragmentami kodu.
Zakładmy, że dane zewnętrzne są dostarczane do klienta Flashowego przez jakiś serwer usług zdalnych. W przykładach pokazane
jest wykorzystanie najpopularniejszych usług AMF (np. Fluorine, OpenAMF, AMFPHP, WebOrb itd.), jednak wykorzystanie WebService
będzie równie proste. Implementacją samego serwera nie będziemy się zajmować. Zaczniemy od najczęściej popełnianych błędów:
Zły przykład!
class pl.peper.examples.BadExample {
public function getData() {
var service:Service = new Service(
"http://someaddress.com/flashservices/gateway",
null,
"SomeService"
);
var pc:PendingCall = service.getSomeData("parameters");
pc.onResult = function() {
doSomething();
//...
}
pc.onFault = function() {
doSomethingElse();
//...
}
}
}
Dlaczego poprzedni przykład jest zły ?
Chcemy aby architektura naszej aplikacji była:
Pierwsze co musimy zrobić, to „opakować" kod wywołujący zdalne usługi tak, żeby zapewnić łatwe w obsłudze i bezpieczne interfejsy do obsługi danych zewnętrznych. W przypadku zmian w wymaganiach niefunkcjonalnych po stronie serwera ewentualne zamiany po stronie Flasha powinny byc wykonane tylko w tej części. Co robimy ?
Korzyści:
Klasa typu service delegate:
class pl.peper.examples.ServiceDelegate {
private var responder:Responder;
private var service:Service;
public function ServiceDelegate(responder:Responder) {
this.service = new Service(
"http://someaddress.com/flashservices/gateway",
new Log(),
"SomeService"
);
this.responder = responder;
}
public function getSomeData(p1:String, p2:Number):Void {
var pc:PendingCall = this.service.getSomeData(p1,p2);
pc.responder = this.responder;
}
}
Przykład zastosowania
var responder:Responder = ...;
var delegate:ServiceDelegate =
new ServiceDelegate(responder);
delegate.getSomeData("news",1);
Responder może być obiektem typu Relay Responder lub inną
klasą implementującą interfejs mx.remoting.Responder (np this).
Implementując metodę onResult Respondera możemy wyświetlić w odpowedni sposób pobrane dane
class done.examples.Ex2ServiceDelegate {
private var service:Service;
public function Ex2ServiceDelegate() {
this.service = new Service(
"http://someaddress.com/flashservices/gateway",
new Log(),
"SomeService"
);
}
public static function initialize():Void {
instance = new ServiceDelegate();
}
public static function getInstance():Ex2ServiceDelegate {
return instance;
}
public function getSomeData(p1:String, p2:Number,
responder:Responder):Void {
var pc:PendingCall = this.service.getSomeData(p1,p2);
pc.responder = responder;
}
}
Przykład użycia
Przy starcie aplikacji wykonujemy :
ExServiceDelegate.initialize();
Responder jak w poprzednim przypadku.
Róznica jak widać jest jedynie w stypu programowania i trudno jest mi znaleźć argumenty przeważające dla jednej lub drugiej metody.
_root.gatewayUrl - jest to standardowa
nazwa zmiennej, którą można podać jako parametr przy
osadzaniu flasha w html. W razie gdy nie jest ustawiona, przyjmujemy
adres domyślny. To pozwala łatwo i bez kompilacji przenieść naszą
aplikacjię na inny serwer.Wywołanie i renderowanie wyników w widoku:
class pl.peper.examples.SomeSection extends Section {
...
private function show():Void {
Ex2ServiceDelegate.getInstance().getSomeData(
"news",1, new RelayResponder(this,"showData","onFault"));
super.show();
}
private function showData(re:ResultEvent):Void {
txt.htmlText = re.result;
//jakiś rendering danych
}
private function onFault(fe:FaultEvent):Void {
//informacja o błędzie
}
}
Przy tworzeniu bardziej rozbudowanych aplikacji w większym zespole stosujemy bardziej rozbudowane architektury. Zależy nam na spełnieniu założenia skalowalności - dodawanie kolejnych funkcjonalności nie powinno zwiększać złożoności kodu. Przy kilkudziesięciu różnych funkcjonalnościach kod powinien być tak samo łatwy do zarządzania, pielęgnowania i opanowania jak przy 10. Staramy się stworzyć i odseparować od siebie warstwy na tyle by umożliwić jednoczesną i niezależną pracę wielu osób. Nie chcemy aby animacje i szczegóły wizualne wpływały na wykonywanie zdalnych metod i odwrotnie (oczywiście czasem trzeba poczekać z animacją na dane z serwera).
Identyfikacja problemu Widoki często są animowane, graficy puszczają wodze fantazji (i bardze dobrze), powoduje to, że:
Dane umieszczane dynamicznie w takich widokach pochodzą z usług przez co widoki są od nich zależne.
_parent._parent._parent._parent._parent. this.mc.mc1anim.tween.textField.text = ... Nie możemy upraszczać widoków. Pamiętajmy o tym, że zwykle główną funkjcą aplikacji flashowych jest to, żeby atrakcyjnie
wyglądały. Dlatego powinniśmy dopasować architekturę w taki sposób, żeby umożliwić tworzenie dowolnch, niezależnych widoków i
efektów wizualnych.
Model
Tworzymy, klasę nazwijmy ją ModelLocator, która będize
zbierać wszelkie dynamiczne dane. Niech będzie singletonem.
class pl.peper.example.ModelLocator {
private static var _instance:ModelLocator = null;
public static function get instance():ModelLocator {
if (!_instance) {
_instance = new ModelLocator();
}
return _instance;
}
public var newsTitle:String = "";
public var newsContent:String = "";
}
Obługa danych:
Obsługując akcję pokazania jakiejś podstrony pobieramy dane.
class pl.peper.examples.MyExampleClass {
...
private function show():Void {
ServiceDelegate.getInstance().getSomeData(
"news",1, new RelayResponder(this,"handleData","onFault"));
}
private function handleData(re:ResultEvent):Void {
ModelLocator.instance.newsTitle = re.result.title;
ModelLocator.instance.newsContent = re.result.content;
//...
}
private function onFault(fe:FaultEvent):Void {
//informacja o błędzie
}
}
Widok
Klasa jakiegoś MovieClip'a, która pokazuje newsy gdiześ w
animacji
class pl.peper.example.NewsView extends MovieClip {
private var txtNewsTitle:TextField;
private var txtNewsContent:TextField;
function onLoad():Void {
//gdyby jakieś dane już były wcześniej
txtNewsTitle.text = ModelLocator.instance.newsTitle;
txtNewsContent.text = ModelLocator.instance.newsContent;
modelLocator.instance.watch("newsTitle",newsChange);
modelLocator.instance.watch("newsContent",newsChange);
}
private function newsChange(propName:String,
oldVal:String,
newVal:String):Void {
if(propName == "newsTitle") {
txtNewsTitle.text = newVal;
} else if(propName == "newsContent") {
txtNewsContent.text = newVal;
}
//jakieś animacje
return newVal;
}
}
Użycie metody watch jest najprostszym rozwiązaniem. Można w tym celu
wykorzystać też zdarzenia.
Takie rozwiązanie pozwala wykonać jakieś animacje np. rozsypanka tekstu
czy inne gdy pojawią się nowe dane bez znajomości nawet klasy,
która pobiera te dane.
Zmiana animacji, zagnieżdżenie newsów w jeszcze 2 tweenach
nic nie zmienia w kodzie.
Wykorzystanie metody watch bardzo przypomina data binding,
który pojawia się np. w komponentach UI flasha czy w innych
językach np. Flex, Java (Java Server Faces, Jface) i innych. Tego typu
rozwiązanie jeszcze się gdzieś na pewno pojawi.
Jeśli klasę, która obsłużyła dane, nazwiemy formalnie
kontrolerem, to widzimy, że w sposób naturalny uzyskaliśmy
pewną realizację wzorca Model-View-Controller.
Mogą zdarzyć się sytuacje kiedy poprzednie rozwiązanie okaże się
niewystarczające.
Akcje (w sensie realizacji jakiejś funkcjonalności) pojawiają się w
aplikacji na skutek pewnych gestów użytkownika (klikanie,
wciskanie klawiszy, przycisków itp.)
W poprzednich przypadkach obsługiwaliśmy to zdarzenie tam gdzie się
pojawiło - w sekcji. Gdy jednak liczba tych funkcjonalności zacznie
rosnąć, każda z klas sekcji będzie ich obsługiwać kilka lub
kilkanaście, kod zacznie się stawać trudny do ogarnięcia. Znalezienie
konkretnej funkcjonalności stanie się coraz trudniejsze.
Te same akcje mogą być pojawiać w kilku sekcjach.
Te same akcje mogą być wynikiem różnych gestów
(kliknięcie przycisku, wybór z menu lub użycie
skrótu klawiszowego)
Wykonanie pewnych akcji może być zależne od siebie (trzeba je
kolejkować), czasami trzeba anulować akcję lub zrobić "undo".
Wzorzec:
Interfejs polecenia zawiera tylko jedną metodę - execute.
interface pl.peper.examples.Command {
public function execute():Void;
}
Przykładowe polecenie (receiverem jest tu ModelLocator):
class pl.peper.examples.GetNewsCommand implements Command, Responder {
private var param1:String;
private var param2:Number;
public function GetNewsCommand(param1:String, param2:Number) {
this.param1 = param1;
this.param2 = param2;
}
public function execute():Void {
ServiceDelegate.getInstance().getSomeData(this.param1,this.param2, this);
}
private function result(re:ResultEvent):Void {
ModelLocator.instance.newsTitle = re.result.title;
ModelLocator.instance.newsContent = re.result.content;
}
private function fault(fe:FaultEvent):Void {
//informacja o błędzie
}
}
Bardzo prosty menadżer poleceń (przy korzystaniu z Gugga Flash Framework można wykorzystać tamtejszy)
class pl.peper.examples.CommandManager {
private static var _instance:CommandManager;
private var commandHistory:Array;
private function CommandManager():Void {
this.commandHistory = new Array();
}
public static function get instance():CommandManager {
if (!_instance) {
_instance = new CommandManager();
}
return _instance;
}
public function executeCommand(command:Command):Void {
command.execute();
this.commandHistory.push(command);
}
}
Przykład zastosowania
class pl.peper.examples.SomeSesction extends Section {
private function show():Void {
CommandManager.instance.executeCommand(
new GetNewsCommand("news",1)
);
}
}
Gdy pojawią się nowe dane w widoku zostaną automatycznie odświeżone. Te same dane mogą zostać wyświetlone w kilku widokach, np. na wykresie i w tabelce w przypadku danych tabelarycznych.
Można wprowadzić jeszcze jedną modyfikację.
Jeśli ktoś stwierdzi, że to wszystko przerost formy nad
treścią to częściowo będzie miał rację. Bardzo istotne jest, aby
dopasować architekturę do złożoności aplikacji, którą
budujemy. Zwykle na poziomie projektowania wstępnego można ocenić jak
bardzo skomplikowana będzie aplikacja i jakich narzędzi trzeba użyć.
Kilka wskazówek z doświadczenia: