poniedziałek, 18 lipca 2011

Smsy w aplikacji Ruby on Rails

Jeśli zdarzy Wam się potrzeba wysyłania esemesków z aplikacji Ruby On Rails, to polecam plugin sms_sender, którego autorem jest moja skromna osoba. Udało mi się wyciągnąć ten plugin z naszego projektu http://restaurantery.pl, który realizujemy w RocketMind od jakiegoś czasu. Do wysyłki smsów używany jest serwis http://smsapi.pl, więc trzeba sobie tam założyć konto no i oczywiście posmarować parę zł ;D Wtyczka jest dostępna przez http://rubygems.org, wystarczy więc wpis w Gemfile-u: gem sms_sender, parę linijek konfigu (szczegóły na Githubie) i można słać smsy. Plugin automatycznie dodaje do kontrolerów metodę: send_sms(telephone, text). Oprócz tego, jeśli piszecie testy, to wtyczka udostępnia dwie extra asercje do sprawdzania, czy kod produkcyjny wywołał lub nie wywołał wysyłki smsów - w czasie testów plugin używa out of the box fejkowej implementacji sendera. Póki co sms_sender nie wykorzystuje wszystkich możliwości smsapi.pl i nie ma możliwości korzystania z innych usług do wysyłania smsów. Jeśli ktoś chciałby rozbudować plugin o obsługę bardziej zaawansowanych ficzerów smsapi lub dodać obsługę innych serwisów smsowych, to zapraszam do udziału w projekcie.

poniedziałek, 7 września 2009

Distributed pair programming

Jak w jednej piosence disco polo, czasem bywa, że "złe kilometry dzielą nas". Wydawałoby się, że w takich sytuacjach nie możemy praktykować programowania w parach. Okazuje się jednak, że mamy parę opcji żeby coś takiego zorganizować na odległość przez Internet. Absolutną podstawą jest połączenie głosowe np. na Skypie. Natomiast kod najprościej współtworzyć korzystając z rozwiązania typu zdalny pulpit, np. VNC. Jeden programista stawia serwer, a drugi się podłącza do tej samej sesji.

Niestety takie rozwiązanie wymaga w miarę szybkiego łącza, a póki co szybki internet i autostrady nie są domeną naszego pięknego kraju ;). Całe szczęście, tak na dobrą sprawę nie potrzebujemy przesyłać całej zawartości ekranu. Transmisję można ograniczyć do zmian w aktualnie edytowanym przez partnerów pliku. A może by tak plugin w IDE ? Szybkie query do wujka Googla i okazuje się, że są co najmniej trzy takie pluginy do Eclipsa.

Osobiście przyglądałem się bliżej Sarosowi i XPairtise. Pierwszy zapowiadał się naprawdę obiecująco, ale koniec końców udało się tylko raz postawić sesję PP. Nie wiadomo jakim cudem, bo później już cały czas był problem z synchronizacją kodu źródłowego, która korzysta z serwera Jabbera - witamy w świecie Eclipsa ;)

Z XPairtise poszło już znacznie lepiej - udało się przeprowadzić kilka sesji PP, w których powstało dobrych parę linii ładnego kodu. Jednakże jest w nim trochę irytujących niedoróbek, które znacznie obniżają produktywność. Dlatego trzeba wyrobić sobie nawyk nie wykonywania pewnych czynności i da się z tego korzystać.

Zwolennicy netBeansa też znajdą analogiczne rozwiązanie w projekcie Collab. Niestety nie miałem jeszcze okazji przetestować go osobiście. Jeśli zatem, tak jak mi, podoba Wam się idea programowania w parach i pracujecie w rozproszonych zespołach, to może warto się przyjrzeć któremuś z zaproponowanych tutaj rozwiązań.

niedziela, 17 maja 2009

Ograniczony kontekst

Jedną z najważniejszych koncepcji Domain Driven Design jest ograniczony kontekst (ang. bounded context). Postanowiłem o niej napisać z kilku powodów. Po pierwsze nie widziałem żeby ktoś ją świadomie stosował. Po drugie wydaje mi się ciekawa. Po trzecie, gdy komuś o niej opowiadam, to widzę coś na kształt "wielkich oczu", zdziwienia.

Starożytna, chińska mapa świata, Chiny w centrum świata, reszta państw dookołaWspółczesna mapa polityczna

