Chamberconf 2016

Luty 5th, 2016

Tak jak w zeszłym roku zapraszamy na zamek. Tym razem do Pałacu w Łagowie. Tylko nie pomylcie z Zamkiem Joannitów w Łagowie, bo wylądujecie jedną autostradę dalej na północ 😉

Co w tym roku?

Po pierwsze dwa dni 5 i 6 marca. Jeżeli jesteście chętni to możecie przyjechać już 4, ale nocleg z własnych dukatów.
Po drugie ciekawi prelegenci.
Po trzecie Pechakucha, czyli 20 slajdów, każdy po 20 sekund. Co w zamian? Masz pewność, że załapiesz się na wejściówkę (płacisz normalnie). Wejściówek będzie ilość mocno ograniczona :)
Po czwarte stolik Jacka Danielsa pw. Lemmego.

Zatem zapraszamy.

Bilety będą do nabycia tu.

ps. mamy, jako organizatorzy, ogromna prośbę by osoby wybierające się na konferencje organizowały sobie wspólny dojazd.

Courserowy kurs o grafach w javie

Styczeń 24th, 2016

Panie my tu mamy taki zapierdol, że nie ma czasu załadować….

I niestety jest to w pewnym sensie prawda. Dlatego tak mało piszę.

Na początek sprawy formalne.

28 stycznia będę mówił na LoGeek Night we Wrocławiu o analizie biznesowej i jej miejscu w projekcie. Miejsc już nie ma, ale możecie przyjść i spróbować się wbić. Efektywnie osoby, których nie ma na liście nie będą miały torby z upominkami i piją na własny koszt.

Chamberconf będzie… kiedyś. Idea pozostaje taka sama – zamek, zajebiści prelegenci i jeszcze bardziej zajebista atmosfera.

Jest jakaś tam szansa, że jeszcze w marcu trafię na PWr/UWr ze spiczem o javie. Jeżeli są tu osoby, które tam studiują i chciały by posłuchać to generalnie niech śledzą mojego twittera bo zapewne tam będzie najszybciej podane. Tak wiem kanały komunikacyjne u mnie leżą… ale nad tym pracuję.

Obrazki na bloga kiedyś poprawię.

Teraz do meritum. Jest sobie kurs Advanced Data Structures in Java. Przy czym Advanced oznacza grafy. I chyba to byłaby lepsza nazwa dla tego kursu (w rodzaju Data Structures in Java – Graphs). Kurs „sprzedał mi” Tomek Dziurko na FB ogłaszając swoje uczestnictwo. Generalnie całość zaczęła się ze 2 tygodnie temu, a dziś jest koniec Week 1, co oznacza mniej więcej tyle, że mogę zacząć narzekać 😀

Sama tematyka jest ciekawa i nie ma się do czego przyczepić. Nagrania są zrobione naprawdę pr0, materiały w miarę jasne i zrozumiałe. Całość fajnie przemyślana. Jedyne co boli to sposób „rozliczania” kursu, ale to chyba jest mankament całej coursery. Zamiast posadzić serwer CI z jakimś gitem tak by uczestnicy robili świętą trójcę commit-push-deploy to trzeba się użerać z plikami zip. No ale nie ma co płakać i należy się cieszyć, że nie każą wysyłać dyskietek 😀 Nie zdefiniowano też wersji javy w jakiś jawny sposób, ale to drobiazg (z kodu wyszło, że ósemka).

Podsumowując. Kurs zapowiada się ciekawie. Dla mnie osobiście jest to też szkoła prowadzenia zajęć. Braki w technice wyszły w zeszłym roku na programie juniorskim. Zatem mam okazję nadrobić.

Klasa Calendar dla opornych

Grudzień 31st, 2015

Wpis dla osób mającym mniejsze doświadczenie w javie, ale i bardziej doświadczeni mogą się z tym zetknąć.

Tak, wiem mamy Javę 8 i w ogóle…

Problem jest jednak taki, że czasami musimy pracować z narzędziami napisanymi przed J8 i one uparcie chcą Calendar.

Problem

Przyjrzyjmy się pewnej metodzie

Listing 1. Metoda parsująca nietypowy format daty

public Calendar parseBusinessDate(String date) {
    if (date.length() != 7) return null;
    String year = date.substring(0, 4);
    String month = date.substring(4);
    int y = Integer.parseInt(year);
    int m = Integer.parseInt(month);
    Calendar c = Calendar.getInstance();
    c.set(Calendar.YEAR, y);
    c.set(Calendar.MONTH, m - 1);
    c.set(Calendar.DATE, c.getActualMaximum(Calendar.DATE));
    return c;
}

Data ma siedem znaków. Cztery pierwsze to rok, a potem trzy znaki miesiąca. Datę ustawiamy na ostatni dzień danego miesiąca. Dlaczego tak nie pytajcie. Teraz napiszmy test. Będzie on specyficzny, bo chcemy sprawdzić rok przestępny:

Listing 2. test dla roku przestępnego

@Test
public void shouldParseLeapYear(){
    // when
    Calendar result = sut.parseBusinessDate("2012002");
    // then
    Assert.assertEquals(result.get(Calendar.YEAR), 2012);
    Assert.assertEquals(result.get(Calendar.MONTH), Calendar.FEBRUARY);
    Assert.assertEquals(result.get(Calendar.DATE), 29);
}

Wygląda ok, prawda? To uruchom ten test 30 lub 31 dnia miesiąca…

Ciekawe prawda?

Przyczyna i rozwiązanie

Problemem jest sposób w jaki inicjowana jest instancja Calendar. Wywołując getInstance otrzymujemy obiekt wskazujący na bieżącą datę. Oznacza to, że dzień może być ustawiony na 30 albo 31. Jednocześnie w czasie wywołania metody parseBusinessDate zanim ustawimy dzień ustawiamy rok i miesiąc. W efekcie jeżeli ustawimy miesiąc, który jest krótszy niż bieżący i zrobimy to w ostatnim dniu miesiąca to nastąpi „przepełnienie” daty i przeskoczymy do kolejnego miesiąca.
Co ciekawe błąd tego typu może umknąć naszej uwadze i testom jeżeli tylko sposób naszej pracy powoduje, że w te nieszczęsne ostatnie dni miesiąca nie puszczamy z jakiś powodów testów np. mamy dwa dni spotkań, jest weekend i nie ma commitów, albo code freeze przed releasem. Jeżeli jeszcze po stronie klienta procedury, które mogłyby wykryć błąd są puszczane pierwszego dnia roboczego, a nie ostatniego to taki babol może długo pozostawać w ukryciu.

