Showing posts with label design patterns. Show all posts
Showing posts with label design patterns. Show all posts

Friday, February 7, 2014

Nice Move With ValueSetter

I've been working on software architecture refactoring for last couple weeks. During a workshop one of architects showed me an interesting approach.

We were applying MVP-like style and then faced with the question: How a Presenter should tell a View to change its state?

Brute force approach looks like following:
public class Presenter {

 private View view;

 public void doSomething() {

//Here, services are processing something really important

 view.setTitle( "Updated" );
 view.setName( "New name" );
 }
}

public class Form extends JFrame implements View {

 private JTextfield nameTf;

 @Override
 public void setTitle( String title ) {
  setTitle( title );
 }

 @Override
 public void setName( String name ) {
  nameTf.setText( name );
 }
}
Well, this is actually ok, but the more components you have on the form, more verbose View interface becomes.

The move I am talking above is encapsulating a setter into separate object. Look here:
public abstract class ValueSetter {

private static Map<Class<?>, ValueSetter> SETTERS;
 
 static {
  SETTERS = new HashMap<Class<?>, ValueSetter>();
  SETTERS.put(JTextField.class, new JTextFieldSetter());
 }
 
 public static ValueSetter get( Class<?> type ) {
  if( SETTERS.containsKey(type) ) {
   return SETTERS.get(type);
  }
  
  throw new IllegalArgumentException( "Missing ValueSetter for " + type );
 }
 
 public abstract void set( Object target, Object value );

 private static class JTextFieldSetter extends ValueSetter {

  @Override
  public void set(Object target, Object value) {
   ((JTextField)target).setText( (String) value );
  }
 }

 private static class JFrameSetter extends ValueSetter {

  @Override
  public void set(Object target, Object value) {
   ((JFrame)target).setTitle( (String) value );
  }
 }
}
Now it is possible to write more elegant code:
public class Form extends JForm implements View {

 private Map<String, JComponent> components;

 @Override
 public void updateComponent(String id, Object value) {
  JComponent component = components.get(id);
  ValueSetter.get(component.getClass()).set(component, value);  
 }
}
So thanks Marcin for this nice tip and fresh idea for me ;)

Tuesday, October 29, 2013

A Thought on Design Patterns

We were told a design pattern is a solution of a problem in a given context.


The metaphor above focuses our thinking on a space of a given problem. But if we extended a view a little more, we would see there is one more thing: tools.


So, a design pattern is a solution of a problem in a given context with given tools usage. And tools are obviously programming languages we use. Observing programming languages evolution (as I remember a great piece of it was given here), we may arrive at the conclustion that design patterns try to solve lacks of programming languages.

From this point of view moving to a next programming language may cause a problem disappears.


So don't try to introduce patterns to solve the problem you faced with. Find a technology where the problem is gone. This is a way I understand the Polyglot Programming movement. I think it is the right direction.

Tuesday, August 9, 2011

Interfejs budowniczego

Na przykład tutaj można dowiedzieć się o niezwykłych korzyściach stosowania wzorca Builder (na przykład do wypiekania pizzy:)). Używam zatem, ale ten budowniczy mi więcej przeszkadza niż pomaga. Można pokusić się o stwierdzenie, że to tylko dodatkowa i nad wyraz rozdmuchana warstwa pośrednia, która niczego użytecznego nie wnosi. Ot, tworzy kilka obiektów i już.

Jak wspomniałem plusy dodatnie i ujemne budowniczego były już nie raz szeroko omawiane. Mnie interesuje odpowiedź na inne pytanie: W jaki sposób zaprojektować interfejs budowniczego?. To znaczy:
  • Jak zdecydować jakie metody powinien mieć budowniczy?
  • Jak go sensownie używać (pragmatycznie, nie sztuka dla sztuki), aby pomagał, a nie przeszkadzał?


Ponieważ debugowałem blog Sławka, żeby podłączyć syntaxhighlightera, to pożyczyłem sobie Order i OrderItem do przykładu:).

Koncept jest następujący: mamy aplikację do składania zamówień na produkty. Zamówienie składa się z grup produktowych, a te składają się z produktów, jak na rysunku.

Dla uproszczenia załóżmy, że jest to aplikacja konsolowa. Właściciel oczekuje, że będzie pracował z aplikacją następująco:
Podaj identyfikator zamówienia: Paczka-E112
Podaj priorytet zamówienia: 1
Podaj nazwy grup produktowych: spożywcze chemia zabawki
Podaj ilość porduktów dla grupy 'spożywcze': 2
Dodaj produkt dla grupy 'spożywcze': sałata 2,45zł 2
Dodaj produkt dla grupy 'spożywcze': morele 6,79zł 1
Podaj ilość porduktów dla grupy 'chemia': 3
...


Startuję z następującym zestawem klas:
public class Order {
private String id;
private int priority;
private Set itemGroups = new HashSet();
}
public class ItemGroup {
private String name;
private Set items = new HashSet();
}
public class Item {
private String name;
private Money price;
private int quantity;
}


Za wykonanie zadania Dodawanie zamówienia, odpowiedzialną uczyńmy klasę:
public class UserInterface { 
private Set orders = new HashSet();
public void addOrder() {
// TOIMPL
} 
public static void main(String[] args) {
new UserInterface().addOrder();
}
}