Powyższe rysunki obrazują dwa różne modele tej samej, niezwykle skomplikowanej dziedziny, z którą związane są rozmaite problemy. Tą dziedziną jest oczywiście świat, w którym żyjemy ;). Zatem który model jest lepszy ? Ten po lewej, czy ten po prawej ? Niestety nie ma poprawnej odpowiedzi na tak postawione pytanie. Przed udzieleniem odpowiedzi należałoby bowiem wyjaśnić: "lepszy do czego" lub raczej "bardziej użyteczny do czego".

Model po lewej, na którym Chiny są w centrum świata, doskonale rozwiązuje problem informowania odbiorcy o tym, że Chiny są najważniejszym państwem na świecie, a na inne państwa nie warto zwracać uwagi. Natomiast nie bardzo nadaje się do nauczania geografii w gimnazjum ;). Model po prawej pokazuje jakie są państwa, ich granice i ich położenie na kontynentach we współczesnym świecie. Dzieci w gimnazjum mogłyby więc z powodzeniem korzystać zeń na lekcjach geografii, w przeciwieństwie do starożytnych Chińczyków, którzy gotowi pomyśleć, że najważniejszym państwem na świecie jest Rosja ;).

Czy istnieje model, który rozwiązuje wszystkie problemy, które mogą się pojawić w jego dziedzinie ? Odpowiedź brzmi nie, powyżej pewnego poziomu złożoności. Gdybyśmy spróbowali stworzyć mapę, która zawiera wszystkie państwa, wszystkie rzeki, jeziora, miasta, drogi, bogactwa naturalne, a do tego informuje, że Chiny są najważniejszym państwem na świecie, to zapewne efekt naszej pracy byłby zupełnie nieczytelny. Zamiast rozwiązywać te wszystkie problemy, nie rozwiązywałby żadnego.

Czas najwyższy na morał tej opowiastki. Analogiczne zjawisko zachodzi w świecie oprogramowania biznesowego. Gdy mamy bardzo skomplikowaną dziedzinę biznesową i duży zespół programistów (np. 20 osób), i próbujemy zbudować aplikację w oparciu o jeden model, to prawdopodobieństwo porażki tego przedsięwzięcia jest bardzo wysokie. Przez porażkę rozumiem tutaj niekoniecznie niemożność wydania działającej aplikacji. Porażką może być nieczytelny kod, nieoptymalne działanie, przekroczony budżet, dużo błędów, narastające trudności przy wypuszczaniu nowych wersji, programiści uciekający z projektu ;), itd.

Jeden model w skomplikowanych projektach nie zadziała zasadniczo z dwóch powodów. Po pierwsze, tak jak w przypadku map, trudno efektywnie rozwiązywać dwa koncepcyjnie oddalone problemy biznesowe przy użyciu tylko jednego modelu. Przez "efektywne rozwiązanie" rozumiem poprawną, przejrzystą, łatwą w rozbudowie implementację. Problem narasta gdy mamy całą masę takich problemów każdy jakby "z innej beczki". Po drugie ciężko 20 programistom implementować jednocześnie jeden model - jest prawie pewne, że prędzej czy później jeden drugiemu namiesza, nie zrozumie intencji drugiego. Podsumowując: jeden model dziedziny o dużej złożoności prowadzi do syfu w kodzie ;).

Koncepcja ograniczonego kontekstu ma na celu pomóc nam uporać się z tym problemem. Jest tym samym jednym z ważniejszych pomysłów DDD na walkę ze złożonością w sercu oprogramowania. Zakłada ona jasne zdefiniowanie granic koncepcyjnie oddalonych od siebie problemów biznesowych. Dla każdego takiego ograniczonego zbioru (kontekstu) tworzymy osobny model i osobny wszechobecny język (ang. ubiquitous language). W każdym kontekście pracuje ograniczona ilość programistów, tak aby nie zakłócali sobie pracy. Nic nie stoi na przeszkodzie, a nawet jest wskazane, aby programistów "przełączać" pomiędzy kontekstami - jak powiedział Martin Fowler, high cohesion i low coupling dotyczą klas, a nie programistów ;).