Rozwiązanie problemu jest proste. Wystarczy „zresetować” zmienną po utworzeniu na przykład ustawiając dzień na 1 albo czas na zerową milisekundę.

Listing 3. Poprawiona metoda parsująca nietypowy format daty

public Calendar parseBusinessDate(String date) {
    if (date.length() != 7) return null;
    String year = date.substring(0, 4);
    String month = date.substring(4);
    int y = Integer.parseInt(year);
    int m = Integer.parseInt(month);
    Calendar c = Calendar.getInstance();
    c.setTimeInMillis(0L);
    c.set(Calendar.YEAR, y);
    c.set(Calendar.MONTH, m - 1);
    c.set(Calendar.DATE, c.getActualMaximum(Calendar.DATE));
    return c;
}

Mała rzecz, a potrafi wkurwić.

Powłamaniowo, czyli dlaczego lubię Hostit

Grudzień 31st, 2015

Jak pewnie zauważyliście, blogasek był wybuchł. Dokładnie wczoraj o 4 nad ranem. Jakiś kutasiarz zrobił mu kuku. Okazał się przy tym idiotą, bo plik z pogróżkami jest pusty.

Problemem nie był, jak podejrzewałem, wordpress lecz jakiś nieznany błąd w php. Włamanie nastąpiło na konto jednego z klientów u usługodawcy i się „było spropagowało” trafiając m.in. mnie.

Sam bez winy nie jestem, bo jak ten chuj głupi miałem nieprawidłowo ustawiony chmod na katalogach z mediami.

Tyle „analizy powłamaniowej”. Teraz podziękowania dla ekipy hostit, która dziś rano zadzwoniła, przeprosiła i wyjaśniła co było źle. W dodatku poprawili to co ja zepsułem. Chłopaki są kochane, ale jak co roku w okolicach sylwestra musi się coś zjebać i jak co roku wszystko kończy się dobrze 😀

Obrazki będą przywrócone w miarę możliwości i czasu. Kiedy dokładnie to nie wiem, ale na pewno w 2016 😀

Kim jest haker wg pewnej fundacji…

Grudzień 1st, 2015

Najpierw link. Jako, że może zniknąć to i screenshocik

I pamiętajcie dzieci „zapijaczona banda nieuków” to przecież nic złego. W powszechnym rozumieniu na jedno wyjdzie z „niedouczonym pracownikiem fundacji, który chciał dobrze”.

FizzBuzz bez ifów w javie

Listopad 21st, 2015

No dobra… po prostu będzie to przykład wykorzystania popularnych narzędzi by stworzyć FizzBuzz bez pisania ifów. W ogólności problem rozbija się o dwie brakujące metody. Pierwsza dostarczająca „zoraclowanego” String-a, czyli takiego gdzie pusty String będzie oznaczać null. Druga to cykl. Pierwszą metodę weźmiemy z Apache Commons Lang, a drugą z Guavy. Cały pomysł bazuje na ostatniej JUG-owej prezentacji Pawła Szulca, który pokazał to coś w Haskell-u. Mi się to spodobało i poniżej wersja w javie.

Listing 1. FizzBuzz bez ifów

public class FizzBuzz {

	public static void main(String[] args) {
		Iterator<String> fizz = Iterables.cycle("", "", "Fizz").iterator();
		Iterator<String> buzz = Iterables.cycle("", "", "", "", "Buzz").iterator();

		OfInt ints = IntStream.iterate(1, n -> n + 1).iterator();
		Iterator<Optional<String>> strings = Stream.generate(() -> StringOptional.of(fizz.next() + buzz.next()))
				.iterator();

		Stream.generate(() -> {
			Optional<String> s = strings.next();
			Integer i = ints.next();
			return s.orElse(i.toString());
		}).limit(32).forEach(System.out::println);

	}
}

class StringOptional {

	public static Optional<String> of(String v) {
		return ofNullable(defaultIfEmpty(v, null));
	}
}

Na początek klasa StringOptional, która będzie nam zamykać wywołanie StringUtils.defaultIfEmpty tak by ładnie produkowało Optional.

I teraz metoda main. Na początek tworzymy dwa cykle pierwszy reprezentuje częstość dla fizz, drugi dla buzz. W przypadku uogólnionym można pokusić się o zaimplementowanie tego w oparciu o metodę w rodzaju Collections.fill i listy o określonej długości. Przy czym należy pamiętać, że java jest ewaluowana zachłannie i dla dużych wartości całość się wywali.

Następnie tworzymy dwa iteratory z nieskończonych Stream-ów. Pierwszy generuje kolejne inty, a drugi w oparciu o konkatenację wartości z cykli oraz naszą magiczną klasę StringOptional odpowiednią wersję Optional.

Ostatnia linijka składa nam to wszystko do kupy. Tworzymy Stream, który będzie budowany według logiki jeżeli optional.empty to wypisz numer inaczej wypisz zawartość. Przy czym warunek jest weryfikowany na poziomie typu Optional, a ten jest determinowany w metodzie ofNullable, zatem pochodzi „z języka”.

Co autor miał na myśli?

Powyższy kod to przykład na to jak odpowiednie użycie typów pozwala na uproszczenie algorytmu. Dobór poszczególnych elementów na początku pozwolił na uproszczenie algorytmu. Co więcej kod ten można zrealizować jeszcze prościej:

Listing 2. FizzBuzz uproszczony

public class FizzBuzz {

	public static void main(String[] args) {
		Iterator<String> fizzBuzz =
				cycle("", "", "Fizz", "", "Buzz", "Fizz", "", "", "Fizz", "Buzz", "", "Fizz", "", "", "FizzBuzz")
						.iterator();

		OfInt ints = iterate(1, n -> n + 1).iterator();
		Iterator<Optional<String>> strings = generate(() -> ofNullable(defaultIfEmpty(fizzBuzz.next(), null)))
				.iterator();

		generate(() -> strings.next().orElse(ints.next().toString()))
				.limit(32).forEach(System.out::println);

	}
}

Ale w tym przypadku pierwsza linijka z fizzBuzz wprowadza trochę zagmatwania ponieważ specyfikacja którą reprezentuje jest tak naprawdę „dwuwymiarowa” tzn. ograniczona przez dwa warunki. W tej wersji one „giną” w postaci jednego dość rozwlekłego warunku.

Odpowiednio dobrane typy pozwalają na specyfikowanie algorytmu w oparciu o nie właśnie, a nie jakieś arbitralne zewnętrzne warunki. W dodatku pozwalają na lepsze przetestowanie, bo testujemy implementację typu, oraz dają większe bezpieczeństwo końcowego rozwiązania ponieważ aplikujemy funkcje, których dziedzina i przeciwdziedzina są ograniczone do konkretnych typów (a mapowanie A→B zazwyczaj może być realizowane w jeden konkretny sposób).

