Archiwum

Archive for Czerwiec 2010

Nie tak już rozwlekła java

24/06/2010 Dodaj komentarz

Jest to kontynuacja tematu który rozpocząłem w poprzednim wpisie. Na końcu doszedłem do wniosku, że przydałaby mi się pewna własność języka. No i proszę długo nie musiałem czekać i już jest (właściwie była tylko ja ją teraz znalazłem). Chodzi o Lombok, może nie daj zbyt wiele, ale to co oferuje zmusza do przemyślenia sposobu jak można programować w javie :). Oprócz strony projektu świetnym wprowadzeniem jest ta prezentacja, którą prowadzą sami twórcy i może jeszcze ten wpis.
Mam wiele pomysłów jakie to może mieć zastosowanie, jednak aby nie rozwlekać pokaże jak można przy pomocy tego rozwiązać problem z poprzedniego wpisu.

Jakie to daje korzyści
Problem dotyczył duplikacji kodu (zaznaczone linie):

    public List getExpenses(long userId, Date start_date, Date end_date) {
        AuditLogger.logLookup(userId, AuditCode.GET_EXPENSES);
        ExpensesConnection connection = new ExpensesConnection(userId);
        List expenses = connection.findAllBetween(start_date, end_date);
        connection.close();
        return expenses;
    }

    public void addExpense(long userId, Date date, BigDecimal amount) {
        AuditLogger.logLookup(userId, AuditCode.ADD_EXPENSES);
        ExpensesConnection connection = new ExpensesConnection(userId);
        connection.saveNewExpense(date, amount);
        connection.flush();
        connection.close();
    }

Rozwiązaniem które można tu użyć to wzorzec szablonu, poprzez użycie klasy anonimowej. Jednak usunięcie duplikacji jest kosztem czytelności.
Dlatego też wprowadziłem byt, który jak się okazało nie rozwiązał tego problemu ze względu na techniczne ograniczenia języka. Teraz dzięki Lombok można to zrobić, kod wygląda tak:

	public List getExpenses(long userId, Date start_date, Date end_date) {
		@Cleanup ConnectionHelper helper = new ConnectionHelper(userId, AuditCode.GET_EXPENSES);
	    return helper.connection.findAllBetween(start_date, end_date);
	}

	public void addExpense(long userId, Date date, BigDecimal amount) {
		@Cleanup ConnectionHelper helper = new ConnectionHelper(userId, AuditCode.ADD_EXPENSES);
	    helper.connection.saveNewExpense(date, amount);
	    helper.connection.flush();
	}
	
	private ExpensesConnection doBefore(long userId, String logId) {
	    AuditLogger.logLookup(userId, logId);
	    ExpensesConnection connection = new ExpensesConnection(userId);
	    return connection;
	}

	private void doAfter(ExpensesConnection connection) {
	    connection.clone();
	}

	class ConnectionHelper {
		ExpensesConnection connection;
	    public ConnectionHelper(long userId, String logId) {
	        connection = doBefore(userId, logId);
	    }
	    public void close(){
	    	doAfter(connection);
	    }
	}

Użyłem tu @Cleanup trochę nie w tym celu do którego został zaprojektowany. Jednak otrzymałem konstrukcje gwarantującą mi że kod techniczny wykona się przed i po kodzie biznesowym (niezależnie co tam się stanie). I jest to czytelne, obie części kodu są odseparowane od siebie, po prostu piękne ;).

Wnioski
Integracja Lombok w projekty javowe jest dobrze zrobiona (na początku był wspierany tylko eclips, ale teraz i reszta IDE też to podobno trawi), użyłem tego w przykładowym projekcie pod mavenem i też śmiga. Znalezione zastrzeżenia: to że narzędzia do analizy jakości kodu mogą tu zgłaszać błędy bo nie widzą kodu.
To w jaki sposób Lombok rozwiązuję problemy rozwlekłości w javie pokazuje zupełnie nową drogę do obchodzenia braków języka. Teraz nie trzeba prosić zarządców javy o dodanie nowego słówka kluczowego, tylko przy pomocy adnotacji zrobić sobie samemu takie coś (twórcy zapewniają że można pisać własne rozszerzenia, a oto dowód).