W pierwszym podejściu kod, który działa, mógłby wyglądać na przykład tak:
public void addOrder() {
Scanner scanner = new Scanner( System.in );
System.out.print( "Podaj identyfikator zamówienia: " );
String orderId = scanner.nextLine();
System.out.print( "Podaj priorytet zamówienia: " );
int orderPriority = Integer.parseInt( scanner.nextLine() );
Order order = new Order( orderId, orderPriority );
System.out.print( "Podaj nazwy grup produktowych: " );
String[] itemGroupsNames = scanner.nextLine().split( " " );
for ( int i = 0; i < itemGroupsNames.length; ++i ) {
ItemGroup itemGroup = new ItemGroup( itemGroupsNames[ i ] );
System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " );
int itemsAmount = Integer.parseInt( scanner.nextLine() );
for( int j = 0; j < itemsAmount; ++j ) {
System.out.print( "Dodaj produkt dla grupy '" + itemGroupsNames[ i ] + "' : " );
String[] itemParams = scanner.nextLine().split( " " );
String itemName = itemParams[ 0 ];
Money itemPrice = Money.parseMoney( itemParams[ 1 ] );
int itemQuantity = Integer.parseInt( itemParams[ 2 ] ); 
Item item = new Item( itemName, itemPrice, itemQuantity );  
itemGroup.addItem( item );
}
order.addItemGrup( itemGroup );
}
orders.add( order ); 
}
Po pobieżny przyjrzeniu się temu rozwiązaniu, można zauważyć, że nastąpiło tu pewne pogmatwanie. Komunikowanie się z użytkownikiem (wyświetlanie komunikatów i odbieranie wpisanych danych) zostało zmiksowane z kodem biznesowym (budowanie zamówienia). W pierwszym zachwycie nad wzorce Builder mam ogromną ochotę go użyć! UWAGA jeśli przychodzi ci go głowy napisać klasę OrderBuilder z jedną metodą build mającą kilkanaście parametrów, to nie tędy droga. Wyszedł by z tego nie budowniczy ale jakieś fabrykoniewiadomoco. Wracając do tematu: wprowadźmy zatem ad hoc budowniczego (bo ponoć to dobra praktyka). Budowniczy ten mógłby wyglądać następująco:
public class OrderBuilder {
private Order order;
public void newOrder( String orderId, int orderPriority ) {
order = new Order( orderId, orderPriority );
}
public Order getOrder() {
return order;
}
public void addItemGroup( String name ) {
order.addItemGrup( new ItemGroup( name ) );
}
public void addItem( String groupName, String name, Money price, int qty ) {
ItemGroup group = order.findItemGroup( name );
group.addItem( new Item( name, price, qty ) );
}
}

Kluczowe pytanie: Czy ten budowniczy jest pomocny?

Już zerkając na kod budowniczego, można mieć wątpliwości. Właściwie nie robi nic ponad, enkaspulację wywołań konstruktorów oraz metod dodających. Ok, można go sobie mokować i dodawać nowe implementacje, ale zobaczmy jak wygląda kod, który użytkuje tego budowniczego:
public void addOrder() {
Scanner scanner = new Scanner( System.in );
System.out.print( "Podaj identyfikator zamówienia: " );
String orderId = scanner.nextLine();
System.out.print( "Podaj priorytet zamówienia: " );
int orderPriority = Integer.parseInt( scanner.nextLine() );
OrderBuilder builder = new OrderBuilder();
builder.newOrder( orderId, orderPriority );
System.out.print( "Podaj nazwy grup produktowych: " );
String[] itemGroupsNames = scanner.nextLine().split( " " );
for ( int i = 0; i < itemGroupsNames.length; ++i ) {
builder.addItemGroup( itemGroupsNames[ i ] );
System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " );
int itemsAmount = Integer.parseInt( scanner.nextLine() );
for( int j = 0; j < itemsAmount; ++j ) {
System.out.print( "Dodaj produkt dla grupy '" + itemGroupsNames[ i ] + "' : " );
String[] itemParams = scanner.nextLine().split( " " );
String itemName = itemParams[ 0 ];
Money itemPrice = Money.parseMoney( itemParams[ 1 ] );
int itemQuantity = Integer.parseInt( itemParams[ 2 ] ); 
builder.addItem( itemGroupsNames[ i ], itemName, itemPrice, itemQuantity );
}
}
orders.add( builder.getOrder() );
}
Jest tak samo brzydki jak był! Budowniczy nie wniósł znaczącego wkładu do tego kodu. Moim zdaniem interfejs budowniczego należy projektować z perspektywy klienta. Pisząc klient mam na myśli usługę, która akurat budowniczego będzie wykorzystywać - w tym przypadku jest to klasa UserIterface. Interfejs budowniczego powinien być wygodny dla klienta.

Krok 1: Dlaczego ItemGroup i Item trzeba dodawać pojedynczo?

Wcale nie trzeba...Przecież budowniczy może mieć metody, które przyjmą input, który przyszedł od użytkownika i samodzielnie go zinterpretuje. Pytanie: Czy czasem znów nie dojdzie do pomieszania warstw? Nie nie dojdzie. Przeanalizujmy odpowiedzialności poszczególnych klas:
  • Order, ItemGroup, Item - to model dziedziny, reprezentuje rzeczywistość
  • UserInterface - komunikuje się z użytkownikiem; odbiera od niego żądania (wpisy z konsoli) i przekazuje je do odpowiednich elementów niżej; prezentuje użytkownikowi wynik działania systemu
  • OrderBuilder - służy do złożenia zamówienia z mniejszych elementów