Prawie na pewno wystąpi konieczność powiązania kontekstów. Rozwiązanie problemu w jednym kontekście może najpierw wymagać pracy innego kontekstu. Z tego powodu definiuje się tak zwaną mapę kontekstów (ang. context map), czyli zbiór reguł, które pozwalają "tłumaczyć" byty żyjące w jednym kontekście na byty w drugim.



Myślę, że czas na bardziej softwarowy przykład, który notabene pochodzi od Evansa. Dziedziną jest biznes transportowy, który zarabia na przewozie kontenerów pomiędzy różnymi miastami na świecie - jak na obrazku powyżej. Transport odbywa się drogą morską lub lądową, co wiąże się z szeregiem załadunków, rozładunków kontenerów w różnych portach, stacjach kolejowych itp. Kluczowym problemem w takiej dziedzinie jest śledzenie historii transportu towaru. Zadanie to doskonale rozwiązuje model przedstawiony na diagramie poniżej.

Mamy tutaj ładunek (ang. cargo), który posiada specyfikację swojej drogi (ang. route specification) od miejsca pochodzenia do celu transportu. Ładunek posiada również rozkład (ang. itinerary), czyli listę gałęzi (ang. leg). Gałąź to nic innego jak fragment drogi ładunku, np. podróż morska z Hong Kongu do Los Angeles lub podróż pociągiem z Los Angeles do Dallas. Gałąź posiada więc miejsce załadunku i rozładunku towaru. Miejsce załadunku pierwszej gałęzi rozkładu musi być równe miejscu pochodzenia towaru (ang. origin), a miejsce rozładunku ostatniej gałęzi musi być równe miejscu przeznaczenia (ang. destination) - wtedy mówimy, że rozkład spełnia specyfikację drogi. Żeby lepiej zobrazować czym są te gałęzie zamieszczam poniższy rysunek.



Przedstawiony model pozwala pracować z planem transportu towaru. Może np. posłużyć w aplikacji wystawiającej zlecenia przewoźnikom na poszczególnych gałęziach. W tej dziedzinie występują jednak inne problemy. Wyobraźmy sobie, że oprócz śledzenia transportu nasza aplikacja musi określić optymalną drogę (np. najkrótszą albo najtańszą albo najszybszą) z miasta, w którym klient nadaje przesyłkę do miasta docelowego. Tego typu zadanie można rozwiązać stosując teorię grafów (problem najkrótszej drogi). Powstaje pytanie czy powinniśmy do naszego modelu wprowadzać klasy, atrybuty, metody realizujące algorytmy teoriografowe ? DDD i koncepcja ograniczonego kontekstu mówią nie ! Powinniśmy raczej skorzystać z drugiego modelu, w którym będą klasy Graph, Vertex, Edge, Path i być może inne byty, o których można poczytać w książce do teorii grafów. Na tym przykładzie oczywiste staje się pojęcie mapy kontekstów. Musimy jakoś "przetłumaczyć" wynik działania algorytmu wyszukiwania najkrótszej drogi (zapewne obiekt klasy Path) na obiekt rozkładu (klasa Itinerary). Mapa kontekstów będzie w tym konkretnym przykładzie zawierać regułę, np. że atrybut name klasy Vertex odpowiada kodowi miasta, w którym następuje rozładunek/załadunek.

W przedstawionym przykładzie granice pomiędzy dwoma kontekstami są dobrze widoczne, wręcz jaskrawe. Prawdziwe scenariusze pisze jednak życie - wydzielanie kontekstów nie jest oczywiste, bo ich granice często są niewyraźne. Rozwinięcie koncepcji, które tutaj naszkicowałem znajdziecie w książce Evansa - zachęcam do lektury.

wtorek, 31 marca 2009

GRASP a anemiczny model

Tydzień temu miałem przyjemność opowiadać o zasadach GRASP na forum grupy Warsaw Design Patterns Study Group. Nijak mają się one do tego co nieraz przychodzi nam robić w pracy, czyli rzeźbić w anemicznym modelu. Wiele osób nie rozumie dlaczego jest to antywzorzec i nie przekonuje ich analogia do Turbo Pascala (siła tradycji). W związku z tym postanowiłem napisać posta, który przekona opornych. Posłużę się w tym celu właśnie zasadami GRASP, które są zestawem reguł dyktowanych zdrowym rozsądkiem, przeznaczone dla młodych adeptów Object Oriented Design.

