Thursday, November 6, 2008

Język wzorców





Przewrotnie nadałem temu wpisowi tytuł nawiązujący do książki Christophera Alexandra, gdyż od niego zaczęła się ta cała zabawa i chyba już nigdy się nie skończy.
Trudno powiedzieć czy Alexander odkrył naturę powtarzalności czy też tylko uświadomił ludziom to, co czynili od zawsze.

Architektura oprogramowania


Jak na złość koncept powtarzalnych wzorców zyskał największy posłuch wśród inżynierów oprogramowania. Gdy się zastanowić, to są ku temu ważkie powody. Po pierwsze znakomita większość projektów programistycznych przekracza budżet i/lub założone ramy czasowe. Po drugie tworząc milion razy ten sam system można to zrobić na milion różnych sposobów. Naturalnie powstaje pytanie: który ze sposobów jest najlepszy? Bez wahania można odpowiedzieć, że ten który najlepiej spełnia założone kryteria będące w tym przypadku funkcją oceny produktu. Utożsamienie wspomnianej funkcji oceny oprogramowania z, tylko i wyłącznie, wymaganiami użytkowników jest poważanym uproszczeniem. Rzeczona funkcja ma wiele składowych: wymagania użytkowników, założony koszt, ramy czasowe, oczekiwania każdego z członków zespołu, itd. Jedne z nich są specyfikowane jawnie, inne manifestowane zupełnie nieświadomie. Mamy zatem pewne kryterium ewaluacji sposobu programowania: końcowy produkt ma być optymalny ze względu na przyjętą funkcję oceny.

Wzorce projektowe


Szansę na optymalizację funkcji oceny oprogramowania dostrzeżono właśnie w konceptach Christophera Alexandra. Założenia były proste: wiele systemów jest do siebie podobnych, podobieństwo dotyczy różnych poziomów abstrakcji i różnych części systemu. Dodatkowo systemy tworzone są za pomocą języka programowania. Stąd pytanie: czy istnieją takie konstrukcje językowe, które w optymalny sposób rozwiązują problemy związane z programowaniem?

Wspomniane problemy dotyczą sposobu w jaki współpracują obiekty w systemie w celu realizacji określonego zdania. (Część zagadnień opisałem w artykule Organizowanie logiki biznesowej.) W 1995 światło dzienne ujrzała książka Design Patterns katalogująca 23 wzorce projektowe dotyczących inżynierii oprogramowania. Niezwykle ważną rzeczą, prócz samych wzorców, którą wniosła wspomniana publikacja, był aparat pojęciowy umożliwiający rozpoznawanie oraz klasyfikowanie nowych pojawiających się wzorców projektowych. Od tego momentu nastąpiła gwałtowna eksploracja tego tematu przez różnych autorów.

Design Patterns została napisana przez czterech autorów i przyjęło się określać ich jako Gang of Four (GoF), natomiast wzorce przez nich skatalogowane jako Wzorce GoF.
Ich charakterystyczną cechą jest, że dotyczą konstrukcji programistycznych używanych do poprawy jakość tworzonego kodu. Wzorce te dotyczą współpracy obiektów w obrębie jednego systemu.
Umiejętne posługiwanie się tymi wzorcami jest absolutnym minimum kompetencji programisty i bazą dzięki której może on efektywnie korzystać z kolejnych technologii. Wzorce GoF są podstawą piramidy, na której opiera się praca i rozwój programisty. Istnieje możliwość wzięcia udziału w Weekendowych Warsztatach z Wzorców GoF, o których wspominałem w poprzednim wpisie.

Klienci stawiali przed programistami coraz większe wymagania. Systemy informatyczne musiały mieć coraz większą funkcjonalność, stawały się coraz większe. Dało to impuls do rozwoju technologii takich jak J2EE czy .NET. Technologie dostarczają wiele możliwości, czasem zbyt wiele,...technologie bywają niedoskonałe. W przypadku J2EE te niedoskonałości i problemy z użytkowaniem spowodowały powstawanie wzorców projektowych w odniesieniu do tej konkretnej technologii, czego egzemplifikacją stała się przełomowa publikacja Core J2EE Patterns.