Zatem jeśli budowniczy może spokojnie przyjąć stringi podane przez użytkownika, a potem je zinterpretować. Dopóki budowniczy nie zacznie pisać na konsolę, to odpowiedzialność klas zostanie zachowana. Zobaczmy jak to mogłoby wyglądać:
public class OrderBuilder {
private Order order;
public void newOrder( String orderId, int orderPriority ) {
order = new Order( orderId, orderPriority );
}
public void addItemGroups( String itemsGroupsInput ) {
String[] itemGroupsNames = itemsGroupsInput.split( " " ); 
for ( int i = 0; i < itemGroupsNames.length; ++i ) {   
order.addItemGrup( new ItemGroup( itemGroupsNames[ i ] ) );
}
}
public void addItem( String itemParamsInput ) {
String[] itemParams = itemParamsInput.split( " " );
String groupName = itemParams[ 0 ];
ItemGroup group = order.findItemGroup( groupName );
Money price = Money.parseMoney( itemParams[ 1 ] );
int qty = Integer.parseInt( itemParams[ 2 ] );   
group.addItem( new Item( groupName, price, qty ) );
}
public Order getOrder() {
return order;
}
}
Jak widać budowniczy zmądrzał nieco, potrafi zrobić coś więcej niż proste dodawanie. A jak wygląda korzystanie z niego?
public void addOrder() {
Scanner scanner = new Scanner( System.in ); 
System.out.print( "Podaj identyfikator zamówienia: " );
String orderId = scanner.nextLine();
System.out.print( "Podaj priorytet zamówienia: " );
int orderPriority = Integer.parseInt( scanner.nextLine() );
OrderBuilder builder = new OrderBuilder();
builder.newOrder( orderId, orderPriority );
System.out.print( "Podaj nazwy grup produktowych: " );
String itemsGroupsInput = scanner.nextLine();
builder.addItemGroups( itemsGroupsInput );
String[] itemGroupsNames = itemsGroupsInput.split( " " );
for ( int i = 0; i < itemGroupsNames.length; ++i ) {
System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " );
int itemsAmount = Integer.parseInt( scanner.nextLine() );
for( int j = 0; j < itemsAmount; ++j ) {
System.out.print( "Dodaj produkt dla grupy '" + itemGroupsNames[ i ] + "' : " );
String itemParamsInput = scanner.nextLine();
builder.addItem( itemParamsInput );
}
}
orders.add( builder.getOrder() );
}
Choć nie idealnie to jednak jest nieco lepiej - metoda mieści się na jednym ekranie:)

Krok 2: Chodzenie po strukturze zamówienia

To, co nam bruździ to fakt, trze trzeba dodać Items do każdej z ItemGroup. Więc najpierw za pomocą OrderBuilder.addItemGroups tworzone są wszystkie grupy, ale jeszcze dodatkowo UserInterface przetrzymuje sobie jeszcze tablicę nazw grup produktowych, że by po nich przeiterować i dodać pozycje zamówienia. Wydaje się to trochę nienaturalne, ponieważ ta sama informacja jest przechowywana w dwóch różnych formach (String[] i Set) w dwóch różnych miejscach (UserInterface i OrderBuilder). Skoro chcemy tylko chodzić po wewnętrznej strukturze zamówienia (przesuwać się do kolejnych grup i dodawać produkty), to dlaczego nie wyposażyć budowniczego w tę możliwość. Niech budowniczy udostępni zestaw metod do chodzenia po grupach produktowych. Zobaczmy:
public class OrderBuilder {
private Order order;
private Iterator groupIterator; 
private ItemGroup currentGroup;
public void newOrder( String orderId, int orderPriority ) {
order = new Order( orderId, orderPriority );
}
public void addItemGroups( String itemsGroupsInput ) {
String[] itemGroupsNames = itemsGroupsInput.split( " " );  
for ( int i = 0; i < itemGroupsNames.length; ++i ) {   
order.addItemGrup( new ItemGroup( itemGroupsNames[ i ] ) );
}
groupIterator = order.getItemGroups();
}
public void addItem( String itemParamsInput ) {
String[] itemParams = itemParamsInput.split( " " );
String groupName = itemParams[ 0 ];
Money price = Money.parseMoney( itemParams[ 1 ] );
int qty = Integer.parseInt( itemParams[ 2 ] );   
currentGroup.addItem( new Item( groupName, price, qty ) );
}
public Order getOrder() {
return order;
} 
public boolean hasNextItemGroup() {
if ( groupIterator == null ) {
return false;
}  
return groupIterator.hasNext();
}
public void moveNextItemGroup() {
currentGroup = groupIterator.next();
} 
public String getCurrentItemGroupName() {
return currentGroup.getName();
} 
}
Natomiast korzystanie z budowniczego wygląda następująco:
public void addOrder() {
Scanner scanner = new Scanner( System.in ); 
System.out.print( "Podaj identyfikator zamówienia: " );
String orderId = scanner.nextLine();
System.out.print( "Podaj priorytet zamówienia: " );
int orderPriority = Integer.parseInt( scanner.nextLine() );
OrderBuilder builder = new OrderBuilder();
builder.newOrder( orderId, orderPriority );
System.out.print( "Podaj nazwy grup produktowych: " );
builder.addItemGroups( scanner.nextLine() );
while ( builder.hasNextItemGroup() ) {
builder.moveNextItemGroup();
System.out.print( "Podaj ilość produktów dla grupy '" +       builder.getCurrentItemGroupName() + "' : ");
int itemsAmount = Integer.parseInt( scanner.nextLine() );
for( int j = 0; j < itemsAmount; ++j ) {
System.out.print( "Dodaj produkt dla grupy '" + builder.getCurrentItemGroupName() + "' : " );
builder.addItem( scanner.nextLine() );
}
}
orders.add( builder.getOrder() );
}
Teraz już klasa UserInterface zajmuje się wyłącznie komunikacją z użytkownikiem, natomiast budowniczy zajmuje się składaniem zamówienia do kupy. Każda z klas zachowała swoją odpowiedzialność, a z używanie budowniczego rzeczywiście przynosi korzyści klientowi większe niż tylko enkapsulacja konstruktorów i dodawania do list. Dzięki wykorzystaniu budowniczego kod kliencki staje się prostszy i bardziej przejrzysty. Dla porządku trzeba dodać, że odpowiedzialność budowniczego została zwiększona. Potrafi on teraz również chodzić po strukturze zamówienia. Jest więc jednocześnie Iteratorem. Dalej warto metody iterator wydzielić do osobnego interfejsu, ale to już nieco inna historia.

Wednesday, July 6, 2011

Poza wzorcami projektowymi

To jest odległa kontynuacja artykułu Język wzorców.