Żeby mieć się na czym wyżywać, rozpatrzmy następujący kod, który jest sztandarowym przykładem podążania ścieżką anemicznego modelu w pewnej biznesowej, warswtowej aplikacji.

Niech to będzie nasz komponent widoku:

class FakturyView {

void dodajPozycjeDoFaktury_onClick() {
int idProduktu = ... pobieranie z kontrolek gui
int iloscSztuk = ... pobieranie z kontrolek gui
String numerFaktury = ...numer bieżąco obsługiwanej faktury
fakturySerwis.dodajPozycjeDoFaktury(idProduktu, iloscSztuk);
... //obsługa wyswietlenia nowej pozycji
}

void obliczCene_onClick() {
String numerFaktury = ...numer bieżąco obsługiwanej faktury
double cena = fakturySerwis.obliczCene(numerFaktury);
... //wyświetlanie ceny na gui
}

}

Jak widać komponent widoku obsługuje dwa zdarzenia na GUI - wciśnięcie przycisku dodającego nową pozycję do faktury i wciśnięcie przycisku, który wyświetla użytkownikowi "total" faktury. W tym celu z widoku zbierane są potrzebne informacje (numer obsługiwanej faktury, identyfikator produktu, ilość sztuk itp.) i wywoływane są "metody" (procedury) realizujące logikę biznesową, a następnie widok jest aktualizowany.

Przejdźmy teraz do warstwy logiki biznesowej, gdzie mamy anemiczne obiekty biznesowe, które są jedynie nośnikami danych i nie mają żadnych metod prócz geterów i seterów. Oprócz tych "obiektów" biznesowych mamy jeszcze serwisy, które wypruwają flaki z obiektów biznesowych w celu wykonania logiki biznesowej. Poniższy kod ilustruje tą sytuację.

"Obiekty" biznesowe:

class Faktura {
getTamto ...

getSiamto ...

getOwamto ...


setTamto ...

setSiamto ...

setOwamto ...

... itd inne atrybuty ...
}

class PozycjaNaFakturze() {

getIlosc ...

getProdukt ...

... itd

}

class Klient {
... same getery setery atrybuty klienta ...
}

class Produkt {
getNazwa...

getCena ...

.... i inne ...
}

Serwisy, czasem zwane fasadami:


class FakturySerwis {

void dodajPozycjeDoFaktury(String numerFaktury, Integer idProduktu, Integer ilosc) {
Faktura faktura = fakturaDao.wyszukajPoNumerze(numerFaktury);
PozycjaNaFakturze pozycja = new PozycjaNaFakturze();
Produkt produkt = porduktDao.wyszukajPoId(idProduktu);
pozycja.setProdukt(produkt);
pozycja.setIlosc(ilosc);
List l = faktura.getListaPozycji();
l.add(pozycja);
falturaDao.zapisz(faktura);
}

double obliczCene(String numerFaktury) {
Faktura faktura = fakturaDao.wyszukajPoNumerze(numerFaktury);
double cena = 0.0;
for(PozycjaNaFakturze pozycja : faktura.getListaPozycji())
cena += pozycja.getProdukt().getCena() * pozycja.getIlosc();
for(Rabat rabat : faktura.getKlient().getListaRabatow())
switch(rabat.getRodzaj()) {
case LOJALNOSCIOWY:
cena -= ileś tam;
...
case SWIATECZNY
cena -= ileś tam;
... obsluga wszystkich typow rabatow ...
}
switch(faktura.getKlient().getRodzaj()) {
case FIRMA:
cena += podatek dla firmy
case OSOBA_PRYWATNA:
cena += podatek dla osobty pryw.
.... obsluga pozostalych rodzajow opodatkowania ....
}
return cena;
}

}

Właściwie serwisy są klasami, tylko dlatego, że w najpopularniejszych językach obiektowych nie da się pisać osobnych funkcji tak jak się to robiło w C++ czy Object Pascalu ;).

Powyższy serwis biznesowy zawiera logikę biznesową dodawania sprzedawanego produktu do faktury oraz bardziej skomplikowaną logikę obliczania ceny faktury. Serwisy odwołują się do najniższej warstwy dostępu do danych (dao). Przepraszam fakturzystki za zapewne dziecinne rozumienie procesu wystawiania faktur ;) Oczywiście w prawdziwych okolicznościach logika biznesowa jest 5 razy bardziej rozbudowana i skomplikowana, ale już na tym trywialnym przykładzie zobaczymy, że anemiczny model to nie jest programowanie obiektowe.

