Friday, October 31, 2008

Wzorce projektowe: Temporal Object




Gdy zaczynałem poznawać wzorce projektowe punktem wyjścia dla mnie były książki w stylu GoF, blogi, fora, itd. Znajdowałem tam przede wszystkim diagramy UML, oraz banalne przykłady kodu w stylu: fabryka pizzy, szablon algorytmu, budowniczy okienka. Mój kłopot polegał na tym, że chociaż rozumiałem o czym się do mnie pisze, to nie wiedziałem jak zastosować te koncepcje w moim kodzie. Moje projekty związane były np. ze sklepem internetowym lub systemem ankietowym i nijak to się miało do pizzy, algorytmów czy okienek. Brakowało mi przede wszystkim sensownej implementacji wzorca oraz konkretnych wskazówek jak i gdzie go użyć.

Po jakimś czasie zacząłem analizować źródła programów OpenSource takie jak Spring i apache-commons. Sęk w tym, że aby zrozumieć dobrze te projekty trzeba było wzorce projektowe wcześniej znać, a ja dopiero chciałem się ich nauczyć. Sporo mnie kosztowało rozgryzanie tego tematu.

W tej serii artykułów odpowiem choć na część w/w pytań. Będę: omawiał różne wzorce, podawał przykładowe implementacje i podpowiadał gdzie można ich użyć. Jeśli chodzi o sam sposób użycia czyli wprowadzanie wzorca do projektu i wykrywanie potencjalnych miejsc jego zastosowania w projekcie, to ten temat zostanie poruszony w wątku o refaktoringu (w najbliższej przyszłości).

Obiekty z historią


Wyobraźmy sobie, że należy stworzyć model obiektowy, który posłuży do zrealizowania funkcjonalności sklepu (taaa...wiem, że przykład mocno wyświechtany, lecz jakże użyteczny i skoro skojarzenia takie jak Hello world!, foo bar i Team-Member mocno zapadły w umysły rzesz programistów, więc i ja nie będę odstawał i posłużę się dobrze znanym przykładem rzeczonego sklepu).

Centralną klasą modelu będzie obiekt Order. Zakładając, że klient użytkujący sklep może zapisać stan swojego zamówienia, a następnie do niego wrócić, okazuje się, że warto śledzić historię jego...hmmm...niezdecydowania? W przypadku sklepu może być to pomocne np. podczas analizy aktywności klienta, na podstawie której można będzie mu w przyszłości zaproponować nowe produkty i usługi.

Postawiony problem można uogólnić następująco: stan danego obiektu może zmieniać się w czasie i należy zapewnić możliwość śledzenia historii zmian.
W tym miejscu zrób krótką przerwę, weź kartkę i długopis oraz zaproponuj przykładowe rozwiązanie omówionej kwestii.

Już masz? Świetnie, zatem porównaj je z dalszą częścią artykułu.

Temporal Object


Twoje rozwiązanie jest z pewnością wystarczające, lecz posłuchaj o innym, które jest na tyle często eksploatowane przez programistów, zostało określone mianem wzorca projektowego.

Intencją wzorca Temporal Object jest śledzenie zmian w obiekcie i udostępnianie ich na życzenie. Wzorzec ten jest również znany pod nazwami History on Self oraz Version History.

W przykładzie mamy do czynienia obiektem reprezentującym zamówienie. Sformułujmy wymagania co do funkcjonalności:
  • usługa pracuje z obiektem zamówienia Order
  • zamówienie można dowolnie zmieniać
  • historia zmiana ma być śledzona i udostępniana na życzenie
  • dla celów raportowych, oprócz bieżących, należy zapamiętywać godzinowe milestones

Spójrzmy na projekt rozwiązania:

Głównym konceptem jest wprowadzenie obiektu OrderVersion, który śledzi zmiany w zamówieni, tzn. dla każdej zmiany tworzony jest nowy obiekt OrderVersion. Sam obiekt klasy Order, z które będą korzystały usługi jest niejako proxy bieżącej wersji zamówienia. Dodatkowo wprowadzona została klasa VersionHistory, której odpowiedzialnością jest zarządzanie historią zamówienia.