Nie pamiętam już u kogo, ale wyczytałem, że jedną z przyczyn powstania wzorców projektowych, było rozwiązanie różnego rodzaju bolączek technologicznych albo bolączek konkretnych języków programowania. Zacząłem się nad tym zastanawiać. I tak:
  • Null Object - bo w C++ referencja musi być zainicjowana; wyeliminowane przez Java, C# (nieścisłość, @see Komentarz pod postem Chrisphera Daniela)
  • State - bo implementacja maszyny stanów oparta o tablicę wskaźników do funkcji oraz serię switch/case
  • jest dość trudna w utrzymaniu
  • Interpreter - jaki tam wzorzec;)? sposób na parsowanie ustrukturyzowanych gramatyk i tyle; wyeliminowane przez tony bibliotek narzędziowych
  • Flyweigh - bo nie ma kasy na więcej pamięci
  • Command - w C# wykoszony przez delegaty (mam na myśli ten kontekst użycia, w którym robimy execute(), a nie przechowujemy komendę, aby ją potem handle() )
  • Właściwie wszystkie wzorce opisane w Core J2EE Patterns i PoEAA - zastąpione przez biblioteki, frameworki i technologie


Czym zatem są wzorce projektowe? Mówi się, że rozwiązaniami typowych problemów. Rozwiązaniami, które wg Alexandra, można stosować za każdym razem nieco inaczej (nieco inna implementacja), ale otrzyma się poprawne rozwiązanie konkretnego problemu. W porządku - może postęp technologiczny modyfikuje tylko implementacje wzorców, lecz idea zostaje podobna. Może jednak nie - czy w wzorzec zamknięty w wszytywne API biblioteki wciąż jest wzorcem, czy tylko zwyczajnym narzędziem pomocniczym?

Jak powstają?
Skąd się biorą i jak powstają? Istnieją trzy elementarne zasady postępowania, które prędzej czy później doprowadzą do dobrego rozwiązania obiektowego:
Przestrzegaj opodwiedzialności
Jeśli nie potrafimy czegoś nazwać, to prawdopodobnie nie rozumiemy jak to działa. Nie znamy odpowiedzialności tego czegoś. W swoim kodzie dbaj o to, aby każda: zmienna, metoda, klasa, interfejs, pakiet, moduł miały określoną tylko jedną opodwiedzialność, która będzie wyrażona nazwą
Enkapsuluj
To, co podlega zmianom, powinno zostać opakowane (w metodę lub w klasę - w zależności od potrzeby). Enkapsulacja pozwala na łatwiejsze zarządzanie zmianą w kodzie. Nie mówiac już o testowaniu.
Preferuj kompozycję ponad dziedziczenie
Związki używania czynią kod bardziej elastycznym, czytelnym i testowalnym. Dodatkowo pozwalają na rozszerzanie funkcjonalności kodu run-time, a nie tylko poprzez przekompilowanie.


Jeśli przestrzegamy tych zasad, prawie na pewno powstanie dobry kod. Jest jeden warunek: trzeba stosować je bardzo skrupulatnie. Żadnego wkuwania nazw wzorców, diagramów, definicji, itd. Zamiast tego: żelazna dyscyplina w stosowaniu w/w zasad.

Wymienione reguły są dość niskopoziomowe, w całościowy proces ujmuje koncept, który nazwaliśmy roboczo Nautralnym porządkiem refaktoryzacji.
Koncept Nautralnego porządku refaktoryzacji


Pisał już o tym sporo Mariusz w artykułach: NPR - Idea rysunkowa, NPR - pod lupą, NPR: Compose Method, NPR: Extract Method, NPR: Refaktoryzacja do wzorców, NPR: Ewolucja architektury, więc nie będę się powtarzał.

Powtarzając konsekwentnie ten proces i stosując wspomniane już trzy metody, otrzymujemy naprawdę porządny kod. Zaczynają pojawiać się dobre rozwiązania. Dość często są to znane wzorce projektowe.

Morfizmy w kodzie
Zauważmy, że w charakterystyka wzorca projektowego zawiera między innymi Motywację/Intencję, Kontekst, Problem. Według definicji wzorzec to rozwiązanie problemu w danym kontekście. Ale przecież nieustanie zmienia się zarówno kontekst, jak i nasze zrozumienie problemu. Wszystko się zmienia!

Aktualne dobre rozwiązanie, które przywykliśmy nazywać wzorcem projektowym, jest tak naprawdę lokalnym optimum, jakieś funkcji (powiedzmy, że Funkcji kodu) w której zmiennymi są przynajmniej: Kontekst oraz Problem.

Tak zwany wzorzec rozwiązuje problem tylko na chwilę, dopóki Funkcja kodu nie ulegnie zmianie, a optimum nie wystrzeli w inną stronę. Mamy wiec do czynienia nie tyle z wzorcami, co z optymalnymi morfizmami w kodzie (niech, Ockham mi wybaczy). Zmiana Funkcji kodu, po której programując się ślizgamy powoduje, że dany morfizm przestaje być optymalny i musimy poszukać nowego optimum właśnie poprzez refaktoryzację.
Optymalne morfizmy


Wnioski
Czy poza gimnastyką umysłu wypływają stąd jakieś wnioski? Owszem, kilka.
  1. Istnieje nieskończenie wiele optymalnych morfizmów w kodzie - ponieważ zakres zmienności Problemów oraz Kontekstów jest również nieskończony; w związku z tym publikacje i dywagacje na temat nowych wzorców projektowych nigdy się nie skończą
  2. Refaktoryzacja musi trwać - ponieważ Funkcja kodu wciąż się przekształca, wciąż musimy poszukiwać optymalnych morfizmów; zaprzestanie tych działań spowoduje takie zniekształcenie tej funkcji, że znalezienie optymalnych morfizmów będzie niemożliwe i trzeba będzie ją wyrysować na nowo - czytaj napisać system raz jeszcze
  3. Ważniejsze jest zdyscyplinowane posługiwanie się wspomnianymi wyżej zasadami i procesem refaktoryzacji niż szastanie kolekcjami nazw wzorców
  4. Nie jest możliwe stworzenie czegoś takiego jak patterns language (por: On Patterns and Pattern Languages), bo optymalne morfizmy wciąż się przekształcają podążając za zmianami Funkcji Kodu