Istota wojny i dlaczego wygra ją ISIS

Listopad 15th, 2015

Liczyłem, że ten tekst będę pisać za jakieś cztery do sześciu miesięcy. Choć z drugiej strony nie powinienem się dziwić, że pisze go dzisiaj. Osobiście obstawiałem zamach przed Euro 2016, bo w czasie były by raczej niemożliwe. Cóż stało się wcześniej.

O istocie wojny

Wojnę można prowadzić z kilku powodów. Pierwszym najbardziej prozaicznym, a jednocześnie najstarszym są zasoby. Ziemia, kopaliny, dostęp do wody. Pierwsze konflikty w dziejach ludzkości, prowadzone za pomocą pięści, kamieni i kijów były wojnami o zasoby. Wraz z rozwojem cywilizacji pojawiły się wojny, których przyczyną były osobiste porachunki poszczególnych grup. Zazwyczaj były to wojny „o honor” władcy/rodu/grupy, albo działania odwetowe. Te ostatnie były po prostu przedłużeniem jakiegoś innego konfliktu. Przykładem tego typu wojen jest chociażby Wojna TrojańskaW. Co prawda można mówić tu o wojnie o zasoby (kobiety kiedyś były traktowane jako zasoby), ale w znacznej mierze chodziło o urażony honor jakiegoś kacyka.
Historia powoli toczy się dalej i wraz z rozwojem systemów wierzeń jako składowych państwa pojawiają się wojny religijne. Tu można wyróżnić dwie gałęzie. Pierwsza to wojny rozłamowe. Prowadzone gdy jakieś dwie grupy mają odmienne zdanie na temat doktryny. Przykładem niech będą Wojny HusyckieW, albo wyprawa Wielkiej ArmadyW. Przy czym często gęsto jest tak, że religia jest tylko propagandowym sloganem dla mas, a rzeczywisty powód jest zupełnie inny. Druga gałąź to wojny „o nawrócenie”, przykładowo Krzyżacy w Prusach czy też wojny konkwistadorskie. Przy czym tak jak w poprzednim wypadku, czym innym jest propaganda, a czym innym rzeczywiste zapotrzebowanie.
Lata mijały, a wraz z Rewolucją Francuską powstała konieczność rozdziału kościoła i państwa. Rolę religii w propagandzie zaczęła przejmować kultura, filozofia, „cywilizacja”. „Cywilizację” dzielnie niesiono ludom afrykańskim, „kultura” była podana Japończykom, ale też i na przykład Chińczykom. „Filozofia” stała się źródłem rewolucji w Rosji, ale i II Wojny Światowej.
Można zatem powiedzieć, że wojna w swojej istocie ma źródło w zapotrzebowaniu na zasoby. Jednak wraz z rozwojem cywilizacji wytworzyliśmy sobie mechanizmy propagandowe, które pozwalają na przekazania społeczeństwu, że wojnę prowadzimy w imię „wyższej sprawy”.

Konflikt cywilizacji

To z czym mamy dzisiaj do czynienia jest kolejny konflikt cywilizacji. Przynajmniej w warstwie propagandowej. W rzeczywistości jest to nadal wojna o zasoby m.in ropę w Libii i Nigerii oraz kontrole nad Kanałem Suezkim, który jest jednym z najważniejszych punktów na mapie świata.
Odłóżmy na chwilę zasoby i skupmy się na tym czym jest konflikt cywilizacji i jakie elementy decydują o rozstrzygnięciu na korzyść jednej czy drugiej strony.
Jeżeli popatrzymy na tego rodzaju spory w historii to warunki zwycięstwa są jasne. Dana strona wygrywa jeżeli uda się jej narzucić innym stronom swój sposób myślenia i postępowania.

Europa vs ISIS

A teraz mięsko.

Współczesna cywilizacja europejska opiera się o wolność.
Przy czym nie jest to wolność jadąca na kucyku, która krzyczy o likwidacji ZUS-u. Swoją drogą możliwość swobodnego negowania systemu gospodarczego jest przejawem wolności.
Wolność w rozumieniu europejskim to przede wszystkim równość wszystkich ludzi. W tej filozofii różnice pomiędzy ludźmi, na które ludzie ci nie mają wpływu, nie mogą być podstawą do różnego traktowania. Przykładowo nie dyskryminujemy ze względu na kolor skóry, pochodzenie etniczne, płeć czy niepełnosprawność. Jednocześnie określiliśmy pewien katalog cech, na które ludzie mają wpływ, ale nie mogą być one przyczyną dyskryminacji o ile nie stoją w jawnej sprzeczności z prawem bądź nie negują podstaw filozofii. Przykładowo religia, poglądy polityczne, strój, zachowanie w miejscach publicznych.
Wolność w takim rozumieniu oznacza też, że nie dzielimy ludzi na „naszych” i „obcych”. Podział ten nie ma tu po prostu sensu.

Co oferuje nam cywilizacja ISIS.
Można powiedzieć, że jest ona zaprzeczeniem cywilizacji jednak nie jest to takie proste. ISIS wyznaje filozofię absolutyzmu w oparciu o interpretację wprost koranu. Paradoksalnie nic niezwykłego dla europy. Przez stulecia władcy europejscy właśnie w ten sposób sprawowali władzę. Chociażby Święte Cesarstwo Rzymskie czy też Cesarstwo Rosyjskie.

Jakie są warunki zwycięstwa

Jak już wspomniałem warunkiem zwycięstwa w tej wojnie jest narzucenie przez jedną ze stron swojego sposobu postępowania poprzez zmuszenie przeciwnika do rezygnacji ze swoich wartości.

Dlaczego ISIS wygra?

Zanim odpowiem na to pytanie rozważmy jakie cele miało ISIS w zamachach w Paryżu.

Pierwszym najprostszym celem jest „żądza mordu”. To jest jednak zasłona dymna. Propagandowo dobrze się sprzedające hasełko o „zabijaniu niewiernych”. Może ono działać w Afryce gdzie ludzi morduje się w ilościach hurtowych, ale gdzie tam detalistom z Europy. Jeżeli celem było by mordowanie niewiernych to zastosowano by zupełnie inne środki chociażby te znane z Iraku. Kilka dużych ładunków wybuchowych wybuchających w jednym miejscu w krótkich odstępach czasu czy też „brudna bomba” dadzą znacznie lepszy rezultat niż kilku gości z kałachami.
W rzeczywistości pierwszym celem jest zniechęcenie społeczeństw zachodnich do udziału w konflikcie w Syrii. Wojnę prowadzi się na lądzie, morzu i w powietrzu. Jeżeli masz duże siły lądowe, ale nie dysponujesz lotnictwem to prowadzenie działań wojennych jest bardzo utrudnione. Możliwe są pewne sukcesy, ale jeżeli przeciwnicy wprowadzą do działań lotnictwo to jesteś na straconej pozycji. Przykład Ofensywa w ArdenachW w 1944. Wermachtowi szło do momentu, aż alianci wprowadzili do zabawy lotnictwo. podobnie w Syrii. ISIS ma bardzo mocne siły lądowe, ale każda próba wykorzystania ich na dużą skalę w „klasycznym blitzkriegu” opartym o siły zmotoryzowane zakończy się klęską, bo przylecą chłopcy z NATO i będzie jak na filmie:

Jedyna metoda na eliminację wrogiego lotnictwa jaką ma ISIS to zatrzymanie fali uchodźców. Uchodźcy są „namacalni”. Widać ich na ulicach w Europie i społeczeństwa domagają się od władz „rozwiązania kwestii”. Władze wiedzą, że poza doraźną pomocą muszą wyeliminować źródło problemu jakim jest ISIS, a zatem wysyłają samoloty. Jeżeli w jakiś sposób z obrzydzić europejczykom uchodźców to:

  • Zaczną „odbijać się” od granicy unii,
  • Zmniejszy się ich liczba na terenie UE, ponieważ przy okazji zaostrzenia procedur dużo ludzi straci prawo pobytu,
  • Znikną z ulic przez co przestaną być „widocznym problemem”,
  • Co oznacza, że nie będzie poparcia społecznego dla eliminacji źródła problemu,
  • Co oznacza, że władze nie będą mogły znaleźć poparcia dla kontynuowania nalotów.

W efekcie konflikt w Syrii przerodzi się w konflikt lokalny, który wygra ISIS. Podobnie jak miało to miejsce w Darfurze. Społeczeństwa europejskie nie odczuły tamtego konfliktu, a w efekcie nie dały mandatu do interwencji. Koniec końców wygrali islamiści.

Drugim celem jest zmiana sposobu myślenia europejczyków i o europejczykach. Realizacja tego celu będzie widoczna w przeciągu następnych 20-30 lat. Jeżeli Europa stanie się nieprzyjazna dla „obcych” to za te 20-30 lat ISIS wychowa sobie pokolenie ludzi, którzy nie znają innej, otwartej i przyjaznej, Europy. Zatem działania propagandowe w stylu „a u was biją murzynów”, nie będą konieczne. Po prostu będzie to prawda.

Tu mamy już odpowiedź na pytanie postawione na początku. Jeżeli Europa zmieni swoje nastawienie w stosunku do wszystkich imigrantów to będzie oznaczać jej przegraną. Pod wpływem działań ISIS odrzucamy podstawę naszej cywilizacji jaką jest przekonanie o wolności i równości wszystkich ludzi niezależnie od ich pochodzenia. Stajemy się też tak samo ksenofobiczni jak ISIS. Jedyna różnica będzie polegać na tym, że nasze nowe hasła, przejęte od ISIS i lekko zmodyfikowane na potrzeby naszych realiów, zapiszemy innym alfabetem.

Czy jest jakaś strategia wygrywająca dla Europy?

To co tu teraz opiszę jest tylko pewnym bardzo ogólnym schematem, który ma szansę zadziałać. Mam świadomość, że sytuacja jest bardziej skomplikowana i żadne proste rozwiązania nie są cudownym lekiem.

Zatem lista kilku działań, które mogą okazać się przydatne.

  • Eliminacja z życia publicznego wszystkich duchownych nawołujących do obalenia ustroju. Niezależnie od religii. To jest dokładnie taka sama zaraza.
  • „Przeproszenie się” z władzami Syrii.
  • Bezpośrednia interwencja na terenie Syrii, Libii, Egiptu, Iraku z użyciem całego potencjału konwencjonalnego. Chce ISIS wojować… to będziemy wojować.
  • Poddanie ostracyzmowi ekonomicznemu państw wspierających ISIS. Ze szczególnym uwzględnieniem krajów Zatoki Perskiej.
  • Jak najszybsza integracja uchodźców z resztą społeczeństwa. Poprzez rozproszenie ich w społeczeństwie i uniemożliwienie budowania zamkniętych jednolitych gett.

Co do ostatniego punktu, to zawsze pozostaje (od 63 sekundy):

Wiem, że wiele osób nie podziela przedstawionego tu poglądu. Dla wielu „wysłanie do piachu” uchodźców jest całkiem „rozsądnym” rozwiązaniem. Na zakończenie chciałbym jednak zwrócić wasza uwagę, na jeszcze jeden aspekt całej sprawy. Jeżeli dziś Europa odrzuci wolność, odmawiając jej pewnej grupie ludzi to za kilka, kilkanaście lat kolejne pokolenia europejczyków zaczną odmawiać jej sobie na wzajem. Co skończy się powrotem do stanu tuż przed IWŚ. Ze wszystkimi tego konsekwencjami.

Sposób na chujową kawę z buirokspressu…

Listopad 5th, 2015

Biurowy ekspress do kawy.

Przez dwa s jak SS, bo

  • Jest to taki mały nazista, o tym za chwilę
  • ponieważ ekspesso piszemy przez dwa s

Generalnie maszyna będąca obiektem kultu w każdym biurze. Słusznie, bo maszyny powinny być obiektem kultu. Występująca w dwóch wersjach. Wersja pierwsza to zwykły ekspres przelewowy. Znaczy się z góry sypiemy paczkę zmielonej kawy do specjalnego pojemniczka (czasami wymagany jest ekstra filtr), odpalamy i na dole w dzbanuszku zbiera się smolisty nektar. Jednak od pewnego czasu w biurach zaczynają królować ekspressy do ekspresso, czyli ciśnieniowe. Tu sypiemy z góry ziarenka, robi się BZIUUUUUUUUUU i po chwili zaczyna lać się nektar. Rzecz w tym, że ten drugi rodzaj ekspressów ma na pokładzie BZIUUUUUkownicę, czyli młynek. Młynek ma to do siebie, że trzeba o niego dbać. Generalnie jest to delikatny, choć potężnym, mechanizm zamieniający ziarenka w zmieloną kawę. Jednym z elementów dbania jest regularne czyszczenie. To dotyczy też innych mechanizmów ekspressu. Szczególnie tych gdzie przepływa mleko.

Wraz z wprowadzeniem ekspressów do ekspresso do biur, ktoś zapomniał o tym, że sprzęt trzeba czyścić i serwisować. W efekcie po kilku tygodniach całość zarasta mlecznym kożuchem chyba, że znajdzie się ktoś kto to czyści, albo jak nawet nie zarośnie to młynek się zacina lub zaczyna źle mielić. W efekcie pijemy typową biurową chujową kawę.