Zrobiliśmy program do wystawiania faktur - pięknie działa, więc teraz zróbmy code review pod kątem spełnienia zasad GRASP, czyli lepszej czytelności, rozszerzalności i utrzymywalności.

Zacznijmy od zasady kreatora, która każe aby obiekt A był tworzony przez B jeśli ten go przechowuje lub wywołuje jego metody lub ma informacje potrzebne do jego utworzenia. Jedynym miejscem w naszym kodzie, gdzie coś jest tworzone jest metoda dodajPozycjeDoFaktury, w której tworzymy pozycje faktury tylko po to aby dodać ją do faktury. Czy tak powinno być ? Nie ! GRASP kreator mówi nam żeby logikę tworzenia obiektów umieszczać w pierwszej kolejności w obiektach które będą przechowywać referencję do nowo tworzonych. W ten sposób zmniejszamy liczbę zależności. W naszym przypadku oznacza to, że logikę tworzenia pozycji faktury powinniśmy umieścić w klasie Faktura. Szybki refactoring i mamy coś takiego:

class Faktura {

List listaPozycji;

... inne atrybuty faktury ...

//wow wprowadzamy logike biznesowa do anemicznego dotad obiektu
void dodajPozycje(Produkt produkt, int ilosc) {
PozycjaFaktury pozycja = new PozycjaFaktury();
pozycja.setProdukt(produkt);
pozycja.setIlosc(ilosc);
listaPozycji.add(pozycja);
}

}

class FakturySerwis {

void dodajPozycjeDoFaktury(String numerFaktury, Integer idProduktu, Integer ilosc) {
Faktura faktura = fakturaDao.wyszukajPoNumerze(numerFaktury);
Produkt produkt = porduktDao.wyszukajPoId(idProduktu);
faktura.dodajPozycje(produkt, ilosc);
fakturaDao.zapisz(faktura);
}

}

Po powyższym refactoringu nasz, dotąd anemiczny, model dziedziny biznesowej zyskał trochę rumieńców. Przenieśliśmy trochę logiki biznesowej z serwisu do obiektu biznesowego - idziemy w kierunku rich domain model. Sprawdźmy co stanie się, gdy zastosujemy pozostałe zasady GRASP.

Pora na zasadę eksperta, która mówi, żeby odpowiedzialność umieszczać w obiekcie, który posiada najwięcej informacji do jej wypełnienia. Analizując tę zasadę na naszym przykładzie widzimy, że nasz model ciągle nie domaga - FakturySerwis wykonuje całą pracę zw. z obliczaniem ceny, a nie ma żadnych informacji do tego potrzebnych ! Wszystkie informacje pobiera (wypruwa flaki) z innych obiektów łamiąc w ten sposób elementarną zasadę OOP - enkapsulację. Czym prędzej refaktoryzujemy:
class Faktura {

List listaPozycji;

Klient klient;

... inne atrybuty faktury ...


void dodajPozycje(Produkt produkt, int ilosc) {
PozycjaFaktury pozycja = new PozycjaFaktury();
pozycja.setProdukt(produkt);
pozycja.setIlosc(ilosc);
listaPozycji.add(pozycja);
}

double obliczCene() {
double cena = 0.0;
for(PozycjaNaFakturze pozycja : listaPozycji)
cena += pozycja.getProdukt().getCena() * pozycja.getIlosc();
for(Rabat rabat : klient.getListaRabatow())
switch(rabat.getRodzaj()) {
case LOJALNOSCIOWY:
cena -= ileś tam;
...
case SWIATECZNY
cena -= ileś tam;
... obsluga wszystkich typow rabatow ...
}
switch(klient.getRodzaj()) {
case FIRMA:
cena += podatek dla firmy
case OSOBA_PRYWATNA:
cena += podatek dla osobty pryw.
.... obsluga pozostalych rodzajow opodatkowania ....
}
return cena;
}

}