Z drugiej strony skala systemów informatycznych, niezależnie od użytej technologii rodziła problemy, których nie rozwiązywały klasyczne Wzorce GoF (GoF, jak pamiętamy, dotyczyły przede wszystkim efektywnej współpracy obiektów pomiędzy sobą w obrębie systemu). Zastosowanie Wzorców GoF w dużym systemie, rzeczywiście ułatwiało pracę i czyniło go bardziej elastycznym, lecz już sama jego rozległość wymagała dodatkowego podejścia - podejścia globalnego, podejścia od strony architektury całego systemu. System miał wykonywać pewne zadania, prezentować dane użytkownikowi i trwale przechowywać wyniki swoich działań. Rozwinęły się: koncepcja warstw w systemie informatycznym oraz pomysły na udostępnianie danych klientom oraz na ich trwałe przechowywanie. Klasyką w tym obszarze jest publikacja Martina Fowlera Patterns of Enterprise Application Architecture, w której autor opisuje wzorce projektowe w aplikacjach klasy enterprise. Pozycja ta daje globalny pogląd na architekturę systemów informatycznych. Wzorce tam opisywane, na własny użytek i dla odróżnienia od GoF, nazywam Wzorcami Architektonicznymi. Warto nadmienić, że Martin Fowler przy gotowuje kolejną publikację na temat Wzorców Architektonicznych. Nie wiadomo kiedy się ona ukaże, jednak z postępem prac można zapoznać się na blogu Fowlera.

Problemy związane z architekturą aplikacji, z użytkowaniem technologii J2EE sprowokowały co najmniej trzy bardzo wartościowe następstwa. Po pierwsze: "stworzyły" środowisko sprzyjające rozwojowi wzorców projektowych. Po drugie: jako implementacja konkretnych Wzorców Architektonicznych powstały frameworki takie jak: Struts, Hibernate, JDO, Spring Framework, WebWork, iBatis i wiele innych. Po trzecie: w konsekwencji popularności frameworków rozpoczął się proces standaryzacyjny JCP, który zaowocował specyfikacją JEE5.

Dalszy rozwój systemów informatycznych uświadomił wszystkim zaangażowanym, że żadna z istniejących technologii jest w jakiś szczególny sposób uprzywilejowana. Jakkolwiek by nie argumentować każda będzie miała swoich zwolenników i przeciwników. Można się z tym poglądem nie zgadzać, ale nie sposób zaprzeczyć, że istnieje jego namacalna konsekwencja: w strukturze informatycznej przedsiębiorstwa działają systemy różnego typu stworzone w różnych technologiach. I choć najbardziej popularne rozwiązania dostarczają sposobów na obsługę całego infrastruktury informatycznej, to jednak każde przedsiębiorstwo ma swoją historię i nikt przy zdrowych zmysłach nie będzie zmieniał sprawnie działającego systemu tylko dlatego, że został napisany w Fortranie a nie w Javie.

Zatem każdy nowy system musi sprawnie komunikować się z istniejącą infrastrukturą. Wymaganie to spowodowało odkrywanie wzorców dotyczących integracji systemów informatycznych. Tę gałąź wzorców projektowych nazywam Wzorcami Integracyjnymi, a jako programową publikację można podać Enterprise Integration Patterns napisaną przez Gregora Hohpe'a i Bobbyego Woolfa.

Poszukiwanie pewnej powtarzalności w tworzeniu systemów informatycznych, mające na celu optymalizowanie określonej wcześniej funkcji oceny, pojawia się na każdym etapie złożoności nie zależnie od tego, czy jest to oprogramowanie desktopowe czy system obsługujący wielką korporację.

Okazuje się, że stosowanie konstrukcji architektonicznych to jedna część układanki. Pracując w różnych projektach programiści wypracowali swego rodzaju wiedzę plemienną, która pomaga im w pracy. Zaznaczam, że nie chodzi tutaj o wzorce projektowe, które rozwiązują konkretne problemy, chodzi raczej o sposób posługiwania się wzorcami w możliwie efektywny sposób.
Odkrycie strategii postępowania programistów wiąże się ze spostrzeżeniem, iż kod źródłowy jest częściej czytany niż pisany. Zatem sposób pisania kodu, konwencje, standardy, w tym sposób używania wzorców projektowych jest kolejnym obszarem, w którym można poszukiwać powtarzalności.
Doświadczony programista przechodząc z projektu do projektu będzie przenosił pewne pomocne nawyki mające ułatwić mu tworzenie oprogramowania. Dalej, doświadczony lider będzie dbał o to, aby uspójniać te nawyki wśród zespołu. Po co? Celem jest tu efektywna komunikacja. Jeden programista sprawniej pracuje posługując się sposobem kodowania który zna, zespół sprawnie pracuje posługując się tym samym stylem kodowania, nowa osoba w projekcie łatwiej wdroży się do zadań jeśli będzie mogła czytać kod wg pewnych, z góry określonych, zasad.
Wzorce wyodrębnione na tym poziomie zostały zaprezentowane przez Kenta Becka w książce
Implementation Patterns.