Kategorie:Java Tagi: ,

Rozwlekła java

23/06/2010 Dodaj komentarz

Wprowadzenia
Uczenie się (czytanie o) innych języków poszerza horyzonty (tak mówią). Miałem ostatnio okazje coś w tym obszarze doświadczyć. Jako „lucky geek” dostałem do wyboru darmowego ebook_a. Z możliwej listy jakoś żaden z tytułów poświęconych javie nie przykuł mojej uwagi, więc w ramach rozszerzania horyzontów zdecydowałem się na „Clojure in Action”. Mam nadzieje że w taki sposób bardziej zmuszę się do zapoznania z tym językiem.
Czytając pierwszy rozdział trafiłem na jeden z sprawdzonych argumentów dlaczego jakiś język jest lepszy niż java. Jest to owa rozwlekłość (verbose), choć nie dotyczył on tylko javy.

Rozwlekłość
Przykład na rozwlekłość, duplikacje wyglądał mniej więcej tak:

    public List getExpenses(long userId, Date start_date, Date end_date) {
        AuditLogger.logLookup(userId, AuditCode.GET_EXPENSES);
        ExpensesConnection connection = new ExpensesConnection(userId);
        List expenses = connection.findAllBetween(start_date, end_date);
        connection.close();
        return expenses;
    }

    public void addExpense(long userId, Date date, BigDecimal amount) {
        AuditLogger.logLookup(userId, AuditCode.ADD_EXPENSES);
        ExpensesConnection connection = new ExpensesConnection(userId);
        connection.saveNewExpense(date, amount);
        connection.flush();
        connection.close();
    }

No tak, duplikacja rzuca się w oczy (kod do obsługi połączenia do jakiegoś repozytorium i do logowania zdarzeń). Ów przykład był pokazany aby zaprezentować cechę języka pozwalającą na zwijanie takich kawałków kodu do zgrabnych form, co w kodzie wygląda tak:

(defn get-expenses [user-id start-date end-date]
	(with-audited-connection [user-id connection]
		(find-all-between connection start-date end-date)))

(defn add-expense [user-id date amount]
	(with-audited-connection [user-id connection]
		(save-new-expense connection date amount)))

I tu jest pokazywana siła języka, jego zwięzłość, kosztem javy ;). Jest wspomniane że java może użyć coś takiego jak szablon (template method pattern), ale i tak efekt tego nie będzie taki ładny jak tutaj.
No cóż trudno się nie zgodzić. Nie wiem jak Clojure robi tą magie (na razie nie doczytałem tak daleko – termin „macro” musi wystarczyć) ale chwali się że wszystko to jest w stanie załatwić na poziomie samego języka (dzięki jego elastycznej składni), poczytamy zobaczymy.
Ze strony javy próby rozwiązania tego w bardziej elegancki sposób kryją się pod hasłami Spring, EJB3 lub AOP; ale to jest dodawanie zewnętrznych bibliotek, uruchamiania kodu w specjalnych warunków, czy modyfikacja na poziomie bytecode. Ogólny przekaz w javie nie da się zrobić tego tak elegancko jak w Clojure.

Sprawdzenie czy się nie da
No to spróbowałem zobaczyć w kodzie czy rzeczywiście wygląda to tak źle. Na początek definiując co jest problemem mamy zrobić coś takiego (tylko pierwsza metoda):

    public List getExpenses(long userId, Date start_date, Date end_date) {
        ExpensesConnection connection = doBefore(userId, , AuditCode.ADD_EXPENSES);
        try {
            return connection.findAllBetween(start_date, end_date);
        } finally {
            doAfter(connection);
        }
        return expenses;
    }

    private ExpensesConnection doBefore(long userId, String logId) {
        AuditLogger.logLookup(userId, logId);
        ExpensesConnection connection = new ExpensesConnection(userId);
        return connection;
    }

    private void doAfter(ExpensesConnection connection) {
        connection.close();
    }