class FakturySerwis {

void dodajPozycjeDoFaktury(String numerFaktury, Integer idProduktu, Integer ilosc) {
Faktura faktura = fakturaDao.wyszukajPoNumerze(numerFaktury);
Produkt produkt = porduktDao.wyszukajPoId(idProduktu);
faktura.dodajPozycje(produkt, ilosc);
fakturaDao.zapisz(faktura);
}

double obliczCene(String numerFaktury) {
Faktura faktura = fakturaDao.wyszukajPoNumerze(numerFaktury);
return faktura.obliczCene();
}

}

W końcu zaczyna to jakoś wyglądać. Nieszczęsny serwis biznesowy nam się odchudził, logika biznesowa jest tam gdzie powinna być. Ale zaraz zaraz, czy wszystko jest już ok ? Niestety nie - ta Faktura z anemii poszła w otyłość ;). Stare przysłowie mówi - co za dużo to nie zdrowo - zasada eksperta ciągle nie jest spełniona - Faktura wypruwa flaki z innych obiektów i ogólnie ma za dużo odpowiedzialności (narusza kolejne zasady GRASP - high cohesion i low coupling). Nie poddajemy się jednak i refaktoryzujemy dalej - musimy zlikwidować łańcuszki: o.getA().getB() - don't talk to strangers ! W tym celu stosujemy starą, dobrą zasadę eksperta i przenosimy odpowiedzialności do klas PozycjaNaFakturze, Rabat i Klient:

class PozycjaNaFakturze {

Produkt produkt;

int ilosc;

double obliczCene() {
return ilosc * produkt.getCena();
}

}

class Rabat {

int rodzaj;

public double dlaCeny(double cena) {
switch(rodzaj) {
case LOJALNOSCIOWY:
cena -= ileś tam;
...
case SWIATECZNY
cena -= ileś tam;
... obsluga wszystkich typow rabatow ...
}
return cena;
}

}

class Klient {

List rabaty;

int rodzajKlienta;

double udzielRabatu(double cena) {
for(Rabat rabat : rabaty)
cena -= rabat.dlaCeny(cena);
return cena;
}

double naliczPodatek(double cena) {
switch(rodzajKlienta) {
case FIRMA:
cena += podatek dla firmy
case OSOBA_PRYWATNA:
cena += podatek dla osobty pryw.
.... obsluga pozostalych rodzajow opodatkowania ....
}
return cena;
}

}

Po tych zabiegach Faktura wygląda tak:

class Faktura {

List listaPozycji;

Klient klient;


... inne atrybuty faktury ...

double obliczCene() {
double cena = 0.0;
for(PozycjaNaFakturze pozycja : listaPozycji)
cena += pozycja.obliczCene();
cena = klient.udzielRabatu(cena);
cena = klient.naliczPodatek(cena);
return cena;
}

}

Teraz to już zaczyna wyglądać, jakby co najmniej leżało koło kodu Object Oriented, ale jak patrze na te switche to przypomina mi się koszmar programowania w C ;). Spróbujemy je usunąć stosując kolejną zasadę GRASP - polimorfizm. Dwa refactoringi wstecz programowaliśmy proceduralnie, więc polimorfizm był poza naszym zasięgiem, ale teraz, to co innego. Zasada polimorfizmu mówi, żeby zachowania zależne od typu umieszczać w osobnych klasach. W naszym kodzie te zachowania zależne od typu to udzielanie rabatu i naliczanie podatku. W zależności od typu rabatu i typu klienta te odpowiedzialności implementuje się inaczej, zatem zasada polimorfizmu będzie tutaj idealna do usunięcia tych paskudnych switchy. W tym celu z klasy Rabat robimy interfejs i dla każdej gałęzi switcha wprowadzamy nową implementację:

interface Rabat {

double dlaCeny(double cena);

}

class RabatSwiateczny implements Rabat {

double dlaCeny(double cena) {
//logika oblicznaia rabatu swiatecznego
}

}

class RabatZaLojalnosc implements Rabat {

double dlaCeny(double cena) {
//logika oblicznaia rabatu dla stalych klientow
}

}

Za pewne dosyć rozbudowana przez switcha klasa Rabat o niskiej kohezji została zamieniona na interfejs i szereg klas implementujących skupionych na jednym zadaniu - obliczaniu rabatu jednego typu. Kod faktury nie zmienił się. Zmieniła się na pewno logika tworzenia rabatu, ale jej nie rozpatrujemy w naszym przykładzie. Dzięki temu przekształceniu uzyskaliśmy wysoką kohezję i dużą łatwość dodawania nowych rabatów bez potrzeby zmiany istniejącego kodu.