Thursday, June 16, 2011

Obiektowość dla embedded?

Często zdarza mi się wypełzać poza Javę, z której wyrosłem i ze zdziwieniem stwierdzam, że są inne obszary, w których ludzie mają podobne do naszych problemy i z utęsknieniem spoglądają w stronę gadżetów którymi dysponujemy.

Jakiś czas temu, konstruktywnie krytykując mój wpis, Sławek powiedział mniej więcej coś takiego: "Kto w obecnych czasach podaje w książkach przykłady w C++? Od razu widać, że old school"

Rzeczywiście, większość koncepcji związanych z: refaktoryzacją, TDD, clean code itd. nabrała obecnego kształtu w okół Javy i C# i ich community (tak, tak, pamiętam: Small Talk, C++, GoF - byli pierwsi :)). Jakby te języki były jedyne, dominujące, a inne to tylko marginalne ekstrawagancje. Zerknijmy na ten ranking. Jak widać Java i C# mają towarzystwo i to całkiem spore.

RTS, Entertainment
Mówię o systemach embedded, ale nie telefonach, grach i smartach, gdyż im (wg programistów embedded) bliżej raczej do PC-tów. Mówię o: systemach czasu rzeczywistego, systemach sterowania produkcją, oprogramowaniu obsługującym stacje bazowe w sieciach komórkowych, o komputerach pokładowych w samochodach, o sofcie który jest zainstalowany na naszych kartach kredytowych. Kto by pomyślał, że na takiej karcie jest procesor, a na nim system operacyjny, a na nim dziarsko działa maszyna wirtualna Javy? (@see Java Card)

C rządzi!
We wspomnianych wyżej domenach często króluje język C. Po pierwsze z powodów historycznych - ktoś zaczął pisać w C i tak już zostało.

W miarę rozrostu tego typu systemów, pojawiają poważne problemy z ich utrzymaniem. Miliony wierszy kodu, gęsto pociętych przez dyrektywy kompilatora, kompilacje warunkowe, ifdefy, makra tam, gdzie to tylko możliwe oraz specyficzny sposób oszczędnego nazywania zmiennych zdecydowanie utrudniają rozwijanie tego kodu. Te problemy powodują, że środowisko embedded coraz częściej rozgląda się za podejściem obiektowym, które pomoże tworzyć łatwiejszy w utrzymaniu kod. Napotykają jednak na pewne przeszkody, które budują przekonanie, że obiektowość nie jest dla embedded. Zebrałem nieco informacji nt. tych przeszkód. Niektóre z nich są rzeczywiscie zaskakujące, inne to raczej przekonania.

Brak kompilatorów
Do pewnego momentu nie było zwyczajnie technicznej możliwości skomplikowania kodu. Jeśli już jakieś istniały, to były tak zabugowane, że dyskwalifikowały się same przez się. Sytuację poprawił gcc, który popchnął sprawę nieco do przodu.

Koszt wejścia
O ile maszyna wirtualna Javy jest dla PC-tów darmowa, to za wersję na embedded trzeba płacić grube pieniądze. Doświadczenia zaprzyjaźnionych firm pokazują, że ta implementacja JVM jest koszmarnie wolna. Istnieją sprzętowe implementacje (tak! sprzętowe) maszyny wirtualnej, ale niewiele o nich wiem.

Niedoskonałość sprzętu
W porównaniu do procesorów z kategorii ix86, procesory dla systemów wbudowanych są dość proste (tylko w porównaniu :)). Pisząc usługę EJB nie zastanawiamy się, czy aby procesor działa poprawnie, czy nie będzie błędu w działaniu urządzenia. W aplikacjach embedded niedoskonałość sprzętu jest częstym kłopotem.

Trudności w poszukiwaniu błędów
Tak powszechne zadanie jak poszukiwanie błędów również może nastręczać trudności, gdyż odbywa się to poprzez debugowanie oraz analizę kodu asemblera. Taka analiza jest zdecydowanie łatwiejsza dla kodu napisanego w C niż w C++.

Koronny argument - wydajność
Hmmm, trochę prawda, trochę mit. Tak myślę. Przeanalizujmy.

Po pierwsze:
Jak wspomniałem JVM jest rzeczywiście wolna. Z drugiej strony istnieje przekonanie, że kod napisany w C++ będzie działa duuuuużo wolniej. Tylko czy ktoś to rzeczywiście zmierzył? Czy ktoś napisał dwie tożsame implementacje, jedną w C, drugą w C++ i stwierdził to jednoznacznie?

Po drugie:
Przyczyną mogą być ograniczenia programowe i sprzętowe. Sporo zależy od tego jak jest napisany sam kompilator, czy potrafi wykorzystywać automagiczne sztuczki optymalizacyjne. Innym i często skutecznym sposobem zwiększania wydajności jest zmiana sprzętu. Ten jednak przenika do branży dosyć wolno. Powód jest trywialny - koszty. Nowy sprzęt - większe pieniądze, kolejny kompilator, kolejne bugi itd.

Po trzecie:
W rozwiązaniach embedded rzadko kiedy brakuje np. 5%, 10%, 20% wydajności. Jeśli już trzeba zwiększyć wydajność, bo powiedzmy, nie nadążamy z pomiarem temperatury pieca i grozi wybuchem, to trzeba ją zwiększyć kilkukrotnie. W takim przypadku obniżenie wydajności o parę, paręnaście procent, w związku z konstrukcjami obiektowymi, jest do przyjęcia. Jeśli efektem tego narzutu będzie łatwość utrzymania i rozbudowywania dylemat jest wart uwagi. Tak doszliśmy to wysłużonego już hasła: czytelność ponad wydajność. Nie oznacza to bynajmniej nonszalancji w szafowaniu pamięcią, lecz to aby w pierwszej kolejności koncentrować się na tworzeniu czytelnego kodu, a dopiero w drugiej mierzyć, profajlować i optymalizować.