Dodałem try aby kod był bardziej życiowy. Widać że mam dwie grupy technicznego kodu wykonywanego wokoło biznesowej części metody. Jak nic prowadzi to do szablonu więc sprawdźmy jak to wygląda:

    public List getExpenses(long userId, final Date start_date, final Date end_date) {
        return new ExpensesConnectionTemplet<List>() {
            List doInTry(ExpensesConnection connection) {
                return connection.findAllBetween(start_date, end_date);
            }
        }.execute(userId, AuditCode.ADD_EXPENSES);
    }

    abstract class ExpensesConnectionTemplet<T> {

        abstract T doInTry(ExpensesConnection connection);

        T execute(long userId, String logId) {
            ExpensesConnection connection = doBefore(userId, logId);
            try {
                return doInTry(connection);
            } finally {
                doAfter(connection);
            }
        }
    }

No i wygląda to niezbyt pięknie (dla mnie jednak jest czytelne :)). Dla potrzeb innych użytkowników można by postarać się o lepsze nazwy i komentarze, ale i tak tworzenie anonimowej klasy zaciemnia obraz tego co metoda robi. Czytelność pierwotnej metody jest poświęcona na rzecz unikania duplikacji. Jakoś trzeba wyważyć to; porównując to z pierwszą wersją można dojść do stwierdzenia: „co tam duplikacja w dwóch metodach przynajmniej kod się łatwiej czyta” :). Wersja z szablonem potrzebuje opatrzenia i po pewnym czasie staje się także czytelna, dla osób które na co dzień pracują z kodem. Jednak dla stykających się z kodem pierwszy raz wymagać będzie trochę wysiłku na początek.
Jednak spoglądając na piękny przykład z Clojure doszedłem do spostrzeżenie że przy pewnym chytrym zabiegu z kodem można coś jeszcze tu zdziałać. Tak więc na początek odizolowanie części biznesowej od technicznej.

    public List getExpenses(long userId, Date start_date, Date end_date) {
        ExpensesConnection connection = doBefore(userId, , AuditCode.ADD_EXPENSES);
        try {
            return _getExpenses(start_date, end_date, connection);
        } finally {
            doAfter(connection);
        }
        return expenses;
    }

    private List _getExpenses(Date start_date, Date end_date, ExpensesConnection connection) {
        return connection.findAllBetween(start_date, end_date);
    }

Pozornie naprodukowałem dodatkowy kod. Podpierajac się autorytetem SRP mam osobny kod do części biznesowej, a poprzez DI na poziomie metody przekazuje tam wszytko co jest potrzebne sprawiając że kod jest łatwiejszy do testowania. Przy okazji lepiej widać że userId należy tylko do części technicznej.
Dzięki takiej separacji można wprowadzić dodatkowy byt, który można porównać do trochę koślawego szablonu, ale w kodzie jest to już bardziej czytelne.

    public List getExpenses(long userId, final Date start_date, final Date end_date) {
        ConnectionHelper helper = new ConnectionHelper<List>(userId, AuditCode.ADD_EXPENSES);
        return helper.executeInTry(_getExpenses(start_date, end_date, helper.connection));
    }

    class ConnectionHelper<T> {
        ExpensesConnection connection;
        public ConnectionHelper(long userId, String logId) {
            connection = doBefore(userId, logId);
        }
        public T executeInTry(T value) {
            try {
                return value;
            } finally {
                doAfter(connection);
            }
        }
    }

