Posts tagged ‘wzorce projektowe’

Wyobraźmy sobie taką sytuację, że mamy aplikację korzystającą z JAXB i do generowania klas wykorzystujemy task ant’owy XJC, na podstawie DTD dostarczanego z zewnątrz. Niestety jako, że nie jesteśmy autorami, ani właścicielami tego DTD modyfikacje mogą się pojawić niespodziewanie z tego powodu zamiana go na scheme nie wchodzi w grę.

Jako wprawni programiści szybko zauważamy, że pewne elementy mają powtarzające się własności, jednak z uwagi na to, że to DTD wykorzystanie polimorfizmu jest żadne i powstają takie brzydactwa:

String name = null;
if( get1 ) {
Tag1 t1 = container.getTag1();
name = tag.getName();
}
else {
Tag2 t2 = container.getTag2();
name = tag.getName();
}

Oczywiście zmienna get1 jest zdeklarowana gdzieś wcześniej. Ten kod aż krzyczy o wywołania polimorficzne, ale z uwagi na to, że obiektów poza nazwą nic nie łączy, nie można skorzystać z tego cudownego wynalazku.

Z pomocą przychodzą nam wzorce projektowe. Na początek weźmiemy się za napisanie zawijacza do naszej struktury, który to pozwoli wykorzystać poliformizm – niektórzy powiedzą, że to Proxy inni, że dekorator, ja uznałem, że opiszę go bardziej ogólnie jako „zwijacz”, gdyż na początku będzie przypominał zwykłe proxy, później już chyba nieco bardziej dekoratora, no ale do dzieła.

Zacznijmy od prostego interfejsu:

public interface MyTag {
String getName();
}

Dodajmy klasę abstrakcyjną, która tenże interfejs implementuje:

public class MyAbstractTag implements MyTag {}

Po co nam klasa abstrakcyjna, o tym już za chwile 🙂

Następnie piszemy konkretne „zawijacze” dla poszczególnych obiektów:

public class MyTag1 extends MyAbstractTag {
private Tag1 tag;
public MyTag1(Tag1 tag) {
this.tag = tag;
}
public String getName(){
return tag.getName();
}
}

I Tag2 prawie identycznie:

public class MyTag2 extends MyAbstractTag {
private Tag2 tag;
public MyTag2(Tag2 tag) {
this.tag = tag;
}
public String getName(){
return tag.getName();
}
}

W c++ byłoby jescze prościej, bo możnaby było zmusić kompilator do wygenerowania dla nas obu klas z szablonu, ale niestety w Javie, szablony nie sięgają do czasu wykonania, więc pozostaje nam czas kompilacji, ale to wcale nie znaczy, że należy z tego rezygnować:)

public class MyAbstractTag<T> implements MyTag {
protected T tag;
public MyAbstractTag(T tag, Class<T> clazz){
this.tag = tag;
}
}

I implementacja dla MyTag1

public class MyTag1 extends MyAbstractTag<Tag1> {
public MyTag1(Tag1 tag) {
super(tag, Tag1.class);
}
public String getName(){
return tag.getName();
}
}

Niby nic, bo metodę i tak musimy zadeklarować w implementacji, ale zawsze można nacieszyć oko 🙂 Dodajmy małą metodę do budowania zawijaczy:

public MyTag getTag(TagType type, Wrapper wrapper){
MyTag ret = null;
switch(type){
case Tag1: ret = new MyTag1(wrapper.getTag1()); break;
case Tag2: ret = new MyTag2(wrapper.getTag2()); break;
}
return ret;
}// wrócmy do przykładu z góry:
MyTag t = getTag(TagType.Tag1, wrapper);
// już nas zupełnie nie interesuje co jest pod spodem:
System.out.println( t.getName() );

I takim to sposobem doszliśmy do „prostej faktorii”, zwanej czasem też (ponoć mylnie) wzorcem projektowym factory. Z którym ma tyle wspólnego, że produkuje obiekty :)Jednak nic nie stoi na przeszkodzie iść dalej tym tropem, załóżmy, że nasze obiekty Tag1/Tag2 zwracają dane w dość dziwnym formacie jak na xml, ale np jeden z atrybutów ma listę oddzieloną przecinkami. Powodów może być wiele, przyjmijmy ten pozytywny – po prostu tak musiało być inaczej masa plików xml, które już mamy i działają wymagałaby modyfikacji, co do tych mniej pozytywnych to już pozostawiam wyobraźni czytelnika.

Wracając do tematu wzorców, jaki by tutaj pasował – metoda szablonowa. Dlaczego? Bo jedyne co potrzebujemy z obiektów Tag1/Tag2 to wartość atrybutu, cała reszta przetwarzania odbywa się w jednym miejscu (klasa abstrakcyjna). Dodajemy publiczną metodę do interfejsu:

// dodajemy metode do interfejsu
public interface MyTag {
// wcześniejsze deklaracje
List<String> getList();
}

Teraz klasa abstrakcyjna implementuje publiczny interfejs i dodaje swoją metodę abstrakcyjną, żeby pobrać wartość atrybutu z implementacji:

public class MyAbstractTag<T> implements MyTag {
// wszystko jak poprzednio
public List<String> getList(){
String list = doGetList();
if( list == null ) {
return new ArrayList();
}
return Arrays.asList(list.split(","));
}
protected abstract String doGetList();
}

Prawda, że pięknie 🙂 Jedyne co musi dostarczyć implementacja, to metodę do pobierania swojego atrybutu:

public class MyTag1 extends MyAbstractTag<Tag1> {
// reszta bez zmian
protected String doGetList(){
return tag.getList();
}
}

I wykorzystaliśmy złotą zasadę Hollywood – „nie dzwoń do nas to my zadzwonimy do Ciebie” implementacja dostarcza nam danych a klasa abstrakcyjna może z tego skorzystać albo i nie, do tego odwracamy zależności.Możnaby się jeszcze rozwodzić długo nad zaletami takiego rozwiązania, ale trzeba go po prostu spróbować!

Tym wpisem pragnę zapoczątkować nowy dział nazwany „wzroce projektowe a akcji„, którego celem jest ukazywanie konkretnych zastosowań/rozszerzeń wzorców projektowych opisywanych na forach w książkach. Dlaczego uważam, że to ważne? Dlatego, że wzorce mają być tylko wskazówką i opisem, który ktoś kiedyś sformalizował dla powtarzalnych zadań, jednak każde z nich miało swoją specyfike i wzorzec jako taki ma być tylko DROGOWSKAZEM, a nie „lekiem na całe zło”.

W tym poście nie będę się skupiał na żadnym konkretnym wzorcu, ale bardziej opisem przykładu zauważania podobieństw/powtórzeń w kodzie. Schematów, które mogą nam wskazać nasze małe bądź większe wzorce.

Przykład będzie bazował na moim poprzednim poście, w którym to opisywałem jak elegancko zamienić typ string na enuma i dzieki temu skorzystać z dobrodziejstw silnej typizacji podczas pracy z obiektami reprezentującymi struktury XML’owe. Aby tego dokonać należy napisać własny adapter, czyli klasę odpowiedzialną za konwersję, które dziedziczy po szablonowej klasie XmlAdapter.

Najprostsze rozwiązanie, to dodanie do naszej implementacji 2 map, jedna z mapowaniem typu na string i w drugą stronę, przykładowa klasa może wyglądać tak:


// plik MyEnum.java
public enum MyEnum{aa,bb,cc};

// plik MyAdapter.java
public class MyAdapter extends XmlAdapter<String,MyEnum>{

// anonimowa Map'a
private Map<String,MyEnum> name2Type = new HashMap<String,MyEnum>(){
// anonimowy blok inicjalizacyjny
{
put( "aa", MyEnum.aa );
// itd
}
}

// teraz w drugą stronę 
private Map<MyEnum,String> name2Type = new HashMap<MyEnum,String>(){
// anonimowy blok inicjalizacyjny
{
put( MyEnum.aa, "aa" );
// itd
}
}

// no i metody marshall/unmarshall, zapewniające dostęp

@Override
String unmarshall(MyEnum type) throws Exception{ return name2Type.get(type);}

@Override
MyEnum marshall(String typeName) throws Exception{ return type2Name.get(typeName);}

}

Wygląda nieźle, ale co jeżeli mamy więcej typów wyliczeniowych i nazw które chcemy obsługiwać. Pewnie czytelnik pomyśli, że możnaby dodać prywatną własność do enum’a, np. name typu String co definitywnie rozwiązałoby sprawę. Problem polega na tym, że takie rozwiązanie niekoniecznie musi prowadzić do faktycznej poprawy jakości kodu, ponieważ do takiej własności trzeba zadeklarować metodę dostępową, co znowu oznacza, że jak już dawno zapomnimy do czego ta własność jest, przyjdzie nam do głowy, żeby z niej skorzystać. Co gorsza, ktoś z naszych współpracowników, który nie jest świadom znaczenia atrybutu name, wykorzysta go w swoim kodzie i znowu silną typizację szlag trafi 🙂

Z tego względu zalecaną przeze mnie ścieżka jest dodanie po 2 mapy do każdego adaptera, pozniej kod drugiego adaptera:


// plik MyEnum2.java
public enum MyEnum2{dd,ee,ff};