Całe piękno tego rozwiązania polega na tym, że usłudze udostępniony będzie obiekt Order, który powinien proksować API OrderVersion tyle, że pracuje zawsze na wersji bieżącej. Reszta przetwarzania jest ukryta przed klientem.

Zgodnie z założeniem tej serii artykułów przedstawiam również implementację wzorca.

public class Order implements Serializable {
private Long id;
private VersionHistory versioningHistory = new VersionHistory();
private OrderVersion currentVersion;
public void createNewVersion( String productId, 
String productName, Double price ) {
OrderVersion orderVersion = new OrderVersion();
orderVersion.setCustomId( productId );
orderVersion.setName( productName );
orderVersion.setPrice( price );
versioningHistory
.addOrderVersion( orderVersion.getDate(),   
orderVersion );
currentVersion = orderVersion;
}

public void setCurrentVersionTo( Date when ) {
currentVersion = versioningHistory.findVersion( when );
}
protected OrderVersion getCurrentVersion() {
return currentVersion;
}
public String getCustomId() {
return getCurrentVersion().getCustomId();
}
public void setCustomId( String customId ) {
getCurrentVersion().setCustomId( customId );
}
//delegacje reszty getterów i setterów
}



public class VersionHistory implements Serializable {
private Map orderVersions
= new HashMap();
private List orderHourMilestones
= new ArrayList();
public OrderVersion findVersion( Date date ) {
return orderVersions.get( date );
}
public void addOrderVersion( Date date, OrderVersion version ) {
orderVersions.put( date , version );
}
public void createHourMilestone() {
//...
}
}



public class OrderVersion implements Serializable {
private Long id;
private String customId;
private String name;
private Double price;
private Date date;
public OrderVersion() {
this.date = Utils.getNow();
} 
}
//gettery i settery



A co z persystencją?


Kolejny problem, o który można potknąć się podczas nauki wzorców to kwestia związana z trwałym przechowywaniem danych. O ile w języku obiektowym można napisać niemal wszystko, również w bazie danych można stworzyć dowolnie złożone rozwiązanie to jednak sklejenie tego razem czasem nastręcza kłopotów. Dlatego, aby opis wzorca był kompletny zajmijmy się teraz trwałym przechowywaniem danych w relacyjnej bazie danych.

Domyślnie, używając dostarczycieli persystencji dla JPA, przyjmowana jest zasada, że jeden obiekt jest mapowany do jednej tabeli w bazie danych. Moim zdaniem przyjęcie takiej arbitralnej zasady prowadzi do bałaganu w bazie danych oraz do jej „niewyważenia”. Niewyważenie rozumiem jako sytuację, gdzie poszczególne tabele przechowują nieproporcjonalnie dużą ilość danych, np. jedna tabela ma 2 kolumny oraz 10 wierszy, natomiast inna 20 kolumn i 10000 wierszy. Taka sytuacja w moim mniemaniu daje przesłanki do zastanowienia się, czy ta mała tabela jest potrzebna. Być może można znajdujące się w niej dane umieścić jako dodatkową kolumnę i innej tabeli i w ten sposób uprość zapytania SQL pracujące na bazie. Zaznaczam, że to moje prywatne zdanie.

Wykorzystując mapowania JPA umieścimy strukturę obiektową w dwóch tabelach: orders – przechowującej zamówienia oraz orders_versions – przechowującą wersje poszczególnych zamówień.

Schemat bazy danych będzie wyglądał następująco:

Wiersze z orders identyfikują poszczególne zamówienia oraz wskazują na jego bieżącą wersję. Natomiast wiersze z orders_versions przechowują dane na temat danej wersji.

Dodatkowo każda wersja wskazuje na zamówienie do którego należy oraz, jeśli jest godzinowym milestonem, to wskazuje na właściciela. Na poziomie obiektowym pomiędzy obiektami Order oraz VersionHistory występuje relacja 1:1, zatem wiersze z orders identyfikują również obiekt VersionHistory. Z tego względu orders_versions posiada dodatkowe wskazanie na orders w postaci klucza ref_order_hour_milestone, określające, że dana wersja należy do historii wersji danego zamówienia.