Wzorce Implementacyjne to kolejny kamień milowy w poszukiwaniu powtarzalnych schematów podczas wytwarzania oprogramowania. Dotyczą one sposobów posługiwania się językiem oraz wzorcami projektowymi, a nie konkretnej architektury oprogramowania. Używając porównania do budownictwa: Wzorce Projektowe mówią jakich cegieł należy używać, Wzorce Implementacyjne wskazują jak najlepiej pchać taczkę.


Dalsza dekompozycja


Richard Dawkins w swojej książce Samolubny gen stawia hipotezę, że rozwój nie jest progresywny lecz ekspansywny. Nie przebiega zgodnie z odgórnie ustalonym planem lecz odbywa się poprzez najlepsze dopasowanie do sytuacji bieżącej. Jednostką rozwoju nie jest istota wysokiego rzędu np. człowiek, lecz najbardziej elementarna jednostka - gen. Gen maksymalizuje swoje korzyści w danej chwili, a rozwój osobników wyższego rzędu jest jedynie skutkiem "samolubnych" zachowań genu.

Zastanawia mnie czy ta sama zasada obowiązuje, jawnie bądź w ukrytej postaci, podczas wytwarzania oprogramowania? Bytem wyższego rzędu jest system informatyczny, lecz kto jest samolubnym genem? Obiekt, czy może...programista? Kto dąży to maksymalizowania swoich korzyści w danej chwili? Dawkinsowy gen zachowuje się samolubnie, gdyż pragnie przetrwać, przekazać swój materiał genetyczny. Rozważając programowanie obiektowe musimy zrezygnować z przypisywania obiektom cech genów, gdyż jest obiekt jest ze swej strony bierny w obszarze replikacji. A zatem programista? Czy programista jest samolubnym genem? Zaznaczam, że nie chodzi to celowe działanie na szkodę, lecz o nieświadomy mechanizm funkcjonowania. Jeśli programista spełnia funkcję genu, to materiałem genetycznym musi być kod, który jest przez niego pisany!

Końcowy produkt jest zatem wypadkową: wymagań klienta, budżetu, ram czasowych oraz samolubności programisty. O ile z zewnątrz, w działaniach objawiających się bezpośrednio użytkownikowi, pierwsze cztery czynniki mają znaczącą rolę, o tyle wewnątrz systemu największe znaczenie ma samolubność programisty. Na czym miałaby ona polegać? Na przekazaniu jak największej ilości swoich koncepcji, rozwiązań, pomysłów do kodu systemu. Z kolei koncepcje te zależą od indywidualnych predyspozycji, umiejętności stosowania Wzorców Projektowych i Implementacyjncych,...humoru, itd. Stąd najkorzystniej dla projektu jest dbać o jakość przekazywanych koncepcji dbając o rozwój programistów.

Wspomniany Chirstopher Alexander w książce Timeless Way of Building przedstawia, podobny w idei, koncept, który nazywa porządkiem naturalnym.
(Przykład za Christopher Alexander czyli w poszukiwaniu doskonałości) Wyobraźmy salę wypełnioną krzesłami w uporządkowany sposób. Chcąc umieścić na sali jak najwięcej osób, można identyczne krzesła ustawić równo w rzędach optymalnie wypełniając salę. Na sali zmieści się wiele osób, ale nie wszystkim będzie wygodnie.
Z drugiej strony można poprosić osoby aby przyszły z własnymi krzesłami i usiadły możliwie najbliżej siebie. W tej sytuacji na sali będą krzesła różnego typu: duże i małe, odległości między nimi będą różne - w zależności od potrzeb siedzących, lecz wszyscy będą się starali, aby na sali zmieściło się najwięcej osób. Takie uporządkowanie Alexander nazywa porządkiem naturalnym. Charakteryzuje się on tym, każda osób ma tyle miejsca ile potrzebuje oraz występuje sprzężenie zwrotne z całością systemu (chęć zmieszczenia jak największej ilości osób). Charakterystyczną cechą porządku naturalnego jest to, że za każdym razem będzie przebiegał według tych samych reguł i za każdym razem efekt końcowy będzie nieco inny od poprzedniego.

W obu przedstawionych koncepcjach kształt ostatecznego systemu jest sterowany przez działania programisty. Alexander dokłada jednak jedną drobną, acz kluczową rzecz: sprzężenie zwrotne.
Można zatem wnioskować, że jeżeli każdy programista ma dokładną świadomość ostatecznego celu, ostatecznego kształtu systemu i dbając przekazywanie własnych koncepcji w kodzie, weźmie pod uwagę to sprzężenie zwrotne, to jakość wytworzonego oprogramowania wzrośnie.