// plik MyAdapter.java
public class MyAdapter2 extends XmlAdapter<String,MyEnum2>{

// anonimowa HashMap'a
private Map<String,MyEnum2> name2Type = new HashMap<String,MyEnum2>(){
// anonimowy blok inicjalizacyjny
{
put( "dd", MyEnum2.dd );
// itd
}
}

// mapowanie w druga strone
private Map<MyEnum2,String> name2Type = new HashMap<MyEnum2,String>(){
// anonimowy blok inicjalizacyjny
{
put( MyEnum2.dd, "dd" );
// itd
}
}

@Override
String unmarshall(MyEnum2 type) throws Exception{ return name2Type.get(type);}

@Override
MyEnum2 marshall(String typeName) throws Exception{ return type2Name.get(typeName);}

}

Sprawa wydaje się czysta, działać działa, ale wygląda jakby kod sie zdziebko powtarzał:

  • każda z klas zawiera 2 mapy, z typami identycznymi do tych przekazanych jako parametry szablonu
  • każda z nich nadpisuje metody marshall/unmarshall, tylko po to aby wybrać odpowiedni element z mapy

Powyższy przykład jest jeszcze dość prosty, a co jeżeli będziemy  chcieli poinformować użytkownika że dana nazwa/typ nie istnieje w tablicy mapowań? Trzebaby rzucić jakiś wyjątek, np IllegalArgumentException, ale jak narazie sprawdzanie trzeba dodać w każdej metodzie, a może istnieje inny sposób?

Tak, a oto lista zmian, które trzeba wprowadzić:

  • stworzyć klasę abstrakcyja AdapterBase, dziedziczącą po szablonie
  • klasa AdapterBase także deklaruje 2 parametry
  • AdapterBase ma 2 własności w postaci Map – name2Type i type2Name
  • name2Type i type2Name nie mają konkretnych typów, tylko parametry szablonu
  • dostęp name2Type/type2Name  jest typu protected, tzn że klasy dziedziczące mogą ustalać ich wartość
  • klasa AdapterBase zawiera implementację metoda marshall/unmarshall, które także są metodami szablonowymi (w przypadku nieznalezienia elementu w odpowiedniej tablicy, obie rzucają wyjątek)
  • konkretne adaptery dziedziczą po AdapterBase
  • deklarują typy z których konwertują
  • deklaruja mapowania typów i już

A teraz jak to może wyglądać, zaczniemy od końca czyli od konretnego adaptera:


// MyEnum zostaje bez zmian
// plik MyAdapter.java
public class MyAdapter extends AbstractAdapter<String,MyEnum>{
// konstruktor dostarcza mapowań
public MyAdapter() {

name2Type.put( "aa", MyEnum.aa );
// itd

type2Name.put( MyEnum.aa, "aa" );
}
// i to wszystko!
}

Klasa drugiego adaptera:

// MyEnum2 zostaje bez zmian
// plik MyAdapter2.java
public class MyAdapter2 extends AbstractAdapter<String,MyEnum2>{
// konstruktor dostarcza mapowań
public MyAdapter2() {
name2Type.put( "dd", MyEnum2.dd );
// itd
type2Name.put( MyEnum2.dd, "dd" );
}
// i to wszystko!
}

A teraz abstrakcyjny adapter


public class AbstractAdapter<T,E> extends XmlAdapter<T,E>{
protected Map<T,E> name2Type = new HashMap<T,E>();
protected Map<E,T> type2Name = new HashMap<E,T>();

@Override
T unmarshall(E type) throws Exception{ return name2Type.get(type);}

@Override
E marshall(T typeName) throws Exception{ return type2Name.get(typeName);}

}

Jak widać AbstractAdapter nie konkretyzuje szablonu, jedynie deklaruje, że wszystkie mapowania będą przekazywane przez Map’y o odwróconych typach.

No dobra, zobaczmy jak teraz dodać obsługę wyjątków:


public class AbstractAdapter<T,E> extends XmlAdapter<T,E>{
protected Map<T,E> name2Type;
protected Map<E,T> type2Name;

@Override
T unmarshall(E type) throws Exception{ 
T ret = name2Type.get(type);
if( ret == null ) {
throw new IllegalArgumentException("Cannot find name for type: " + type);
}
return ret;
}

@Override
E marshall(T typeName) throws Exception{ 
E ret = type2Name.get(typeName);
if( ret == null ) {
throw new IllegalArgumentException("Cannot find type for name: " + typeName);
}
return ret;
}

}

Jak widać jest to bajecznie proste, zmiana w jednym miejscu, konkretne adaptery nawet nie są świadome tego, że coś się zmieniło, a do tego wszystkie zareagują tak samo na brakujące mapowanie.