Odpowiednie mapowania JPA wyglądają następująco:

@Entity @Table( name = "orders" )
@NamedQuery( name="Order.findAll", query="from Order" )
public class Order implements Serializable {
@Id
@GeneratedValue( strategy=GenerationType.AUTO )
private Long id;

@Embedded
private VersionHistory versioningHistory = new VersionHistory();

@OneToOne
@JoinColumn(name="ref_current_version")
private OrderVersion currentVersion;
}



@Embeddable
public class VersionHistory implements Serializable {

@OneToMany(cascade=CascadeType.ALL)
@MapKey( name="date" )

@JoinColumn( name="ref_order_history" )   
private Map orderVersions
= new HashMap();

@OneToMany
@JoinColumn( name="ref_order_hour_milestone" )
private List orderHourMilestones
= new ArrayList();
}



@Entity @Table( name = "orders_versions" )
public class OrderVersion implements Serializable {
@Id
@GeneratedValue( strategy=GenerationType.AUTO )
private Long id;
@Column( name="custom_id" )
private String customId;
private String name;
private Double price;
private Date date; 
}



Jak można zauważyć pomiędzy tabelami ordersa orders_versions występuje powiązanie dwukierunkowe.
Na wstępie tego rozdziału wspominałem o dbaniu o optymalność zapytań. Przeprowadziłem test i zapis jednego zamówienia z trzema wersjami powoduje wykonanie na bazie następujących zapytań SQL:

Hibernate: insert into orders (ref_current_version) values (?)
Hibernate: insert into orders_versions (custom_id, date, name, price) values (?, ?, ?, ?)
Hibernate: insert into orders_versions (custom_id, date, name, price) values (?, ?, ?, ?)
Hibernate: insert into orders_versions (custom_id, date, name, price) values (?, ?, ?, ?)
Hibernate: update orders set ref_current_version=? where id=?
Hibernate: update orders_versions set ref_order_hour_milestone=? where id=?
Hibernate: update orders_versions set ref_order_history=? where id=?
Hibernate: update orders_versions set ref_order_history=? where id=?
Hibernate: update orders_versions set ref_order_history=? where id=?



Zmieńmy jedna nieco schemat bazy danych przenosząc powiązanie zamówienia z jego bieżącą wersją do tabeli orders_versions. Rysunek poniżej:

Zmiana na w mapowaniach jest bardzo niewielka:

//...
public class Order implements Serializable {
//...
@OneToOne(mappedBy="parentOrder")
private OrderVersion currentVersion;
//...
public void createNewVersion( String productId, String productName, 
Double price ) {
//..
orderVersion.setParentOrder( this );
}
}



//..
public class OrderVersion implements Serializable {
//..
@OneToOne
@JoinColumn(name="ref_order")
private Order parentOrder;
//..
}


Zestaw zapytań tym razem wygenerowany przez Hibernate wygląda następująco:

Hibernate: insert into orders values ( )
Hibernate: insert into orders_versions (custom_id, date, name, ref_order, price) values (?, ?, ?, ?, ?)
Hibernate: insert into orders_versions (custom_id, date, name, ref_order, price) values (?, ?, ?, ?, ?)
Hibernate: insert into orders_versions (custom_id, date, name, ref_order, price) values (?, ?, ?, ?, ?)
Hibernate: update orders_versions set ref_order_hour_milestone=? where id=?
Hibernate: update orders_versions set ref_order_history=? where id=?
Hibernate: update orders_versions set ref_order_history=? where id=?
Hibernate: update orders_versions set ref_order_history=? where id=?


Zatem mamy o jedno zapytanie mniej. Czy to dużo? Trudno powiedzieć, aczkolwiek na każde tysiąc zapisów zamówienia do bazy danych mamy tysiąc zapytań mniej...

Podsumowując