No i już mogłem myśleć że trafiłem na jakiś „święty graal”, że mam konstrukcję tak samo elegancką jak ta z Clojure ;). Ale drugie spojrzenie uświadomiło mi że kod nie będzie działać dobrze kiedy wystąpi wyjątek (wychodzą braki w znajomości języka). Część biznesowa będzie wykonywać się poza try_em.
Wszystko byłoby pięknie gdyby można byłoby symulować konstrukcje try {} finally {} jakoś inaczej (można!!! szczegóły). Na pocieszenie zostaje fakt że to rozwiązanie można stosować kiedy trzeba otoczyć jakiś fragment kodu innym kodem i nie ma finally.

Wniosek
Java nie jest aż tak elastyczna. Można byłoby się pokusić o użycie jakichś mechanizmów np. Dynamic Proxy. Jednak to prowadziłoby do wymyślania koła na nowo; i jeśli rzeczywiście bardzo to jest potrzebne to już lepiej dołączyć parę jarów. Względnie może idea domknięć (closures) w javie rozwiąże tu wszystko i sprawi że całe dywagacje zawarte w tym rozwlekłym pości będą nieaktualne ;).

PS. Co do rozwlekłości w javie trafiłem też na ciekawy post i dyskusje, podejmujący temat z innej perspektywy.

Kategorie:Java Tagi:

Podejście do mavena

07/06/2010 Dodaj komentarz

Maven w połączeniu z m2eclipse jest o wiele przystępniejszy ;).
Jeśli pracuje się w eclipse, to taki zestaw oferuje wygodny sposób na sprawdzanie różnych koncepcji czy bibliotek, a to poprzez zarządzanie zależnościami. Zarządzanie jako takie oferuje san maven, ale plugin (m2eclipse) sprawia że jest to wygodniejsze (prywatne odczucie :)). Ma się także dzięki temu dostęp do ładnego edytora pomów, jednak treścią tego wpisu będzie to, że taki zestaw daje większą kontrole nad procesem budowy realizowanym przez maven.

Tak więc maven to wspaniałe narzędzie, pozwalające na szybkie wystartowanie z pracą. Wszytko pięknie jak postępuje się zgodnie z ustaleniami. W chwili kiedy sypnie błędem; albo nie, ale nie robi tego co się oczekiwało, mogą zaczynać się schody (znowu prywatne odczucie :)). Jest w tym chyba także trochę winy szybkości z jaką można tworzyć nowe projekty czy dodawać rozszerzenia. Ponieważ kiedy potrzebne jest aby pakować wynikową aplikacje do wara to odszukuje się w sieci odpowiedni wycinek skryptu (jakiegoś pluginu) i już jest war; trzeba wygenerować jakieś klasy, to kopiowany jest następny wycinek i generowanie działa. I bardzo często (mi się tak zdarzało) pracując w taki sposób robi się to bez dokładniejszego wczytania się w dokumentacje danego plugina (jeśli jest na przyzwoitym poziomie) i zadowala się tym, że teraz wydaje się że działa tak jak chcemy. Później natomiast okazuje się że przestaje działać (np. w połączeniu z innymi pluginami) i trzeba szukać dlaczego.
Tak więc mamy na początku zachętę w postaci szybkiego efektu, ale jeśli chce się coś więcej to późniejsza nauka narzędzia (plugin_a) nas nie o minie ;).
Przerywając te wywody skupie się na konkretnym przypadku i przedstawieniu w nim jak można się zabrać za maven_a (jego pluginy), kiedy coś do końca nie działa.