Koncept sprzężenia zwrotnego uwidacznia się w programowaniu między innymi w takich podejściach jak Continuous Integration, Continuous Testing, czy Test-Driven Development. Całość podejścia do wytwarzania oprogramowania wywiedzionego z porządku naturalnego uwidacznia się w metodykach Agile.
Należy podkreślić jednak niezwykle istotną rzecz. We wspomnianym przykładzie nt. porządku naturalnego, to ludzie przynosili własne krzesła i ustawiali je optymalnie do własnych potrzeb, biorąc pod uwagę całość systemu. Również w podejściu Agile ciężar odpowiedzialności przeniesiony został na programistę. Już nie procedury sterują projektem, lecz ludzie. Wymaga to od programisty wiele uwagi, dojrzałości, a przede wszystkim odpowiedzialności. Wymaga również kompetencji innego rodzaju niż biegłość techniczna w stosowaniu określonych technologii, Wzorców Projektowych, czy Implementacyjnych.

Poza kodem źródłowym


W poszukiwaniu powtarzalności podczas wytwarzania oprogramowania przeszliśmy ścieżkę od Wzorców GoF, poprzez Wzorce Architektoniczne, Implementacyjne, aż do powtarzalnych procesów Agile, gdzie programista tworząc kod nieustannie bierze pod uwagę końcowy kształt systemu.
Każdy programista wie, że nauka wymienionych rzeczy zabiera wiele czasu. Moim zdaniem minimum 2 lata, a to i tak w odpowiednich warunkach. Jednym zabiera to wspomniane 2 lata, innym dłużej, a może i krócej, wielu zniechęca się po drodze. Lecz ci, którym się udało zaczęli programować w inny sposób. Jaka jest zatem różnica pomiędzy stanem początkowym, a końcowym w ścieżce rozwoju programisty i gdzie tej różnicy poszukiwać?
Z pewnością różnica nie polega na znajomości wzorców, bo wiedza teoretyczna może nie zmienić się przez ten czas. Ale przecież różnica istniej! Widać ją w sposobie kodowania, w sposobie podejścia do problemu, w jakości wytwarzanego oprogramowania.
Być może chodzi o to, że taki programista czuje programowanie, myśli poprzez wzorce, widzi je i potrafi zastosować w praktyce. Następuje zatem zmiana w sposobie myślenia (o pewnych jej aspektach pisałem w artykule Metaprogramy w tworzeniu oprogramowania).

Skoro programista czujący programowanie ma pewien nowy sposób myślenia, a jest wielu takich programistów, to można wnioskować, że istnieje pewien zbiór sposobów postępowania czy myślenia, charakterystyczny dla tych ludzi. Z pewnością każdy robi to w nieco inny sposób, ale skoro potrafią otrzymywać identyczne wyniki w postaci fantastycznie napisanego kodu, to jest jakaś część wspólna tych strategii. Zatem istnieją pewne powtarzalne wzorce, coś jak najlepsze praktyki skutecznych programistów, które charakteryzują ludzie osiągających sukcesy w naszej branży.
Odkrycie tych wzorców jest moim zdaniem kolejnym wyzwaniem przed inżynierią oprogramowania, kolejnym etapem odkrywania i stosowania optymalnych metod pracy.
Kolejne kroki usprawniania pracy wiodą od udoskonalania narzędzi i metod do rozwoju programisty. Tak już jest, że jakkolwiek skomplikowany by system nie był, w centralnym punkcje i tak znajduje się człowiek.

Wednesday, November 5, 2008

Weekendowe Warsztaty Wzorce Projektowe

Serdecznie zapraszamy na „Warsztaty weekendowe poświęcone wzorcom projektowym”. Jest to oferta specjalna dla użytkowników portalu Goldenline.pl i czytelników mojego bloga i bloga Mariusza. Tej oferty nie znajdziesz na stronie BNS IT!

Warsztaty umożliwiają nabycie praktycznych umiejętności tworzenia aplikacji z użyciem wzorców projektowych w języku Java.

Zobacz program warsztatów


Czego potrzebujesz?
• Notebook z kartą wi-fi
• Zapału, chęci i otwartego umysłu

Twoja inwestycja to jedyne: 800 zł.

Nigdzie nie znajdziesz szkolenia ani warsztatów w takiej cenie i jakości.

Powiedz znajomym.

Gdzie?
Warszawa, 13-14.12.2008
Wrocław, 13-14.12.2008
Kraków, 13-14.12.2008

Kontakt:
m.sieraczkiewicz [[[[[ MAUPA ]]]]]] bnsit.pl
+48 500 189 752
http://www.bnsit.pl

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