Wzorca Temporal Object można użyć jeśli występuje potrzeba śledzenia i zapamiętywania zmian w modelu obiektowym. Używając narzędzi O\RM o trwałego zapisu danych, nie dajmy się zwieść iluzji, że programista może zapomnieć o bazie danych. Jeśli mamy na uwadze wydajność należy o tym pamiętać, zwłaszcza wtedy, gdy większość narzędzi ukrywa przed programistą złożoność swoich działań.

Kompletny kod źródłowy omawianego rozwiązania znajdziesz na blogu, którego adres widoczny jest na stronie tytułowej artykułu. W projekcie zostały użyte mapowania JPA, Hibernate jako dostarczyciel persystencji oraz baza danych MySQL

Wednesday, October 29, 2008

Uwaga na Immutable + JPA

Ostatnio użyłem wzorca Immutable w tradycyjnej implementacji

public class Team {

public Member getMember() {
return new Member( this.member );
}

}


Po zmapowaniu klasy adnotacjami JPA sporo głowiłem się dlaczego dostaję albo zbyt wiele wierszy w bazie albo wyjątek z informacją, że nastąpiła próba zapisu obiektu transient...rzut oka na powyższy kod wyjaśnia sprawę ;) ech...

Tuesday, October 28, 2008

Strzeż się ludzi, którzy są pewni tego, że mają rację!

Programista, architekt, bazodanowiec, temaleader...cokolwiek człowiek by nie robił, z biegiem czasu się specjalizuje, z biegiem czasu staje się ekspertem. I to chyba jest bardzo niebezpieczne...

Zbyt łatwo zdarza się nam powiedzieć do siebie samego: "Ok, już wszystko umiem". Kiedy przyjdzie Ci do głowy takie zdanie wiedz, że już po tobie! Twoja kariera, kimkolwiek byś nie był, właśnie rozpoczęła powolny lecz konsekwentny ruch w dół, czy raczej w tył.

Powyższe, wypowiedziane na głos lub choć wewnętrznie zadeklarowane, stwierdzenie automatycznie zamyka na dalszy rozwój, gdyż zakłada, że on się zakończył. Świat się rozwija, technologia się rozwija i zatrzymanie się w miejscu faktycznie oznacza cofanie się.

Profesjonalizm można poznać po tym, że osoba pozostaje otwarta. Otwarta na to, że każdego dnia może nauczyć się czegoś nowego, że może nauczyć się czegoś od młodszego kolegi właśnie przyjętego do pracy, od podwładnego, od kogoś kogo uważa za mniej kompetentnego od siebie, że może nauczyć się czegoś od swojego ucznia. Tylko ta postawa gwarantuje możliwość ciągłego doskonalenia się. Oto jest sens słynnego Wiem, że nic nie wiem!

Thursday, October 2, 2008

Organizowanie logiki biznesowej