Stworzenie problemu
Istniała potrzeba generowania dwóch jarów (część kliencka i serwerowa), użyty plugin to maven-ejb-plugin. Pierwsza wersja (skopiowana/sklejona radośnie z znalezionych przykładów) to:

	 
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-ejb-plugin</artifactId>
	<configuration>
		<generateClient>true</generateClient>
		<ejbVersion>3.0</ejbVersion>
		<clientIncludes>
			<clientInclude>com/test/client/**</clientInclude>
		</clientIncludes>
	</configuration>
</plugin>

i działa, pojawia się drugi jar kliencki z klasami ze wskazanych pakietów. No i hula to jakiś czas, aż przychodzi prośba aby jar serwerowy miał też wyselekcjonowane klasy (nie zawierał części klienckiej). Nie ma problemu, w dokumentacji pisze, że tak można, więc wpis wygląda teraz tak:

	 
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-ejb-plugin</artifactId>
	<configuration>
		<generateClient>true</generateClient>
		<ejbVersion>3.0</ejbVersion>
		<clientIncludes>
			<clientInclude>com/test/client/**</clientInclude>
		</clientIncludes>
		<excludes>
		  <exclude>com/test/client/**</exclude>
		</excludes>
	</configuration>
</plugin>

Uruchamia się proces budowy i nie widać efektu, jar serwera ciągle zawiera klasy ze wszystkich pakietów. Wykonało się dla pewności clean_a, ale ewidentnie widać, że nie robi tego co miało i nie skarży się też żadnymi wyjątkami.
Tutaj powinienem spokojnie przeczytać dokumentacje i zrobić dokładnie jak tam jest, ale nastąpił pewien etap blokady i nie widziałem dość oczywistego błędu który popełniałem.
Zamiast tego zacząłem szukać rozwiązania na około.

Dochodzenie do rozwiązania problemu
Jako że ewidentnie plugin nie robił co twierdziła dokumentacja, a że był napisany w javie to postanowiłem dobrać się do jego źródeł. Najpierw poprzez dodanie odpowiedniej zależności:

	 
		<dependency>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-ejb-plugin</artifactId>
			<version>2.2.1</version>
		</dependency>

sprawiłem że jar plugina był w moim projekcie. Potem wykorzystując możliwości jakie dawał m2eclipse pobrałem źródła i mogłem przejrzeć w miarę prosty kod pluginu(dwie klasy z czego jedna to help). Okazała się że jest tam obsługa owego parametru, który powoduje odfiltrowanie klas z części serwerowej. W takim razie jedno zostało wyjaśnione, to że dokumentacja zgodna jest z kodem. Wykorzystałem więc następną możliwość jaką dawał mi plugin do eclipsa i odpaliłem cały proces budowy maven w trybie debug stawiając pułapki wewnątrz kodu podejrzanego plugina (było trochę zabawy, aby to zrobić, ale da się zmusić eclipsa do tego). W ten sposób zobaczyłem że moje pułapki w (wykonywany) kodzie zupełnie rozjeżdża się z tym co prezentowane jest w podglądzie kodu, tak jakbym patrzył na zupełnie inny kod. Co wskazało na błąd, który tu popełniałem. Budowałem na starszej wersji, która nie obsługiwała tego co chciałem zrobić. To że nie wskazałem wersji definiując plugin (co dziwne zrobiłem to kiedy chciałem podciągając źródła) sprawiło że w tym wypadku maven podciągną wersje starszą (ciekawa jak jest polityka wybierania wersji?). Tak więc wpis uległ zmianie:

	 
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-ejb-plugin</artifactId>
	<version>2.2.1</version>
	<configuration>
		<generateClient>true</generateClient>
		<ejbVersion>3.0</ejbVersion>
		<clientIncludes>
			<clientInclude>com/test/client/**</clientInclude>
		</clientIncludes>
		<excludes>
		  <exclude>com/test/client/**</exclude>
		</excludes>
	</configuration>
</plugin>

i działa tak jak trzeba (do następnych problemów).

Wnioski
Nauczka na przyszłość to pamiętać, w którym miejscu można popełniać błędy w mavenie:

  • poprzez niedokładne czytanie dokumentacji i pośpiech w realizacji (to akurat jest bardziej ogólne ;));
  • nie wskazywanie wersji pluginu może dużo namieszać.

A co do tego jak poszukiwać rozwiązania to dużą zaletą jest możliwość:

  • szybkiego podglądu kodu pluginu;
  • a jeśli to nie pomaga to prześledzenie jego pracy.