Co dalej?
Jakie wyzwania stoją przed technikami refaktoryzacji i wzorcami, które w swoim najbardziej światowym wydaniu, utknęły gdzieś pomiędzy Javą a C#?
  • Jak pracować z hybrydowym legacy code, pisanym trochę w C trochę w C++?
  • Jak bezpiecznie przepisywać kod proceduralny na obiektowy?
  • Jakie standardy kodowania przyjąć dla C++, aby uniknąć pułapek wynikających z bogactwa języka
  • Jak sensownie rozwinąć narzędzia wspierające refaktoryzację. Sorki, ale VC++ i wsparcie dla refaktoryzacji to porażka. Visual Assist X jest super, ale to jeszcze nie to. Eclipse C/C++ nie próbowałem, Wind River Workbench jest ponoć dobry, ale i kosztuje słono
  • i cała reszta bajerów, których się w okół Javy i C# dorobiliśmy

Za różnymi problemami oraz przekonaniami kryją się ludzie, którzy poszukują skutecznych rozwiązań swoich problemów. Na pewno coś da się zrobić.

Thursday, December 17, 2009

Dziedziczenie i kompozycja



Ostatnio wygrzebałem w polskiej blogsferze namiar na Aristotle's Error or Agile Smile. Wynika z niego, że nasza wspaniała obiektowość jest dziełem przypadku (a co nie jest?), a ideologię o modelowaniu świata rzeczywistego dorobili spece od marketingu. Jak było w rzeczywistości nie dociekałem. Skoro już obiektowość, a wraz z nią sztandarowe dziedziczenie.

Dziedziczenie


Na Rysunek 1 jest fragment jakiegoś tam systemu. Pewnie każdy z nas widział podobne rzeczy choćby w Java API, które roi się od podobnych lub o wiele bardziej skomplikowanych konstrukcji.

Choć programiście całkiem nieźle używa się klas zaprojektowanych w ten sposób, to gdy programista chciałby na bazie takich konstrukcji rozwijać swoją aplikację (czytaj: dalej nieograniczenie dziedziczyć), to pojawia się kilka problemów:
  • Aby zrozumieć działanie metody MultiUserRemoteBookStore.findBook() zapoznać się z całą hierarchią dziedziczenia,
  • Faktycznie uruchamiany kod metody tej metody jest rozsiany pomiędzy wiele klas w hierarchii,

  • Wymusza się na nas użycie konstruktorów (parametrowych) z nadklas, których wcale nie mieliśmy zamiaru używać,


  • Dodatkowo nie wiadomo co robią te kostruktory; a nóż przy tworzeniu mojego obiektu coś wybuchnie…


  • Trudno przetestować jednostkowo nową klasę. No, bo jak tu zamokować kod wywoływany w testowanej metodzie poprzez super.findBook()?


Kompozycja

Większość problemów wynikających ze swobodnego dziedziczenia rozwiązuje kompozycja. Zamiast wyprowadzać nową klasę AcmeBookStore z MultiUserBookStore implementujemy główny interfejs BookStoreService a do potrzebnych metod odwołujemy się poprzez delegację. I kawałek kodu:

public class AcmeBookStore implements BookStoreService {

private MultiUserBookStore multiUserBookStore;

public Book findBook( String title ) {
//...
multiUserBookStore.findBook( title );
//...
}

}
(Na marginesie warto zauważyć, że w ten sposób stworzyliśmy implementację Dekoratora.) To rozwiązanie jest zdecydowanie bardziej czytelne i unit-testing-friendly. Kłopot pojawia się gdy nie w architekturze, z którą pracujemy nie istnieje odpowiednik interfejsu BookStoreService. Wtedy już, chcąc nie chcąc, zazwyczaj godzimy się godzimy się na dziedziczenie.

Programowanie poprzez interfejsy

Mając na uwadze w/w mogę zastanawiać się: w jaki sposób mogę projektować architekturę mojego kodu tak, aby nie generował problemów z dziedziczeniem. Pierwszą rzeczą, która przychodzi mi na myśl jest programowanie poprzez interfejsy. Nie oznacza to bynajmniej, że każda nowa klasa UserManager ma swój interfejs UserManagerService, albo (w wersji hardcore) z każdą nową klasą pojawiają się trzy nowe byty w systemie (UserManagerService, UserManagerImpl, AbstractUserManager). Zerknijmy na rysunek: W tej architekturze centralnym punktem systemu (podsystemu, biblioteki) jest interfejs – kontrakt, który mają realizować poszczególne implementacje. Specyfika implementacji np. RemoteBookStore uzyskiwana jest nie poprzez odpowiednie klasy narzędziowe np. RemotingUtilty. Dzięki temu można tworzyć kolejne implementacje specjalizujące się w coraz to nowych rzeczach, bez konieczności dziedziczenia.

Podsumowując

Ujawniły nam się następujące rzeczy:
  • Preferowanie kompozycji ponad dziedziczenie,
  • Programowanie poprzez interfejsy.
    Są to jedne z kluczowych paradygmatów programowania obiektowego. Mamy z nich następujące korzyści:
  • Kod jest czytelny


  • Kod jest otwarty na testowanie.



Ostatecznie pozostaje jedno pytanie: czy dziedziczenie jest złe? Nie, jest bardzo dobre... w pewnych kontekstach… Złe jest jego nadużywanie. Jak więc rozpoznawać, które konteksty są odpowiedni dla dziedziczenia? Pewnie można wykombinować jakieś obiektywne kryteria, ale ja proponuję znać się na intuicję: czy dziedziczenie uprościło czy zagmatwała architekturę? Czy łatwiej testować, czy trudniej? Czy przyjemniej się pisze, czy - przeciwnie - chce Ci się...

Tuesday, April 7, 2009

Nawyki programisty

Artykuł ukazał się w kwietniowym numerze Software Developer's Journal