Chris Richardson, w książce Pojo in Action podaje kilka decyzji, które musi podjąć projektant systemu enterprise w Jawie. Jednym z wyborów przed którym rzeczony projektant stoi dotyczy sposobu w jaki zorganizowana jest logika biznesowa. Autor nazywa to wyborem pomiędzy podejściem proceduralnym a obiektowym. Rzecz w tym, by wybrać jeden z trzech opisanych przez Martina Fowlera wzorców. Choć Richardson podaje pewne kryteria, to jednak operuje na pojęciach wybitnie nieostrych typu: duże projekty, małe projekty, skomplikowana logika, niewiele logiki. W artykule chciałbym przyjrzeć się problemowi i podać bardziej namacalne kryteria wyboru.
(Czytelnik niezaznajomiony z wzorcami enterprise znajdzie zwięzłe charakterystyki na końcu artykułu; po szczegóły odsyłam do bliki Martina Fowlera, rozdział Domain Logic Patterns.

Proceduralnie czy obiektowo?


Bez obawy! Nikt nie zmusza Cię do cofnięcia się w świat języków proceduralnych, nie w tym rzecz...Istotę problemu można sformułować następująco: Powstało wymaganie biznesowe, aby napisać system robiący COŚ TAM. Jak się do tego zabrać, aby uczynić zadość oczekiwaniom klienta i jednocześnie włożyć to wysiłek odpowiedni do natury rzeczy. Wiadomo, że klient chciałby jak najwyższą jakość za jak najniższą ceną, natomiast dostawca chce dostarczyć najniższą dopuszczalną jakość za jak najwyższą cenę. Słowem, kwestia jest poważna.

Klasyczne podejście obiektowe każe nam zbudować obiektowy model dziedziny problemu charakteryzujący się współpracującymi pomiędzy sobą obiektami, z których każdy charakteryzuje się swoim stanem oraz zachowaniem. Obiekty będą współpracować ze sobą odzwierciedlając swój stan w bazie danych oraz w interfejsie użytkownika w taki sposób, aby zrealizować zdefiniowane przez niego wymagania.

W podejściu proceduralnym nie będziemy modelować rzeczywistości, nie będziemy modelować dziedziny problemu. W tym podejściu każemy bazie danych krok po kroku zapamiętać pewne dane, każemy interfejsowi użytkownika wprost wypisać pewne dane tak, aby w konsekwencji użytkownik dostał to, co chciał. Skąd wiemy co chciał? Chciał to, co definiują use cases.

Jak przełożą się powyższe decyzje na prace programisty? Np. tak, że w pierwszym przypadku zaprzęgniemy do działania Spring Framework, JSF i Hibernate albo EJB i resztę a drugim zdecydujemy się na PHP. Albo jeśli lubimy Jawę, to w drugim przypadku weźmiemy Tomcata, Struts2 i nie zważając na to co nam mówią o warstwach i odpowiedzialnościach, zaimplementujemy całą logikę w akcjach (tu właśnie mogą być pomocne Transaction Script lub Table Module, sprawdź w jaki sposób:))

Zauważmy, że działania użytkownika w każdym, nawet najbardziej skomplikowanym systemie, w ostatecznym rozrachunku sprowadzają się odpowiedniej sekwencji operacji CRUD. Tak, końcowym rezultatem interakcji pomiędzy obiektami jest zmiana stanu bazy danych. Można zatem twierdzić, że każdą usługę systemu zdefiniowaną poprzez use case można zastąpić skończoną ilością operacji elementarnych CRUD. Ot i istota całego problemu.

Kwestia wyboru pomiędzy podejściem obiektowym a proceduralnym sprowadza się do rozstrzygnięcia jaka jest relacja pomiędzy usługą systemu a operacjami elementarnymi.

Jeśli jest to przełożenie 1:1 np. sklep internetowy, katalog książek, itp, gdzie działania użytkowników sprowadzają się właściwie do operacji CRUD to opłaca się użyć podejścia proceduralnego.

Jeśli mamy do czynienia np. z aplikacją kadr i płac, bankiem czy obsługą giełdy to usługa systemu może mieć przełożenie na setki albo tysiące operacji elementarnych. W takim przypadku stworzenie rzetelnego modelu dziedziny ułatwi panowanie nad rozwojem projektu. Skorzystamy też z dobrodziejstwa wielu frameworków, które ułatwiają pracę. Pamiętajmy, że w konsekwencji i tak ostatecznym rezultatem będzie zestaw CRUDów z tą różnicą, że nie będziemy zmuszeni tworzyć go samodzielnie, dzięki modelowi obiektowemu oraz frameworkom zatrzymamy się na wysokim poziomie abstrakcji.

Wiem, że pominąłem kilka istotnych aspektów takich jak bezpieczeństwo, transakcyjnośc itd. Koncentrowałem się tylko na organizowaniu logiki biznesowej.



Transaction Script - w podejściu proceduralnym pozwala na ujecie w całość wielu operacji, które muszą być wykonane w jednej transakcji

Domain Model - podejście obiektowe charakteryzuje się tworzenie modelu obiektowego dziedziny problemu

Table Module - w podejściu proceduralnym jest czymś pośrednim pomiedzy Transaction Script a Domain Model, pozwala na skupienie logiki biznesowej w okół danych, na których logika pracuje