Ja znalazłem na to sposób, a w zasadzie dwa.

Sposób 1. Uprawiany

Źli, niedobrzy, jebani imigranci otworzyli pod biurem kawiarnię i robią zajebistą kawę odbierajonc prace prawdziwym polakom z okolicy przez co ci nie mają na Amarene. Do tego te kurwy lewackie zatrudniają na umowach o pracę. Espresso kosztuje 5PLN, czyli w granicach 1-1,5 EUR. Jak w całym cywilizowanym świecie.
Zacząłem ich regularnie wizytować. Miła obsługa, dobra kawa, i nie jebie gotowanym dorszem po wegańsku (bo ktoś akurat podgrzewa w mikrofali)

Sposób 2. Uprawiany będzie w innym biurze

Zakup własnego dripera i zbudowanie emulatora ekspresu przelewowego. Jednak nie w obecnym biurze, bo tu mi go po prostu odparują. Nawet nie przez czystą złośliwość, ale z uwagi na to, że takie rzeczy jak sztućce, kubki i inne drobne wyposażenie lubi wędrować po piętrach.

Co, gdzie, kiedy – czyli przykład refaktoryzacji w kierunku FP.

Październik 28th, 2015

Dziś na warsztat idzie przykład refaktoryzacji, który nazywam „Co, gdzie, kiedy”. Nazwa nie jest przypadkowa, ponieważ celem jest takie przekształcenie kodu imperatywnego by jak najmocniej odseparować od siebie trzy elementy, które są stałe przy pracy z kolekcjami.

Co

Pierwszy element to określenie co chcemy zrobić. Zazwyczaj chodzi o weryfikację jakiegoś warunku albo o dokonanie obliczeń na jakiejś kolekcji. Za ten element w zwykłym programie odpowiada odpowiednio zdefiniowana metoda.

Gdzie

Jeżeli wiemy co chcemy osiągnąć to musimy też zdefiniować na jakiej kolekcji będziemy pracować. Gdzie jest tu rozumiane jako punkt w naszej domenie, w którym mamy dostęp do pewnej kolekcji z którą możemy pracować w określony sposób.

Kiedy

Ostatnim elementem pracy z kolekcją jest określenie w którym momencie chcemy dokonać naszej operacji na jakiejś kolekcji. W typowym programie odpowiada za to miejsce wywołania naszej metody z kolekcją jako parametrem (lub metody, która jakoś sobie „dociąga” kolekcję).

Przypadek praktyczny

W zasadzie kliniczny przykład kodu, który należy zrefaktoryzować.

Listing 1. Tradycyjne, imperatywne podejście

public boolean checkDomainObject(String objName, String objHash) {     
	Collection<DomainObject> domainObjects = getDomainObjects();

	if (domainObjects.isEmpty()) {
		return false;
	}

	for (DomainObject st : domainObjects) {
		if (st.getName().equals(objName)) {
			return !(objHash != null && !objHash.equals(st.getHash()));
		}
	}

	return false;
}

Metoda przyjmuje dwa parametry nazwę obiektu i jego hash. Pobiera listę obiektów, po czym sprawdza czy istnieje na niej obiekt o podanej w parametrze nazwie. Jeżeli istnieje i podano też parametr hash to sprawdzana jest poprawność hasha. Dokonano tu „optymalizacji” w postaci dodatkowego ifa, który sprawdza czy kolekcja jest pusta. Jeżeli jest to od razu zwraca false. Poza zaśmieceniem kodu to nic nie da.
Kod ten jest dość prosty w testowaniu. metoda getDomainObjects woła pod spodem repozytorium, które możemy zamockować.
Jednak czy ten kod jest rzeczywiście czytelny?
Niektórzy powiedzą, że jest. I będą mieć rację. Przynajmniej w świecie programowania imperatywnego. Problem z tym kodem polega na tym, że miesza

  • Co – czyli instrukcje warunkowe i stojącą z animi logikę biznesową.
  • Gdzie – czyli kolekcję, którą do której „doszywa” sposób jej przetwarzania.
  • Kiedy – czyli będzie to wywołane w momencie gdy wywołujemy metodę.

Jeżeli będziemy wstanie odseparować od siebie poszczególne elementy kodu to stanie się on czytelniejszy.

Refaktoryzacja krok po kroku

Prześledźmy zatem proces refaktoryzacji tego kodu krok po kroku. Należy tu zaznaczyć, że kod, który będziemy zmieniać ma napisane testy. Zatem możemy bezpiecznie dokonywać przekształceń za każdym razem uruchamiając testy by sprawdzić czy zachowanie pozostało bez zmian.

Uproszczenie kodu

Pierwszym etapem jest uproszczenie i oczyszczenie kodu z nadmiarowych elementów. Możemy z czystym sumieniem wyrzucić instrukcję sprawdzającą czy kolekcja jest pusta. Korzystając z praw De MorganaW możemy też uprościć warunek sprawdzający hash obiektu.

Listing 2. Tradycyjne, imperatywne podejście po oczyszczeniu

public boolean checkDomainObject(String objName, String objHash) {
	Collection<DomainObject> domainObjects = getDomainObjects();

	for (DomainObject st : domainObjects) {
		if (st.getName().equals(objName)) {
			return objHash == null || objHash.equals(st.getHash());
		}
	}

	return false;
}

W obecnej formie nie możemy zrobić wiele więcej. Kolejnym przekształceniem może być zamiana pętli foreach na while, ale to tylko sztuka dla sztuki. Nie wniesie nam nic nowego. Kod choć prostszy nadal miesza w sobie różne elementy. Mamy tu zarówno logikę biznesową, pobieranie danych, sposób przetwarzania, a całość jest wykonywana od razu.

Opakowanie warunków

W powyższym kodzie mamy trzy warunki. Pierwszy dla nazwy, drugi dla parametru objHash i trzeci porównujący objHash z wartością z elementu kolekcji.
Zamieńmy te trzy warunki na predykaty.

Listing 3. Zamiana warunków na predykaty

public boolean checkDomainObject(String objName, String objHash) {
	Collection<DomainObject> domainObjects = getDomainObjects();

	Predicate<DomainObject> name = domainObject -> domainObject.getName().equals(objName);
	Predicate<DomainObject> nullHash = domainObject -> objHash == null;
	Predicate<DomainObject> hash = domainObject -> objHash.equals(domainObject.getHash());
	Predicate<DomainObject> fullHash = nullHash.or(hash);

	for (DomainObject st : domainObjects) {
		if (name.test(st)) {
			return fullHash.test(st);
		}
	}

	return false;
}