Jak myślisz, co ma wpływ na Twoją skuteczność, kiedy programujesz? Wśród wielu czynników z pewnością wymienisz: wiedzę, umiejętności techniczne, doświadczenie, motywację, zaangażowanie, współpracę, chęć uczenia się i wiele, wiele innych. Na ogólnym poziomie wymienione rzeczy dzielą się na dwie grupy: czynniki techniczne oraz czynniki nietechniczne. Zakładamy, że skoro jesteś programistą, to doskonale wiesz, czego potrzebujesz w obszarze umiejętności technicznych. Dlatego proponujemy Ci przyjrzenie się umiejętnościom z drugiej grupy – umiejętnościom nietechnicznym.

Rzucamy wyzwanie


Zaprezentujemy kilka prostych czynności, które konsekwentnie stosowane każdego dnia mogą wprowadzić ogromną jakościową zmianę w Twojej pracy.
Nie próbujemy Cię przekonywać, nie prosimy, abyś uwierzył na słowo. Sprawdź
i przekonaj się sam.

Zawrzyjmy kontrakt, że przez najbliższe trzydzieści dni będziesz stosować opisywane nawyki, a potem ocenisz efekty. W tym względzie wymagamy sumienności. Trzydzieści dni to trzydzieści dni i ani jednego mniej. To bardzo niewielkie wymaganie, biorąc pod uwagę fakt, że jakość Twojej pracy znacząco wzrośnie. Jeśli zrobisz przerwę w trakcie eksperymentu, to zacznij odliczanie od nowa. Mamy umowę?

Nawyk 1. – Rozpoczynaj pracę o stałej porze


Nie bez powodu metodyki z nurtu Agile zalecają trzymanie się ustalonej długości iteracji. Gdy wykonujesz rzeczy regularnie, w określonym porządku – nabierasz rytmu. W ten sposób skupiasz się na rzeczach najistotniejszych i nie zaprzątasz sobie głowy nieustannym organizowaniem swojego dnia pracy, ponieważ masz pewien nawykowy schemat postępowania.

Wielu programistów ma elastyczny czas pracy. Muszą być być na swoim stanowisku, np.: od 10 do 16, a pozostałe godziny mogą odpracować w domu. To bardzo wygodne rozwiązanie, ale powoduje, że dużo energii poświęcają na zorganizowanie każdego dnia, ponieważ każdy dzień jest inny. Stosowanie stałego rozkładu dnia pozwoli Ci skoncentrować się na rzeczach najważniejszych. Na początek rozpoczynaj dzień pracy o stałej porze.

Nawyk 2. – Planuj swój dzień


Jak często zdarza Ci się mówić: Nie mam czasu? To zdanie to bardzo dobry pretekst lub wymówka. Masz czas. Masz dokładnie tyle samo czasu, co każdy inny programista na Ziemi – 24 godziny w ciągu doby (życie na innych planetach pomijamy). Najważniejsze pytanie brzmi – co robisz z czasem, który masz do dyspozycji?

Przeprowadź eksperyment. Przeznacz jeden dzień na przyglądanie się wszystkiemu, co robisz w trakcie pracy. Zanim cokolwiek zrobisz – zapisz to. Przykładowe czynności, które możesz zanotować: napisanie testu metody, napisanie implementacji metody, wdrożenie aplikacji na serwer testowy, wyszukanie informacji w Internecie, rozmowa telefoniczna, sprawdzanie poczty, rozmowa, problemy z oprogramowaniem lub sprzętem.

Zapisanie zajmie najwyżej kilka sekund, zatem koszt naszego eksperymentu jest znikomy. Na koniec dnia powinieneś mieć spis rzeczy, które robiłeś w ciągu dnia. Następnie oblicz, jaki procent czasu przeznaczyłeś na programowanie. Jak bardzo zdziwił cię wynik?
Niech planowanie dnia stanie się Twoim codziennym rytuałem. Zanim zaczniesz pracę zaplanuj, co będziesz robić danego dnia i ustal, co jest dziś najważniejsze. Przeanalizuj jak wiele zrobiłeś i zaznacz wykonane zadania w swoim planie. Być może właśnie postanowiłeś, że najpierw przeczytasz jakąś supermądrą książkę na temat zarządzania czasem, zainstalujesz odpowiednie oprogramowanie i dopiero wtedy będziesz gotowy do działania zgodnie z planem. Nic z tych rzeczy, absolutnie tego nie rób. Jesteśmy bardzo dumni z Twojej kreatywności, ale książki i oprogramowanie nie są Ci w tej chwili potrzebne. Wystarczy kartka papieru i długopis. Zachowuj te kartki, niech to będzie Twój Osobisty Dziennik Sukcesów.

Nawyk 3. – Opracuj rozwiązanie przed implementacją


Słynny fizyk John Wheeler powiedział kiedyś: Nigdy nie zabieraj się do obliczeń, dopóki nie znasz odpowiedzi. Możemy sparafrazować tę myśl: nigdy nie zbieraj się do programowania, dopóki nie znasz rozwiązania. Dość często programiści rzucają się do implementowania, nie zadając sobie sobie podstawowego pytania: Czy to ma sens?. Gdy programujesz pod wpływem impulsu, napotykasz szereg problemów, na które nie jesteś przygotowany. Poświęcasz na ich rozwiązanie wiele energii i jednocześnie odczuwasz frustrację, że to zajmuje aż tyle czasu. Po kilku godzinach lub co gorsza po kilku dniach okazuje się, że funkcjonalność najlepiej zaimplementować w zupełnie inny sposób. Dochodzisz wtedy do wniosku, że wykonałeś kawał wspaniałej, lecz niepotrzebnej roboty.

Nigdy nie zabieraj się do programowania, dopóki nie znasz rozwiązania. Najpierw przemyśl swój pomysł, naszkicuj kilka diagramów, zbadaj konsekwencje rozwiązania, zastanów się nad alternatywami. Czasem kilkanaście minut prac koncepcyjnych oszczędza wiele godzin programowania.

Nawyk 4. – Szacuj zadania


