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.