Pozostaje jeszcze usunąć switcha decydującego jak naliczyć podatek. Moglibyśmy w tym celu zrobić z klasy Klient klasę abstrakcyjną i dla każdego typu klienta (prywatny, firma, ...) wprowadzić podklasę implementującą abstrakcyjną metodę oblicz podatek. Hmm... Klient zaczyna mieć za dużo odpowiedzialności - już oblicza sobie rabat i pewnie wie/robi parę innych rzeczy, których tutaj nie rozpatrujemy - spada wysoka kohezja. Tak nie może być - zastosujemy zasadę GRASP pure fabrication (czysta improwizacja) i wprowadzimy strategię (GoF) obliczania podatku. Dlaczego improwizacja ? A dlatego, że "strategia obliczania podatku", to nie jest coś, co występuje w świecie rzeczywistym - jest to byt czysto softwareowy:

class Klient {

List rabaty;

KalkulatorPodatku kalkulatorPodatku;

double udzielRabatu(double cena) {
for(Rabat rabat : rabaty)
cena -= rabat.dlaCeny(cena);
return cena;
}

double naliczPodatek(double cena) {
return kalkulatorPodatku.naliczPodatek(cena);
}

}

interface KalkulatorPodatku {

double naliczPodatek(double cena);

}

class KalkulatorPodatkuOdOsobyFizycznej implements KalkulatorPodatku {

double naliczPodatek(double cena) {
//logika nalicznia podatku
}

}

class KalkulatorPodatkuOdFirmy implements KalkulatorPodatku {

double naliczPodatek(double cena) {
//logika nalicznia podatku
}

}

Rozumując w ten sam sposób należałoby z Klienta usunąć pętlę obliczającą rabat wprowadzając klasę KalkulatorRabatu (pure fabrication), ale ten post robi się już przydługi, więc pozostawiam to czytelnikom jako ćwiczenie ;D.

Wypadałoby z tego wszystkiego wyciągnąc jakieś wnioski - anemiczny model to nie jest programowanie obiektowe, ponieważ, jak pokazałem, całkowicie gwałci podstawowe zasady GRASP: kreator, ekspert, polimorfizm, high cohesion, low coupling. Dopiero seria refactoringów w kierunku zachowania tych zasad, doprowadziła do bardziej czytelnego, rozszerzalnego i utrzymywalnego kodu zorientowanego obiektowo. Nie bójcie się więc PMów, którzy każą Wam napier.... byle szybciej i refaktoryzujcie !

środa, 7 stycznia 2009

Naked Objects

Ostatnio moje zainteresowania programistyczne mocno oscylują wokół tematyki Domain Driven Design i przy okazji, jako że jest to temat dosyć pokrewny, natrafiłem na wzorzec zwany Naked Objects. W tym wpisie chcę przybliżyć główne założenia tego zagadnienia.

Wzorzec ten został opisany przez niejakiego Richarda Pawsona, który sporządził na ten temat swoją rozprawę doktorską. Postanowiłem więc sięgnąć do źródeł. Jak na każdą pracę doktorską przystało, również i ta Pawsona, stawia pewien problem, stara się odnaleźć jego genezę, proponuje rozwiązanie i rozprawia nad jego zaletami/wadami. W tym przypadku problemem jest niesławny anemiczny model - antywzorzec, który objawia się obiektami biznesowymi pozbawionymi odpowiedzialności rozsianej po procedurach. Pawson zauważa popularność tego antywzorca w aplikacjach enterprise dopatrując się takiego stanu rzeczy w niekontrolowanej ewolucji wzorca MVC. Można się zgodzić z taką genezą lub nie, ale fakt jest faktem, że problem istnieje. Proponowanym lekarstwem są właśnie Naked Objects.

Aby zrozumieć o co chodzi rozważmy poniższy rysunek:


Przedstawia on 4-warstwową architekturę aplikacji znaną chociażby z DDD, czy z UP, której sercem jest warstwa obiektów biznesowych (Domain object) odpowiadjących bytom, że świata rzeczywistego. Obiekty te nie są, w przeciwieństwie do anemicznego modelu, pozbawione odpowiedzialności i łączą w sobie własności i zachowania realizujące logikę biznesową aplikacji. W warstwie zwanej konrolerem znajdują się pogrupowane w klasy procedury, które realizują poszczególne use casey aplikacji delegując odpowiedzialność do obiektów biznesowych - NIE ZAWIERAJĄ tym samym logiki biznesowej. Z procedur tych korzystają komponenty widoku umieszczone w warstwie prezentacji. Oczywiście dane w aplikacji przeważnie muszą być utrwalane, chociażby w relacyjnej bazie danych - tym zajmuje się warstwa Data management. Każdy obiekt biznesowy ma jakieś odzwierciedlenie w każdej z tych 4 warstw, więc każda zmiana czy nowa koncepcja biznesowa oznacza zazwyczaj modyfikacje w każdej z warstw, czyli mnóstwo pracy. Rozwiązaniem może być uczynienie warstw widoku i kontrolera całkowicie generycznymi. Pawson proponuje ograniczyć się jedynie do implementacji modelu domeny biznesowej i generować UI z obiektów biznesowych. W rezultacie architektura naszej aplikacji zaczyna wyglądać następująco:


Wygenerowany interfejs użytkownika jest zorientowany obiektowo (OOUI - Object Oriented User Interface). Oznacza to tyle, że użytkownik widzi na ekranie odpowiedniki obiektów dziedziny, może przeglądać/edytować ich własności oraz wysyłać do nich komunikaty tj. wywoływać ich metody. W oczywisty sposób wymusza to, żeby obiekty biznesowe miały odpowiedzialność - bye bye anemic model. Jeśli komuś trudno sobie wyobrazić po tym opisie jak wygląda obiektowe UI, to dodam, że z takim GUI mamy do czynienia na co dzień użytkując współczesne systemy operacyjne - zarówno microshitowe jak i linuxowe z dowolnym środowiskiem graficznym. Obiekty są reprezentowane w nich poprzez ikony na pulpicie, które są abstrakcją aplikacji, sprzętu, elementów systemu plików, itp. Używając menu kontekstowego możemy przeglądać własności poszczególnych obiektów i wywoływać na nich różne akcje. Zauważmy, że tak samo może być w przypadku aplikacji enterprise - użytkownik aby zrealizować jakiś biznesowy use case musi odszukać na UI obiekt(y) które posiadają potrzebną odpowiedzialność, wyedytować ich własności i/lub wywołać jakieś akcje być może przekazując do nich parametry. Obiekty, które użytkownik widzi na ekranie mają oczywiście swoje odpowiedniki w warstwie domenowej, stąd właśnie nazwa wzorca Naked Objects - obnażone dla użytkownika obiekty dziedziny biznesowej.

Podstawowym zadaniem frameworków implementujących wzorzec Naked Objects jest zatem generowanie interfejsu użytkownika na podstawie obiektów warstwy biznesowej. Wystarczy wskazać, które obiekty mają być udostępnione użytkownikowi. Na rynku mamy kilka Javowych narzędzi tego typu. Jednym z nich jest framework stworzony przez firmę samego Pawsona i nosi tę samą nazwę co wzorzec, czyli Naked Objects. Inne darmowe toole to: JMatter (nie mylić z JMetter), Trails, Sanssouci, Domain Object Explorer. W najbliższym czasie postaram się przyjrzeć bliżej niektórym z nich i zamieszczę tutaj swoje wrażenia.

Podsumowując, do najważniejszych zalet Naked Objects należą:
  • NIE anemiczny model dziedziny biznesowej,
  • Znaczne zwiększenie szybkości developmentu - implementujemy 1 warstwę, resztę robi automat,
  • Wspólny język pomiędzy developerami, a użytkownikami - kwestia mocno podkreślana w DDD,
  • Moc OOP w interfejsie użytkownika.
Jako wady wymienić należy:
  • Brak możliwości zmiany wyglądu generowanego UI, ktróry całkowicie zależy od frameworka,
  • Użytkowanie OOUI może wymagać więcej nauki niż użytkowanie tradycyjnego use case driven UI.
Powyższe wady i zalety wskazują, że wzorzec Naked Objects może być stosowany tylko wtedy, gdy nie ma absolutnie żadnych wskazań do ręcznego tworzenia UI i użytkownik końcowy będzie długotrwale korzystał z aplikacji.