Kod wydłużył się, ale udało nam się częściowo odizolować logikę od sposobu przetwarzania. Kod ten ma też inną ciekawą właściwość. Widać jakie warunki brzegowe potrzebne są do testów. Jeżeli uda nam się przygotować testy dla pierwszych trzech predykatów tak by obejmowały wszystkie możliwe warianty to mamy pokryty te wszystkie gałęzie kodu. Widać tu też dodatkowy warunek, którego jeszcze nie zamieniliśmy na predykat. Instrukcja if i następujący w jej bloku predykat można połączyć spójnikiem i.
To oczywiście w obecnej formie jest dość zagmatwane zamieńmy więc pętlę na coś bardziej do ludzi i wprowadźmy ten dodatkowy predykat.

Usunięcie pętli

Pętla w tym kodzie służy do wyszukania pierwszego elementu spełniającego warunek. Jeżeli w kolekcji jest taki element to gdy zostanie on odszukany to zwracamy true. Takie samo działanie ma metoda anyMatch ze strumieni. Zastąpmy więc pętlę wywołaniem tej metody.

Listing 4. usunięcie pętli i nowy predykat

public boolean checkDomainObject(String objName, String objHash) {
	Predicate<DomainObject> name = domainObject -> domainObject.getName().equals(objName);
	Predicate<DomainObject> nullHash = domainObject -> objHash == null;
	Predicate<DomainObject> hash = domainObject -> objHash.equals(domainObject.getHash());
	Predicate<DomainObject> domainObjectPredicate = name.and(nullHash.or(hash));

	return getDomainObjects().stream().anyMatch(domainObjectPredicate);

}

W kodzie tym zastąpiliśmy jeden z predykatów innym, który jest bardziej ogólny. Całość jest już prawie dobra. Mamy odseparowaną logikę i sposób przetwarzania. Potrafimy powiedzieć co chcemy zrobić i gdzie będziemy to robić. Pozostaje nam zmiana kodu tak by nie wykonywał się zachłannie.

Przesunięcie momentu wywołania

Ostatnim krokiem jest sprawienie by nasza metoda tylko przygotowywała odpowiednie wywołanie, ale nie wykonywała go. Niech zrobi to ten, który tego potrzebuje.

Listing 5. Wprowadzenie leniwej ewaluacji zadania

public Callable<Boolean> checkDomainObject(String objName, String objHash) {
    Predicate<DomainObject> name = domainObject -> objName.equals(domainObject.getName());
    Predicate<DomainObject> nullHash = domainObject -> objHash == null;
    Predicate<DomainObject> hash = domainObject -> objHash.equals(domainObject.getHash());
    Predicate<DomainObject> domainObjectPredicate = name.and(nullHash.or(hash));

    return ()-> getDomainObjects().stream().anyMatch(domainObjectPredicate);
}

Zmiana taka wiąże się ze zmianą sygnatury metody. Sam kod jest już odpowiednio posegregowany. Wiemy co chcemy zrobić. Wiem gdzie to chcemy zrobić i dajemy wolną rękę wywołującemu co do terminu wykonania.

Uogólnianie

Nadal jednak jest to kod silnie związany z pewną ściśle określoną logiką. Podobne konstrukcje będą przewijać się przez projekt w kontekście różnych warunków i różnych kolekcji. Pytanie brzmi czy możemy napisać kod, który uogólni nam rozwiązanie.

Uogólniona funkcja

Nasza uogólniona funkcja będzie musiała sobie poradzić z bardzo ogólnymi założeniami. Pierwszym z nich jest to, że musi zwracać Callable pasujące do tego co zrobimy z kolekcją. Drugim, że musi przyjmować jako argumenty pewną operację reprezentowaną przez funkcję dwuargumentową, której pierwszym argumentem będzie Stream, a drugim argument pasujący do operacji na strumieniu obiekt, jakiegoś dostawcę kolekcji z którego wyprodukujemy strumień, oraz pasujący obiekt.

Mówiąc prościej potrzebujemy czegoś takiego:

Listing 6. Funkcja uogólniona

interface Invoke {
	default <T, U, R> Callable<R> invoke(BiFunction<Stream<T>, U, R> bf, Supplier<Collection<T>> c, U a) {
		return () -> bf.apply(Optional.fromNullable(c.get()).or(Collections.emptyList()).stream(), a);
	}
}

I można się zapytać co się tu odpierdala… Cała „magia” tego rozwiązania leży w tym w jaki sposób zamieniamy kolejne argumenty funkcji invoke na elementy wywołania naszej logiki. Przykładowa specjalizacja powyższego kodu gdy chcemy zastosować go w naszym przypadku.

Listing 7. Specjalizacja funkcji uogólnionej

interface AnyMatch extends Invoke {

	default <T> Callable<Boolean> anyMatch(Supplier<Collection<T>> c, Predicate<T> a) {
		return invoke((s, p) -> s.anyMatch(p), c, a);
	}
}

Pierwszym argumentem jest to co chcemy wykonać. Drugim to na czym chcemy wykonać daną operację z, i trzecim, użyciem jakich argumentów.
Dodatkowo jeżeli dostaniemy kolekcję, która jest null (jak to w javie bywa), to zamiast niej użyjemy pustej, niemodyfikowalnej kolekcji. Jako, że nie wiemy co możemy podstawić pod domyślne parametry dla argumentów operacji (może być cokolwiek) zatem przekazujemy je tak jak są. Jak będą null to co najwyżej coś się wywali.

Finalnie nasz kod będzie wyglądał tak:

Listing 8. Finalne rozwiązanie

public Callable<Boolean> checkDomainObject(String objName, String objHash) {
    Predicate<DomainObject> name = domainObject -> objName.equals(domainObject.getName());
    Predicate<DomainObject> nullHash = domainObject -> objHash == null;
    Predicate<DomainObject> hash = domainObject -> objHash.equals(domainObject.getHash());
    Predicate<DomainObject> domainObjectPredicate = name.and(nullHash.or(hash));

    return anyMatch(this::getDomainObjects, domainObjectPredicate);
}

Odseparowaliśmy to co chcemy zrobić od tego gdzie to jest robione. Zwracamy zaś coś co pozwala wywołującemu na zdecydowanie kiedy wykonać daną operację.

Utrzymanie wielu wersji API w obcym środowisku cz. II

Październik 25th, 2015

Utrzymanie wielu wersji API w obcym środowisku cz. I

W pierwszej części ogólnie omówiłem z jakiego rodzaju problemami możemy mieć do czynienia gdy mamy narzuconą infrastrukturę. W tej części omówię jak radzić sobie z tymi ograniczeniami w zależności od tego jakie restrykcje zostały nam narzucone.