Wykonanie zadania zajmie przeważnie co najmniej tyle czasu, ile na nie przeznaczysz. Jeśli na zaimplementowanie jakiejś funkcjonalności przeznaczysz 16 godzin, a uda Ci się zakończyć zadanie w 10, to kolejne 6 spędzisz na udoskonalaniu i poprawianiu rozwiązania albo nawet na sześciogodzinnej refaktoryzacji. Co więcej, znajdziesz bardzo wiele racjonalnych powodów, dla których było to absolutnie konieczne.

Zanim przystąpisz do programowania, oszacuj czas potrzebny na to zadanie. Prawdopodobnie miałeś już do czynienia z podobnymi zadaniami, a nawet jeśli nie, to znasz kogoś, kto robił coś podobnego. Będziesz mieć punkt odniesienia dla szacowania. W skrajnym przypadku oszacuj intuicyjnie. Od razu uprzedzamy, że często będziesz się mylić – to nieuniknione. Wszelkie informacje dotyczące odchylenia od przewidywanego czasu zapisuj w swojej bazie szacowań. Zaglądaj do niej często i koryguj przewidywania według zapisanych obserwacji. Bardzo szybko staniesz się ekspertem w tym obszarze.

Nawyk 5. – Odłącz się od internetu


Internet jest niecenioną pomocą w naszej pracy, lecz potrafi przykuć uwagę na długie godziny. Zastanów się jak często poszukujesz informacji w sieci i jak często kończysz surfowanie, gdy znajdziesz potrzebne rzeczy. Czy odsyłacze do innych stron nie kuszą Cię? Czy podczas zapoznawania się z API danej metody, nabierasz chęci zapoznania się z całą dostępną dokumentacją biblioteki lub frameworka? Jeśli tak,to odłącz się od internetu na czas pracy. Być może przychodzą Ci teraz do głowy myśli, że nie możesz dobrze pracować bez internetu, że będziesz potrzebował absolutnie niezbędnych informacji. Jeśli tak właśnie myślisz, to zajrzyj do notatek, które zrobiłeś w trakcie ćwiczenia dotyczącego planowania dnia (lub zrób je jeszcze raz na bardziej szczegółowym poziomie). Ile czasu poświęcasz poszukiwanie informacji potrzebnych do pracy, a ile na czytanie ciekawych artykułów nie związanych z tym co aktualnie robisz, na sprawdzanie poczty co pięć minut, na korzystanie z komunikatorów internetowych? Porównaj wyniki z czasem, który przeznaczasz na odnajdywanie informacji związanych z Twoim bieżącym zadaniem.

Aby pożytecznie korzystać z internetu, najlepiej podporządkować to aktualnemu zadaniu. Proponujemy następując schemat postępowania:
  • Zaplanuj swój dzień (patrz: Nawyk 2.);
  • Opracuj rozwiązanie przed implementacją (patrz: Nawyk 3.);
  • Zdobądź informacje potrzebne do wykonania zadania;
  • Odłącz internet i programuj przez dwie godziny;
  • Zrób przerwę, przeanalizuj to, co udało się wykonać i przejdź do kolejnej części zadania.

Musimy wyjaśnić jeszcze jedną rzecz. Przyczyną naszej propozycji odłączenia się od sieci nie jest internet sam w sobie, a celem wskazanego sposobu postępowania nie jest odcięcie Cię od źródła informacji. Naszym zamierzeniem jest zapanowanie na nawykowym i częstym, niż to potrzebne, korzystaniem z internetu, które rozprasza programistę i negatywnie wpływa na jego efektywność.

Nawyk 6. – W pierwszej kolejności poszukuj błędów w swoim kodzie


Za chwilę przeczytasz bardzo kontrowersyjny akapit, zatem zrób krótką przerwę. Przerwij na chwilę czytanie, popatrz na drzewa, zanuć ulubioną piosenkę.

Uwaga! Z naszego doświadczenia i z doświadczenia liderów, z którymi współpracujemy wynika, że większość przyczyn błędów w oprogramowaniu leży po stronie programisty, natomiast ten poszukuje ich na zewnątrz: w technologiach, w bibliotekach, w niedociągnięciach innych programistów, w złośliwości rzeczy martwych.

Przed Tobą ostatnie i najtrudniejsze ćwiczenie. Najtrudniejsze, ponieważ dotyka Twoich przekonań. Wiemy, że to niełatwe zdanie. Odwagi, uda Ci się!
Za każdym razem, gdy napotkasz irytujący błąd w kodzie, nad którym pracujesz, załóż, że to Ty zrobiłeś coś, co go spowodowało. Poszukuj przyczyny przez założony czas i dopiero, gdy to nie przyniesie efektu, przyjmij, że możesz szukać na zewnątrz.

Nie ma drogi na skróty


Być może zadajesz sobie pytanie: Ile czasu mam poświęcić, aby stać się wysoce efektywnym programistą?. Odpowiedź brzmi: Tyle, ile trzeba. Nie ma drogi na skróty. Nabywanie nowych umiejętności i rozwijanie ich wymaga czasu, pracy, cierpliwości i sumienności. Jeśli zaakceptujesz ten fakt, nauka stanie się łatwiejsza i przyjemniejsza. Nawyki, które opisaliśmy, mogą pomóc Ci skoncentrować działanie na ważnych rzeczach i zorganizować sposób pracy, lecz odpowiedzialność za ich wprowadzenie w życie leży po Twojej stronie – Ty decydujesz.

Monday, December 22, 2008

Wzorce projektowe - Fabryki. Rysunkowy tutorial



Kreacyjne wzorce projektowe dostarczają programistą szeregu metod pozwalających na sprawne zarządzanie tworzeniem obiektów w systemie. W tutorialu omawiam kilka z nich, prezentując strukturę w postaci diagramów UML, implementację oraz obszary zastosowań.

Jeśli chcesz przypomnieć sobie jak czytać diagramy UML w odniesieniu do kodu Java przeczytaj najpierw wcześniejsze tutoriale:
















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

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...

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