Na samym początku należy uzmysłowić sobie, że nie każda zmiana w aplikacji oznacza konieczność zmiany wersji API.

Co oznacza wersjonowanie API?

Jeżeli będziemy chcieli odpowiedzieć na to pytanie to musimy najpierw określić jakie elementy API powodują, że potrzebujemy je wersjonować. Jeżeli przyjrzymy się typowej metodzie lub funkcji w praktycznie dowolnym języku programowania to będziemy wstanie wyznaczyć takie trzy elementy. Pierwszym najbardziej oczywistym jest nazwa. Jeżeli zmieniamy nazwę metody to zmieniamy nasze API. W przypadku REST mówiąc o nazwie metody mam na myśli adres URL, jego fragment, pod którym leży sobie usługa. Drugim elementem są parametry. Zarówno wejściowe jak i wyjściowe. Możemy je dodawać, usuwać, zamieniać. W REST parametry mogą przyjmować różne postacie. Mogą być częścią adresu URL, mogą znajdować się w RequestBody, a jako odpowiedź będą to zarówno kody odpowiedzi jak i ResponseBody. Parametrami są też nagłówki HTTP. Trzecim elementem jest struktura parametrów. Szczególnie jeżeli wysyłamy i odbieramy je w postaci JSON albo XML. Zmiana w strukturze tych danych może, ale nie musi, pociągać za sobą konieczność zmiany wersji.
Tym co nie wpływa na wersję REST API jest jego zachowanie. Paradoksalnie jeżeli zachowamy sygnaturę metody to zmiana jej implementacji będzie co najwyżej zmianą z bardzo abstrakcyjnego biznesowego punktu widzenia.

Przykład

Jaś i Małgosia rozwijają swoją aplikację „Burn the witch”. Udostępnia ona REST API. Jedna z metod pozwala na pobranie listy czarownic do spalenia. Jako parametr w URL przyjmuje identyfikator regionu, a w RequestBody można doprecyzować dodatkowe reguły wyszukiwania. Lista reguł wyszukiwania jest zamkniętym słownikiem, czyli akceptowane będą tylko określone wartości. Odpowiedź zawiera listę czarownic wraz z ich adresami.

Listing 1. Przykładowe wywołanie serwisu

/api/witches/region/666
{
    'name' : '%Baba%',
    'age' : '>=100'
}

Listing 2. oraz jego odpowiedź

HTTP/1.1 200 OK

[
   {
      'name' : 'Baba Jaga',
      'age' : 101,
      'address' : 'Leśna Łączka 11'
   }
]

Wraz z rozwojem biznesu zaistniała konieczność zmiany bazy danych. Nowa implementacja jest szybsza i wydajniejsza, ale pomimo, że była dość dużą zmiana po stronie serwera to z punktu widzenia API nic się nie zmieniło. Kolejna zmiana polegała na otwarciu słownika. Od tego momentu można włożyć do RequestBody dowolne wartości jako klucze. Aplikacja będzie jednak ignorować nieznane klucze. Z punktu widzenia klienta jest to zmiana API pomimo, że po stronie aplikacji praktycznie prawie nic się nie zmieniło. Co prawda starsze wersje klienta nie muszą być aktualizowane by być kompatybilne z nowym API, ale bez aktualizacji nie będą wstanie korzystać z nowych możliwości. Co ważne aktualizacja klienta może odbywać się poprzez usunięcie z procesu pobrania listy kluczy słownika. Przykładowo:

Listing 3. Stara wersja klienta

var paramMap = getSearchConditions();
var validKeys = $client.getDictionaryKeys();
var paramMap = removeInvalidKeys(paramMap, validKeys);
$client.getWitches(paramMap);

zamieni się na:

Listing 4. Nowa wersja klienta

var paramMap = getSearchConditions();
$client.getWitches(paramMap);

Jednak nie nastąpią zmiany w samym obiekcie $client. Oczywiście o ile przyjmiemy, że nie posiada on własnych mechanizmów walidacji.

Kolejną zmianą była zmiana domeny i mapowania metod. Z punktu widzenia kodu aplikacji nic się nie zmieniło. Z punktu widzenia klienta takiego API jest to bardzo duża zmiana, która pociąga za sobą konieczność zmiany w bardzo wielu miejscach. Taka zmiana wymaga już wprowadzenia nowej wersji ponieważ zmieniło się mapowanie metod. Uwaga zmiana domeny nie oznacza zmiany w mapowaniu samych metod.

To by było na tyle jeśli chodzi o pojęcie zmiany API. Mam świadomość, że może być to niejasne, ale jakoś nie mam pomysłu jak to ładnie ubrać w słowa. Przejdźmy do problemów jakie wynikają z konieczności wersjonowania API.

Problem najprostszy – własny tool klienta

Kiedyś dawno temu jak nie było Dockera… ok nie tak dawno temu w sumie duże instytucje pisały własne narzędzia do zarządzania aplikacjami i ich położeniem na infrastrukturze. Mówiąc prościej w każdej organizacji był zespół, który dostarczał narzędzie pozwalające na umieszczanie konkretnych apek na konkretnych maszynach. Czasami takie narzędzia kupowano, czasami pisano samemu. Wspólnym mianownikiem dla tego typu systemów jest brak publicznego API lub też jego mała popularność.
Wymaga to oczywiście dostosowania naszych skryptów do tego API. W efekcie proces budowania i uruchamiania aplikacji wymaga dużo nauki na „dzień dobry”.

Jest to najprostszy z możliwych problemów. Ponieważ tak naprawdę jest on tylko pozornym ograniczeniem wynikającym z braku wiedzy. W praktyce problem własnego API klienta jest punktem wyjścia w większości przypadków. Jeżeli go rozwiążemy to mamy dużo prościej.

Problemy rzeczywiste

Te zazwyczaj związane są z ilością oraz jakością dostępnych zasobów. Ich rozwiązanie będzie wymagać od nas pracy w dwóch obszarach. Pierwszy to konfiguracja infrastruktury. Drugi to konfiguracja aplikacji. Poniżej postaram się opisać najpopularniejsze problemy, a następnie podać rozwiązania pozwalające na redukowanie ich do problemu z API systemu zarządzania infrastrukturą.

Bierzcie i jedzcie

W tym przypadku klient udostępnia nam nieograniczone zasoby. Praca w takim środowisku rozbija się tylko o przygotowanie odpowiednich skryptów by móc zautomatyzować proces uruchamiania kolejnych instancji aplikacji.

Masz kilka serwerów

W tej klasie problemów mamy nałożony limit na ilość maszyn, które możemy wykorzystać. Może być jedna, może być kilka. Warunkiem koniecznym do rozwiązania tego rodzaju problemu jest odpowiednie balansowanie poszczególnych wersji aplikacji. Inaczej będziemy konfigurować nasze środowisko gdy musimy obsłużyć duży, ale stosunkowo lekki ruch. Inaczej jeżeli ilość żądań jest niewielka, ale są „masywne” (np. produkcja raportów w postaci plików pdf czy excela). Ciężko jest mi tu wskazać jedyną słuszną drogę.

Masz jeden serwer

Jest to specyficzna wersja poprzedniego problemu. W tym przypadku mając jedną maszynę musimy rozważyć możliwość uruchomienia wielu instancji aplikacji. Można zrobić to na dwa sposoby. Pierwszym jest uruchomienie wielu instancji serwera aplikacyjnego na różnych portach. W ten sposób działa domyślna konfiguracja TeamCity. Na jednej maszynie mamy uruchomiony serwer master oraz domyślnie trzy workery, każdy na oddzielnym porcie.
Tego typu rozwiązanie wymaga jednak dużej elastyczności samej maszyny. Niczym niezwykłym jest tu serwerek w rodzaju 16 rdzeni, 128GB ramu. Gorzej jak mamy ograniczenia w mocy maszyny…

Masz jeden serwer 2

Sytuacja gdy poza ograniczeniem ilości maszyn do jednej jesteśmy też ograniczeni w ilości instancji serwerów aplikacyjnych do jednej stawia nas w bardzo trudnym położeniu. W takim przypadku musimy oprzeć się na uruchamianiu poszczególnych aplikacji w różnych kontekstach w ramach jednego serwera. Jako, że standardowe mechanizmy GC wysiadają w okolicach 10-12GB ramu, działają z mniejszą efektywnością, zatem pojawia się też ograniczenie na zasoby. W efekcie musimy zwrócić szczególną uwagę na możliwość wycofywania starych wersji aplikacji.

Masz jeden kontekst

A teraz drogie dzieci najpopularniejsza sytuacja… zazwyczaj ograniczenia w infrastrukturze ze strony klienta objawiają się pełną wirtualizacją środowiska. Wrzucamy naszą apkię na „magiczne API” i wiemy jaki jest adres. Nie mamy możliwości wrzucenia jej wielu instancji. Zapomnijmy o uruchomieniu wielu kontekstów. Jest jedyny słuszny kontekst, który został nam narzucony. W takim wypadku musimy obsłużyć wersjonowanie w ramach pojedynczej aplikacji. Cały routing związany z rozróżnianiem wersji musi zostać zrealizowany w ramach.
Taka sytuacja jest, paradoksalnie, prostsza do obsłużenia od poprzedniej pod warunkiem, że korzystamy z jakiegoś DI oraz mamy przemyślaną architekturę naszej aplikacji. W takim wypadku routing może zostać ograniczony do minimum.

Jak wersjonować API

W ogólności istnieją dwa „koszerne” sposoby na rozróżnianie wersji API.

Zasada ogólna

Niezależnie którą wersję wybierzemy musimy trzymać się jednej zasady. Przed swoją aplikacją należy umieścić router w postaci czy to serwera proxy (z loadbalancerem) czy to mechanizmu w aplikacji, który pozwoli na konfigurowalne decydowanie, która wersja API zostanie wywołana.

Różne wersje – różne URL-e

Jest to najprostszy mechanizm. Czasami jest on też najbardziej naturalny sposób na dostęp do API. Przykładowo jeżeli nasz router znajduje się po stronie klienta np. jest to system ERP albo BPM, który umożliwia korzystanie z wielu wersji danego API i potrafi parametryzować wywołania, oraz nasza aplikacja działa na kilku różnych serwerach lub w kilku kontekstach to wykorzystanie URL-i do wersjonowania jest bardzo fajne.
Oczywiście oznacza to, że zmiana wersji API może powodować pewne problemy po stronie infrastruktury. Nowy URL może wymagać dodatkowych reguł na firewallach.
Bardzo ważną rzeczą w przypadku wyboru tego rozwiązania jest odpowiednie zbudowanie adresu. Numer wersji nie powinien znajdować się w samych mapowaniach usług.

Listing 5. Złe mapowania

http://mojaaplikacja.com:80/api/usługa/v1/metoda
http://mojaaplikacja.com:80/api/usługa/metoda?v1

Listing 6. Dobre mapowania

http://mojaaplikacja.com:80/api/v1/usługa/metoda
http://v1.mojaaplikacja.com:80/api/usługa/metoda

Złe mapowania powodują, że obsługa mapowania jest trudna. Trzeba używać dodatkowych mechanizmów do ekstrahowania wersji. Trudna jest też konfiguracja wersji domyślnej.

Nagłówek

Protokół HTTP pozwala na zdefiniowanie własnych nagłówków oraz na zdefiniowanie dodatkowych wartości do istniejących, standardowych nagłówków.
Jeżeli wybieramy własny nagłówek to należy pamiętać o pewnej konwencji, która mówi, że nazwa własnego nagłówka powinna zaczynać się od X-.
Inną metodą jest użycie nagłówka Accept z niestandardową (czytaj nie wspieraną przez np. przeglądarki czy serwery w domyślnej konfiguracji) wartością. Tu znowuż konwencja jest taka, że wartość powinna wyglądać mniej więcej w taki sposób

Listing 7. Przykładowa wartość nagłówka Accept

Accept: application/vnd.company.appname.witch-v1+json

W przypadku użycia nagłówka Accept należy zadbać o odpowiedni nagłówek Content-Type

Listing 8. Przykładowa wartość nagłówka Content-Type

Content-Type: application/vnd.company.appname-v1+json

Pierwszy z nagłówków określa czego oczekuje klient. ważne jest doprecyzowanie, że oczekujemy reprezentacji pewnego obiektu witch w formacie json. Odpowiedź z aplikacji informuje nas w jakim formacie są dane.

Podsumowanie

Wersjonowanie aplikacji to sprawa dość upierdliwa. Z punktu widzenia programisty rozwiązanie oparte o użycie nagłówka Accept jest najlepsze i najbliższe ideałowi. Jednocześnie ma też największy narzut.
Z drugiej strony użycie różnych URL-i jest bardzo proste, ale może odbić się w przyszłości ograniczeniem elastyczności aplikacji. Wymusi na nas trzymanie się złej konwencji, a próba zmiany będzie oznaczać konieczność przepisywania dużej ilości kodu zarówno po naszej stronie jak i stronie klientów.

W trzeciej części przyjrzymy się temu jak rozwiązać problem wersjonowania API w bazie danych gdy nasze zasoby są ograniczone.


Translate »