Używaj Varv.io mówili, będzie fajnie mówili

O ile oczywiście ładnie potniesz kod na mniejsze elementy, bo możesz spłodzić potwora:

Listing 1. Generowanie losowego NRB

public String generateNrb() {
    return concat(
            continually(() -> r.nextInt(10)).take(24),
            Stream.of(2, 5, 2, 1, 0, 0)
    ) 
            .zip(NrbGenerator.WEIGHTS.toStream()) 
            .unzip(t -> of(t._1() * t._2(), "" + t._1())) 
            .map((crcOfDigits, digits) ->
                    of(
                            (98 - (crcOfDigits
                                    .collect(
                                            summarizingInt(Integer::intValue)
                                    )
                                    .getSum() % 97)
                            ) + "",
                            digits.collect(
                                    joining()
                            )
                    )
            ) 
            .map(
                    (crc, val) -> of(
                            crc.length() < 2 ? "0" + crc : crc,
                            val.substring(0, 24)
                    )
            ) 
            .toSeq() 
            .map(Object::toString) 
            .collect(joining());
}

I pamiętajcie nie zostawiajcie kodu w takim stanie. Po doprowadzeniu do tego etapu należy kod jeszcze zrefaktoryzować wyciągając poszczególne kroki do osobnych funkcji/metod, tak by uzyskać:

Listing 2. To samo inaczej

public String generateNrb() {
    return concat(
            generateRandomNrbDigits(),
            getPL00Indicator()
    ) 
            .zip(NrbGenerator.WEIGHTS.toStream()) 
            .unzip(t -> of(
                    calculateDigitCrcValue(t), digitToString(t)
                    )
            ) 
            .map(this::crc, this::joinNrbDigits) 
            .map(this::addLeading0ToCrc, this::dropPL00Indicator) 
            .toSeq() 
            .map(Object::toString) 
            .collect(joining());
}

Dużo lepiej 🙂 Choć znając scalowców to by zapisali jako operator w rodzaju 8======|)—

Logger to też jest biznes

Typowa scenka gdzieś na mieście. Stoi dwóch policjantów w bramie i coś tam bazgrze w notatnikach. Co oni robią? Oczywiście notują informacje, które są istotne z ich punktu widzenia dla przebiegu patrolu. Inaczej mówiąc, logują informacje istotne z punktu widzenia biznesu. I właśnie o logowaniu dziś będzie.

Rodzaje logowania

Z punktu widzenia architekta możemy wydzielić trzy rodzaje logów. Każdy z nich odpowiedzialny jest za inny aspekt działania aplikacji, ale jeden zapis do logu może być istotny dla wielu aspektów.

Logi biznesowe

W tej grupie mamy zebrane logowanie wszystkich informacji istotnych z punktu widzenia biznesu. Mogą to byś zarówno informacje na potrzeby audytu – kto, gdzie, kiedy podjął daną akcję, przebieg procesu biznesowego, jak i informacje na potrzeby analizy pod kątem rynku np. systemu rekomendacji. W tej kategorii istotne jest określenie wspólnie z biznesem co i kiedy należy logować.

Ten log może, choć nie musi, być obiektem domenowym. Tak! Obiektem domenowym, ponieważ ma znaczenie biznesowe. Dlatego też należy przyznać temu logowi odpowiedni status i uwzględnić go na poszczególnych etapach projektowania. Ciekawym doświadczeniem może być pociśnięcie ekspertów domenowych w czasie stormingu, by określili się, co warto zapisać. Jest tu jednak zaszyta pewnego rodzaju pułapka projektowa. W wielu organizacjach, w których IT jest narzędziem, a nie istotą biznesu, istnieje silne przywiązanie do bazy danych jako miejsca składowania informacji istotnych dla biznesu. Bardzo ciężko jest w takim przypadku wytłumaczyć, że np. plik tekstowy, albo baza dokumentowa są wystarczającym rozwiązaniem.

Logi operacyjne

Logi operacyjne są najważniejsze z punktu widzenia programistów. Pozwalają na analizę stanu aplikacji, przyczyn awarii, dostarczają danych dla narzędzi monitorujących. Najczęściej logi te są wykorzystywane na etapie testów manualnych, kiedy testerzy wykorzystują je jako załączniki do zgłoszeń. Tu będą wszystkie zrzuty stacktrace, parametry wywołania metod czy czasy ich wykonania.

Oczywiście różne informacje mają różne priorytety, zatem projektując taki log, należy wziąć to pod uwagę. W efekcie będziemy tu intensywnie wykorzystywać różne poziomy logowania. Co więcej, jeżeli nie wykorzystujemy żadnego mechanizmy składowania i analizy logów w rodzaju Grayloga czy Logstasha, to koniecznie należy dopieścić konfigurację, tak by informacje o różnych aspektach trafiały do różnych plików (tak, wiem, nagle niskopoziomowo się zrobiło, ale to pewne uproszczenie myślowe). Dzięki temu analiza będzie prostsza.

Logi deweloperskie

Czyli taki bardziej rozbudowany dupadebug. Dzięki tym logom możemy śledzić przebieg programu bez konieczności korzystania z debuggera. Nie są one jakoś często stosowane, ale niekiedy są dość ważne. Tu też zbieramy informacje, które posłużą do dalszej analizy programu. Popularnym zastosowaniem jest rozrzucenie tego typu zapisów w celu odtworzenia procesów dla kodu zastanego i starego. Pozwala to na szybkie określenie warunków wykonania i ścieżek w kodzie na podstawie parametrów wywołania.

Błędy

Gdy mówimy o błędach popełnianych przy logowaniu, to zazwyczaj wskazywane są:

  • Synchronizacja zapisu do logu
  • Łączenie Stringów
  • Podwójne sprawdzanie poziomu logowania (konstrukcja w rodzaju if(log.isDebugEnable())

Rzecz w tym, że poza pierwszym z ww. problemów nie są, to poważne rzeczy. Znacznie częściej mamy do czynienia z zupełnie innymi wrzodami.

Nieprawidłowy poziom logownia

Chyba najpopularniejszy błąd. Pisząc kod, nie zastanawiamy się nad tym, co i jak będzie logowane. Wiadomo błędy na error albo podobny, reszta pomiędzy info, a debug. Błędy do olania na warrning. Nie ważne, czy informacja jest ważna dla biznesu, operatorów czy chcemy mieć jakiś marker w logu. W ten sposób do logu trafiają losowe informacje, które ciężko analizować i jeszcze ciężej coś z nich wywnioskować.

Jak sobie z tym radzić? Myśleć co się pisze oraz, oczywiście, poświęcić trochę czasu na określenie zasad logowania.

Różne systemy logowania

Drugim popularnym błędem jest mieszanie systemów logowania. Niestety wykorzystywane przez nas narzędzia nie ułatwiają nam życia. Część korzysta z log4j, część z apache logging, a jeszcze inne obudowują to w slf4j/slf4j2. Jeżeli dołożymy do tego zawsze dostępny Logger z JRE oraz stary dobry System.out.println, to okaże się, że w ramach jednej aplikacji mamy kilka niespójnych systemów logowania.

Problemy

Drugi z ww. błędów jest objawem poważnego wrzodu na ciele społeczności JVMowej. Język ma co prawda logger na pokładzie, ale pojawił się on dość późno oraz jest stosunkowo prymitywny. W efekcie mamy dużo różnych bibliotek, które mają niespójne API. To oczywiście powoduje, że wraz ze wzrostem złożoności projektu i przyrostem liczby zależności pojawią się coraz to nowsze mechanizmy logowania. Co prawda sfl4j miał rozwiązać ten problem, ale no cóż…

Drugim problemem związanym z niespójnością jest ograniczona możliwość dostosowania loggerów do potrzeb naszej aplikacji. Wybierając jakieś rozwiązanie, musimy pogodzić się z jego ograniczeniami. W ten sposób nasz kod zostaje w pewnym sensie przywiązany do biblioteki. W dodatku tracimy swobodę definiowania, jak logowanie ma działać. Utworzenie loggerów na potrzeby różnych aspektów jest trudne, a w dodatku zmiana biblioteki staje się praktycznie niemożliwa. Słabo. Bardzo słabo.

Podsumowanie

W kolejnym wpisie zabawimy się już z kodem. Mam nadzieję, że to krótkie wprowadzenie nie zabiło waszej ciekawości.

Confitura 2018 – tym razem na zimno

Pierwszy raz od dawna nie mam pomysłu jak zrelacjonować konferencję. Po prostu nie mam. Spróbujmy więc może tak.

Miejsce

Czyli hale Expo XXI na Prądzyńskiego są bardzo OK. Zastanawiam się tylko, czy osoby spoza Warszawy były wstanie ogarnąć, gdzie to jest. Szczególnie idąc od strony dworca zachodniego. Widziałem tam grupy ludzi, którzy po przyjechaniu pociągiem chyba nie do końca wiedzieli gdzie iść. Niech se kupią mapę 😉

Dużo miejsca, zarówno dla wystawców, jak i uczestników. Przestrzeń, w której nie było widać tych prawie dwóch tysięcy ludzi.

Miesce II

Kręgielnia w której była Spoina zawsze na propsie 🙂 Choć miałem wrażenie, że mało ludzi było.

Wystawcy i gadżety

Od kogoś usłyszałem, że Confitura to taka konferencja gdzie są najfajniejsze gadżety. I ta osoba miała rację. Wystawców sporo, gadżety i nagrody najróżniejsze. Fajnie, że było miejsce. I co najważniejsze, wszyscy się postarali, by było fajnie!

Papu

Papuśne, poza kawą, która tradycyjnie była niepijalna. Sytuację ratował UBS, bo mieli ekspres na stoisku. Klasycznie też pora obiadu, to pora kolejek. Choć nadal zastanawia mnie pytanie o rodzaj posiłku w ankiecie biletowej… W przerwach było inne papu i trzeba było się pilnować, by nie nawpieprzać się ciastków.

Ludzie

Wspaniali.

Prelekcje

Różnie.

Antywzorce projektowe albo jak rozwalić dowolny projekt w 4-6 tygodni – Paweł Lewtak

Taka prezentacja, w której czegoś mi zabrakło. Jakiejś katastrofy. Poza tym spoko.

Micro-monolith anti-pattern – Tomasz Fijałkowski

Całkiem fajnie opowiedziane o tym problemie. Swoją drogą widać już, że µSerwisy zaczynają być passe. Ludzie zaczynają dostrzegać ich ograniczenia.

Kariera sfrustrowanego developera – Mateusz Parada

Najmądrzejsza prezentacja nietechniczna całej tegorocznej Confitury. Może trochę przejaskrawiona, ale bardzo sensowna. Moim zdaniem mocny kandydat do nagrody na najlepszą prezentację C’18.

Blockchain: more than bitcoin! – Andrzej Grzesik

Technicznie bajka. Rzeczowo, precyzyjnie i logicznie. W końcu zaczynam rozumieć, jak to działa w bebechach.

Programistów z biznesem przypadki trudne – Jakub Kubrynski, Sławomir Sobótka, Michal Michaluk

Super prezentacja, której głównym składnikiem był show. Merytorycznie bardzo fajny temat, ale nie odkrywczy. Może to już moje doświadczenie „niweluje” wartość, ale dla młodszych stażem na pewno ważna lekcja.

Co do poprawy?

Dźwięk niósł się z jednej sali, na drugą. Jako prelegentowi mi to nie przeszkadzało, bo nie słyszałem tego (siebie też nie, odsłuchy by się przydały), ale jako słuchaczowi już tak.
Druga rzecz to streaming. Był, ale nie mogę nigdzie znaleźć nagrań streamingowych. Trzeba było je zostawić na tydzień w necie, by można było nadrobić.

Podsumowanie

Na zakończenie chciałbym przeprosić wszystkich tych, z którymi przybiłem piątkę, ale nie znalazłem czasu, by zamienić kilka słów. Może przy kolejnej okazji uda się chwilę porozmawiać o życiu i GC oraz o tym, jak małe forki się sprawują.
Dziękuję organizatorom, wolontariuszom i uczestnikom. Było super.

JLupin Next Server – dodajemy bazę danych, czyli gdzie jest classpath

Dziś będziemy dodawać bazę danych, a właściwie „bazę danych” do naszego projektu. Jest to całkiem dobry moment by przyjrzeć się skąd JLNS zaczyna wyliczać classpath.

Baza danych

Niekoniecznie relacyjna. Wielokrotnie spotkałem się z niezrozumieniem ze strony poganiaczy w dziedzinie składowania danych. Architekci przy wsparciu managerów zazwyczaj forsują rozwiązania relacyjne. Relacyjna baza danych zapewnia ich zdaniem trwałość danych (pewność ich zapisania) oraz spójność (zapis jest transakcyjny). Rzecz w tym, że bardzo szybko baza danych staje się odbiciem modelu domeny i to 1:1. Do czego to prowadzi?

Po pierwsze do ograniczenia elastyczności modelu domenowego. Jeżeli zachodzi potrzeba większej zmiany w modelu domenowym, np. przeorganizowania encji z powodów biznesowych, to jesteśmy w dupie. Nagle okazuje się, że trzeba przygotować różne dzikie migracje w strukturze tabeli. To pociąga za sobą konieczność przepisania czy to SQLi, czy to mapowiań ORMowych, a na koniec i tak się okazuje, że jest jeszcze cała masa procedur, triggerów i jobów bazodanowych, które nagle tracą sens. A my tylko chcieliśmy wynieść jeden z VO jakiegoś zagłębienia do głównej encji.

Po drugie wszelkie problemy z wydajnością, które wynikają z niedostosowania modelu relacyjnego do pracy w świecie obiektowym, zaczyna się rozwiązywać poprzez pisanie procedur po stronie bazy danych. W efekcie część logiki jest realizowana w bazie danych, część po stronie aplikacji, a część na poziomie procesowym. Oczywiście utrzymanie takiego potwora jest kosztowne i trudne, ale za hajs właściciela/inwestora baluj.

Po trzecie pojawiają się problemy ze składowaniem nietypowych, nierelacyjnych, struktur danych. JSON czy XML zapisany w bazie danych? Why not! Struktury typu słowniki z referencjami i hierarchią jako osobna instancja bazy danych? Dawaj Janusz, będzie dobrze! Co ciekawe na końcu jak całość działa w żółwim tempie, to opierdala się programistów.

Znacznie ciekawiej robi się, gdy zaczynamy wchodzić w środowiska rozproszone. Jak zapewnić transakcyjność w momencie, gdy musimy podzielić nasz system na kilka aplikacji, albo musimy całość rozrzucić w klastrze? Jeszcze zabawniejsze są jednak miny naszych mistrzów projektowania z kwadratów i beczek, gdy mówimy im, że jednak te mikroserwisy, to nie ruszą z jednym oraclem i trzeba zapewnić kilkanaście instancji. Wtedy okazuje się, że hajs i architektura mają pewne powiązania.

A można inaczej…

DataLayer

Naszą przygodę z dodawaniem bazy danych do systemu rozpoczniemy od dodania małej zależności do naszego pom-a w customer-storage/implementation:

Listing 1. Airomem na ratunek

pl.setblackairomem-core1.1.1

Na początek wystarczy, że dane będą przechowywane w pamięci. Serio, serio. RAM jest tani, chyba że masz serwery IBMa, to wtedy jest drogi no ale… Następnie należy, to wszystko jakoś pokleić w kodzie.

Generowanie DAO

Plugin JLNS pozwala na wygenerowanie klas reprezentujących DAO, domyślna zawartość interfejsu jest pusta, a implementacja jest oznaczona adnotacją @Repository(value = „COŚTAMDAO”). Po dorzuceniu kilku rzeczy interfejs CustomerDAO wygląda w nastepujący sposób:

Listing 2. Interfejs CustomerDAO

public interface CustomerDAO {

	Option<Customer> getById(UUID customerId);

	Set<Customer> all();

	UUID add(Customer newCustomer);
}

Zwróćcie uwagę na klasę Customer, która będzie reprezentować encję w rozumieniu DDD. Tu będą składowane wszystkie informacje o użytkowniku, które przechowujemy na poziomie jego składowania. W Warstwie usług biznesowych też pojawi się taka klasa, ale będzie miała inne znaczenie.

Następnie musimy dodać prostą implementację, która jest też ładnym przykładem do pewnego wpisu.

Listing 3. Klasa CustomerDAOInMemory

class CustomerDAOInMemory implements CustomerDAO, Serializable {

	private Map storage = new ConcurrentHashMap();

	@Override
	public Option getById(UUID customerId) {
		return Option.of(storage.get(customerId));
	}

	@Override
	public Set all() {
		return storage.values()
				.stream()
				.collect(HashSet.collector());
	}

	@Override
	public UUID add(Customer newCustomer) {
		storage.put(newCustomer.getCustomerId(), newCustomer);
		return newCustomer.getCustomerId();
	}
}

Na koniec całość owiniemy w springowy bean, który będzie nam ogarniał logikę związaną z biblioteką airomem:

Listing 4. Klasa CustomerDAOAiromem

@Repository(value = "customerDAO")
public class CustomerDAOAiromem implements CustomerDAO {

	private Persistent controller;

	@Autowired
	public CustomerDAOAiromem(
			@Qualifier("storagePath") Path path,
			@Qualifier("useRoyalFoodTester") boolean useRoyalFoodTester) {
		Path resolve = Paths.get(path.toAbsolutePath().toString(), "customer");
		this.controller = Persistent.loadOptional(resolve,
				CustomerDAOInMemory::new, useRoyalFoodTester);
	}

	@Override
	public Option getById(UUID customerId) {
		return controller.query(c -> c.getById(customerId));
	}

	@Override
	public Set all() {
		return controller.query(CustomerDAOInMemory::all);
	}

	@Override
	public UUID add(Customer newCustomer) {
		return controller.executeAndQuery(c -> c.add(newCustomer));
	}

	@PreDestroy
	void close() {
		controller.close();
	}
}

Jak to uruchomimy, to okaże się, że chyba zgubiliśmy gdzieś naszą bazę danych…

Jak JLNS liczy classpath

JLNS w trakcie uruchamiania usługi podaje classpath. Należy do niego m.in. katalog ./application/customer-storage/, ale korzeniem jest katalog ./start. Trochę to nieintuicyjne, ale jak już o tym się pamięta, to można sobie policzyć classpath. I tak oto możemy zapisywać użytkowników w bazie danych. Jeżeli chcemy zmienić bazę na przykład oracla, to wystarczy podmienić implementację CustomerDAO. W przypadku ORMów trzeba by dodać jeszcze mapowania, ale po pierwsze, jeżeli chcemy to zrobić bez zmian w klasie Customer, to możemy wykorzystać XMLa, albo stworzyć klasę CustomerEntity, która będzie naszym opisem struktury bazodanowej.

Na dziś starczy.

JLupin Next Server – komunikacja między usługami – podstawy

Na początek link do repozytorium GH gdzie leży sobie kod.

Dziś zajmiemy się komunikacją pomiędzy usługami oraz podstawami testowania z wykorzystaniem JLNS.

Konfigurowanie zależności

Jak wspomniałem w poprzednim wpisie, usługi natywne są wewnętrznie podzielone na interfejsy i implementacje. Wykorzystując mavena, możemy uzależniać się od interfejsów, bez konieczności dociągania implementacji. Ma to dwie zalety. Po pierwsze ogranicza ilość kodu jaki, zostanie dodany do naszego finalnego artefaktu. Po drugie pozwala na kulturalną współpracę pomiędzy zespołami. Pośrednikiem będzie tu repozytorium mavena. Miodzio.

Zależności gateway

Gateway udostępnia nam wybrane API. Na tym poziomie musimy zatem mieć zależności do procesów biznesowych. Dlatego w ./AccessLayer/gateway-access/implementation/pom.xml musimy dodać zależności do serwisów z warstwy biznesowej:

Listing 1. pom.xml w gateway

pl.koziolekweb.jlns.banksteraccount-business-logic-interfaces1.0-SNAPSHOTpl.koziolekweb.jlns.bankstercustomer-business-logic-interfaces1.0-SNAPSHOT
...

Tu objawia się kolejna mała niedoróbka domyślnej struktury projektu JLupinowego. W takiej strukturze ciężko w jakiś sensowny sposób zarządzać wersjami usług. Z jednej strony można wszystko po prostu zapisać jako zmienną i podstawiać wszędzie gdzie trzeba. Rzecz w tym, że tych zmiennych może być dużo. Drugim sposobem może być wykorzystanie parametru ${parent.version}, ale zadziała to tylko w przypadku natywnych mikroserwisów. Mikroserwisy servletowe jako „rodzica” mają ustawionego SpringBoota w wersji 1.5, co niestety nie jest parametryzowane z poziomu kreatora i jest kolejną małą niedoróbką. Przy czym problemu wersjonowania usług w sensowny sposób to jeszcze nikt dobrze nie wymyślił, a JLNS tak naprawdę deleguje problem do użytkownika.

A co z dostępem do usług ogarniających dane? Moim zdaniem, to ważne, nie należy ich linkować bezpośrednio do gatewaya. Wynika to ze sposobu, w jaki publikowane są usługi i może spowodować, że ujawnimy za dużo. Nawet jeżeli na poziomie warstwy biznesowej serwis pośredniczący będzie tępą rurą bez logiki, to nadal daje nam to separację modelu danych na niskim poziomie od eksponowanego modelu.

Zależności w serwisach biznesowych

Tu zasada jest ta sama. Z tą małą różnicą, że zależność dodajemy tylko do pom-a w module z implementacją np.

Listing 2. zależność w ./BusinessLogicLayer/customer-business-logic/implementation/pom.xml

pl.koziolekweb.jlns.bankstercustomer-storage-data-interfaces1.0-SNAPSHOT
...

I tak oto mamy już ogarniętą konfigurację na poziomie zależności. Czas na zabawę w kodzie.

Jak korzystać z usług?

Proces konfiguracji zależności już za nami. Teraz mamy dostęp do interfejsów, a JLNS zrobi z nich interfejsy zdalne. Tyle tylko, że trzeba mu trochę w tym pomóc. Zrobimy to na przykładzie usługi customer-storage. Wystawimy z niej prosty interfejs StorageCustomerBasicInformationService, który wygląda następująco:

Listing 3. Interfejs StorageCustomerBasicInformationService

public interface StorageCustomerBasicInformationService {

	Set<BasicCustomerInformation> all();

	Option<BasicCustomerInformation> byId(UUID customerId);
}

Klasa CustomerBasicInformationService znajduje się w module common-pojo, a to dlatego, że jest to minimalny zbiór informacji o kliencie. Coś w rodzaju VO klienta. Będziemy z tej klasy korzystać w wielu miejscach i można przyjąć, że na poziomie systemu to element naszego języka domenowego (Podstawowe Dane Klienta, czyli PaDaKa). Sam interfejs znajduje się w module interfaces.

Kolejnym krokiem jest implementacja tego interfejsu w module implementation. Oczywiście nie mam zamiaru tego pisać z palca. W InteliJ rozwijamy moduł implementation i odnajdujemy pakiet COŚTAM.service.impl, naciskami alt+insert i:

Następnie podajemy nazwę serwisu bez przyrostka Service, ten zostanie dodany automatycznie.

W efekcie zostaną wygenerowane dwa pliki (interfejs i klasa):

//offtop
Po drodze zrobiłem refaktoryzację nazw, by było łatwiej, więc nie do końca trzyma się to to z obrazkami kupy.
//koniec offtopa

Upublicznienie usługi

Teraz należy jeszcze usługę upublicznić. Generator nie tylko stworzył pliki, ale też zmodyfikował klasę CustomerStorageSpringConfiguration:

Listing 4. Klasa CustomerStorageSpringConfiguration

@Configuration
@ComponentScan("pl.koziolekweb.jlns.bankster")
public class CustomerStorageSpringConfiguration {
	@Bean
	public JLupinDelegator getJLupinDelegator() {
		return JLupinClientUtil.generateInnerMicroserviceLoadBalancerDelegator(PortType.JLRMC);
	}

	@Bean(name = "jLupinRegularExpressionToRemotelyEnabled")
	public List getRemotelyBeanList() {
		List<String> list = new ArrayList<>();
		list.add("storageCustomerBasicInformationService");
		return list;
	}
}

JLupin na podstawie listy nazwanej jLupinRegularExpressionToRemotelyEnabled odszuka i opublikuje wybrane serwisy. Tu bardzo ważnym elementem jest nadanie naszym springowym beanom nazw. Co prawda istnieje konwencja nazewnicza, dzięki której możemy wykorzystać domyślne nazwy, ale może być ona źródłem różnych pomyłek. Zatem nasza implementacja wygląda w następująco:

Listing 5. Interfejs StorageCustomerBasicInformationServiceImpl

@Service(value = "storageCustomerBasicInformationService")
public class StorageCustomerBasicInformationServiceImpl implements StorageCustomerBasicInformationService {
	@Override
	public Set<BasicCustomerInformation> all() {
		return HashSet.empty();
	}

	@Override
	public Option<BasicCustomerInformation> byId(UUID customerId) {
		return Option.none();
	}
}

Używam tu Vavr, bo chcę mieć serializowane kontenery. Dlaczego? O tym za chwilę. Przejdźmy do konfiguracji po stronie klienta.

Konfiguracja kliencka

Klientem naszej usługi jest serwis customer-business-logic. Dlatego też udamy się teraz do klasy konfiguracyjnej CustomerSpringConfiguration:

Listing 6. Klasa CustomerSpringConfiguration

@Configuration
@ComponentScan("pl.koziolekweb.jlns.bankster")
public class CustomerSpringConfiguration {
	@Bean
	public JLupinDelegator getJLupinDelegator() {
		return JLupinClientUtil.generateInnerMicroserviceLoadBalancerDelegator(PortType.JLRMC);
	}

	@Bean(name = StorageCustomerBasicInformationService.name)
	public CustomerBasicInformationService getStorageCustomerBasicInformationService() {
	     return JLupinClientUtil.generateRemote(getJLupinDelegator(),
			     "customer-storage",
			     StorageCustomerBasicInformationService.name,
			     StorageCustomerBasicInformationService.class);
	}

	@Bean(name = "jLupinRegularExpressionToRemotelyEnabled")
	public List getRemotelyBeanList() {
		List<String> list = new ArrayList<>();
		return list;
	}
}

Uwaga! Po drodze wyrzuciłem jeszcze nazwę serwisu, którą używamy w nazwie beana na poziom interfejsu. Taka mała rzecz, a cieszy i daje trochę większą kontrolę nad tym, co się dzieje.

Metoda getStorageCustomerBasicInformationService produkuje nam proxy, które opakowuje JLupinowy mechanizm wywołań zdalnych. W naszym kodzie możemy później użyć takiego wywołania tak, jak byśmy wywoływali zwykłą, lokalną, metodę. Bardziej doświadczeni czytelnicy rozpoznają tu zapewne mechanizm RMI obecny w EJB. Podstawowa różnica pomiędzy tymi rozwiązaniami leży w sposobie ich implementacji. To, czy dany interfejs jest dostępny zdalnie, czy też nie jest definiowane na poziomie konfiguracji, a nie interfejsu. Nie ma ty adnotacji Local czy Remote, bo nie są do niczego potrzebne. Co prawda można by dodać adnotację, która by wyszukiwała za nas implementacje, ale to szczegół. Zresztą i tak musimy przepchnąć nasze obiekty dalej do gateway-a. Powtórzymy zatem proces tworzenia serwisu na poziomie warstwy usług biznesowych i skonfigurujmy usługę tak, by można było jej używać z zewnątrz. Nasza implementacja będzie wyglądać następująco:

Listing 7. Klasa CustomerSpringConfiguration

@JLupinService
@Service(value = CustomerBasicInformationService.name)
public class CustomerBasicInformationServiceImpl implements CustomerBasicInformationService {

	private final StorageCustomerBasicInformationService delegate;

	@Autowired
	public CustomerBasicInformationServiceImpl(StorageCustomerBasicInformationService delegate) {
		this.delegate = delegate;
	}

	@Override
	public Set<BasicCustomerInformation> all() {
		return delegate.all();
	}

	@Override
	public Option<BasicCustomerInformation> byId(UUID customerId) {
		return delegate.byId(customerId);
	}
}

Testy integracyjne

Na tym etapie wystarczyło by zrobić mvn clean install i po problemie. Zanim jednak tak zrobimy musimy jeszcze trochę rzeczy związanych z testowaniem. W zasadzie testowanie integracyjne w JLNS nie różni się zasadniczo od zwykłych testów integracyjnych. W module integration-test dodajmy sobie prosty test naszego CustomerBasicInformationService

Listing 8. Klasa CustomerBasicInformationServiceITest

public class CustomerBasicInformationServiceITest extends BaseTest {

	 @Test
	 public void exampleTest() {
	    CustomerBasicInformationService service = JLupinClientUtil.generateRemote(getJLupinDelegator(),
			    "customer",
			    CustomerBasicInformationService.name,
			    CustomerBasicInformationService.class);
		 Assertions.assertThat(service.all()).isEmpty();
	 }
}

Klasa BaseTest przygotowuje nam prostego klienta, który będzie komunikować się z serwerem na localhost.

Uruchomienie

Pozostało nam to wszystko uruchomić. Wystarczy zatem uruchomić serwer i następnie odpalić mvn clean install. Poniżej nagrałem ten proces (koniecznie full screen):

Górna połowa to oczywiście maven. Dolna to podgląd konsoli JLNS. Ładnie widać jak wstają kolejne serwisy.

Podsumowanie

Podstawowa komunikacja pomiędzy serwisami w ramach JLNS jest bardzo prosta, a dzięki kilku „sztuczkom ze Springiem” można to ładnie ogarnąć. Fajne, prawda?

// edit: w Listingach 6 i 8 zabrakło jednej linijki. Nazwy usługi do której się odwołujemy.

JLupin Next Server – pierwsze kroki, pierwsze usługi

W poprzednim wpisie było trochę dupereli o mikroserwisach. Dlaczego są nie do końca OK oraz jak JLupin może pomóc nam ogarnąć tę kuwetę. Dziś będziemy bawić się już w programowanie i na przykładach zademonstruję co i jak.

Co piszemy?

Jak już 11 lat bloga prowadzę, to zawsze przewijał się na nim temat bankowości. Banki duże, banki małe, systemy rozliczeniowe, windykacyjne, transakcyjne, raportowanie i cała masa innych niegodziwych elementów tego szatańskiego tworu zwanego systemem bankowym. Dlatego też bez zaskoczenia. Napiszemy bank. Dokładnie napiszemy sobie fragment związany z kontami, klientami i jakieś proste raportowanie. Docelowo będziemy mieli tu kilka aspektów, które ładnie będzie można przenieść na poszczególne usługi:

  • Klienci i konta – informacje o klientach i ich kontach, czyli takie typowe CRUD-owe usługi.
  • Operacje finansowe – tu będzie się działo mięsko związane z przelewaniem kasy.
  • Raportowanie – zazwyczaj w takim miejscu jest jakiś system raportowo-analityczny, my zrobimy sobie wydmuszkę.
  • Parametryzacja – pewne elementy banku muszą być pobierane z parametrów biznesowych.
  • API Gateway – końcówka dla klientów.

By jeszcze sobie utrudnić życie, a jednocześnie móc zademonstrować kilka elementów JLNS, wprowadzimy funkcjonalność „partnerów”, czyli coś, co na co dzień znamy jako kredyty sprzedawane w sklepach z AGD, albo karty kredytowe marketów.

// offtop

Od wczoraj żyjemy z RODO, a to oznacza, że takie produkty jak np. karta kredytowa „Stonka i Ty” muszą mieć znacznie ciekawszą konstrukcję. Jak będziecie brać kredyt w markecie, to wczytajcie się w regulaminy, bo może okazać się, że kredytodawcą jest jakiś niezbyt ogarnięty bank.

// koniec offtopa

Oczywiście będzie dużo uproszczeń (albo i nie), ale na początek zajmiemy się czymś zupełnie trywialnym Przygotujemy projekt w IntelliJ. Jest to o tyle proste, że JLNS udostępnia wtyczkę do IDE. Obrazkowo będzie.

Projekt Bankster

Na początku oczywiście menu „Create new Project” i następnie wybieramy projekt JLupinowy

Kolejnym krokiem jest określenie wersji grupy mavenowej oraz miejsca, w którym zapisaliśmy zipa zawierającego serwer. Osobiście uważam, że osadzanie serwera w ramach projektu jest po prostu wygodne. Czasami trzeba coś pogrzebać w konfiguracji czy rzucić okiem na logi.

Następnie stworzymy sobie projekt, który będzie ogarniał API Gateway:

Do tego dodajmy projekty dla klienta i rachunków oraz ich „tło” do składowania danych:

Na koniec nadajemy całości nazwę bankster i gotowe:

Na obecnym etapie otworzy się okno IDE, w którym mamy podgląd do naszej architektury:

Podgląd co prawda nie jest najlepszy, bo nie pozwala nam na tworzenie zależności za pomocą drag’n’drop, ale lepsze to niż nic. Szczególnie że przy większych projektach i tak szybciej będzie dodać zależność w pom.xml niż kombinować z myszką.

Struktura projektu

Przyjrzyjmy się teraz strukturze projektu. Pierwszą rzeczą, która rzuca się w oczy, jest organizacja modułów. Poszczególne projekty są modułami głównego projektu. I to jest nie do końca OK. Taki układ wymusza na nas pracę z projektem jak z monolitem. W dodatku trudno będzie zarządzać takim projektem, tak by poszczególne usługi miały swoje repozytoria. Moim zdaniem jest to bardzo duży błąd ze strony twórców. Z drugiej strony pozwala, to na zarządzanie całością z jednego miejsca. Trochę innym podejściem jest stworzenie projektu typu bom, w którym upakujemy konfigurację zależności. Przyjmijmy jednak, że ta domyślna konfiguracja ma jakiś głębszy sens. Przyjrzyjmy się, jak wyglądają poszczególne projekty.

Zacznijmy od gatewaya. Projekt ten umieszczony jest w katalogu ./AccessLayer/gateway-access/. Znajdziemy tam dwa katalogi. Pierwszy to additional-files, w który znajdują się dwa pliki:

  • log4j2.xml – zawiera konfigurację log4j2, która zostanie wykorzystana przez techniczne elementy usługi. Jest to też domyślny log usługi.
  • servlet_configuration.yml – tu znajdziemy konfigurację usługi, która obejmuje kanały komunikacyjne, pamięć, bufory, ale też na przykład zasady rotowania logów.

Pozostałe podprojekty mają taką samą strukturę. Przy czym projekty z warstwą biznesową znajdują się w katalogu ./BusinessLogicLaywer/{account,customer}-businesslogic/, a projekty związane z dostępem do danych w katalogu ./BusinessLogicLaywer/{account,customer}-storage-data/. Sama struktura poszczególnych projektów jest następująca:

  • ./additional-files/ – to samo co wyżej, czyli konfiguracja, ale zamiast servlet_configuration.yml, mamy configuration.yml.
  • ./interfaces/ – w tym projekcie definiujemy interfejsy naszej usługi. Co ważne inne usługi korzystające z naszego serwisu będą zależne od tego właśnie projektu.
  • ./implementation/ – implementacja naszych usług. Tym się nie musimy z nikim dzielić.

Poza wyżej wymienionymi elementami mamy jeszcze:

  • common-pojo – w tym projekcie leżą sobie klasy, które reprezentują pewne podstawowe struktury danych w naszym systemie. W praktyce to kubeł na różne VO.
  • common-utils – skrzynka na narzędzia. Tu umieść klasę StringUtils. Na pewno będziesz jej potrzebować.
  • integration-test – w tym miejscu leżą testy integracyjne, które można odpalić w procesie CI/CD.

Rodzaje mikroserwisówserwisów

Taka organizacja poszczególnych projektów jest wynikiem wyróżnienia dwóch rodzajów mikroserwisów.

Mikroserwisy servletowe

Pod tą nazwą kryją się mikoserwisy, które są aplikacjami webowymi. Co do zasady jest to spring boot. Mikroserwisy te wystawiają API na potrzeby naszych klientów. Na tym poziomie mamy dostęp do naszej aplikacji za pośrednictwem HTTP(S) i możemy pisać aplikacje zgodne ze specyfikacją servletową. Z drugiej strony usługi te są pozbawione wbudowanych kolejek, które pozwalają na tworzenie reaktywnych rozwiązań. Nie jest to nic dziwnego, bo tu nie potrzebujemy tego typu magii.

Mikroserwisy natywne

Czyli aplikacje (jary), które zawierają logikę biznesową, zarządzają danymi, komunikują się z innymi, zewnętrznymi usługami. Tych usług nie wystawiamy na zewnątrz bezpośrednio. Jednocześnie stanowią one największą część naszej aplikacji.

Podsumowanie

Praca z JLNS w zakresie tworzenia projektów jest bardzo prosta. Z drugiej strony niektóre pomysły twórców uważam za swoistą pomyłkę np. super-projekt mavenowy. Warto też zwrócić uwagę na jeszcze jeden aspekt. JLNS bardzo intensywnie wykorzystuje Spring Framework, ale jeżeli chcemy pisać bez wsparcia ze strony Springa, to nadal możemy to robić. W kolejnej części zaczniemy wiązać usługi między sobą i zaimplementujemy jakiś prosty przypadek użycia.

JLupin Next Server – wprowadzenie i instalacja

Tradycją w Polsce jest wykorzystanie urlopu/chorobowego do prowadzenia remontów. Jako że siedzę na L4, to jest to najlepsza okazja, by zadbać o blogaska. Przy okazji przedstawić wam pewną ciekawą technologię.

Mikroserwisy robione bez sensu

W epoce mikroserwisów coraz częściej można spotkać aplikacje, które są tak naprawdę „mikroserwisami”, bo nie posiadają cech wymaganych dla tej architektury. Z moich obserwacji wynika, że są dwa główne źródła takiego stanu rzeczy.

  • Źródło marketingowe – zazwyczaj typowe BDD (Buzzword Driven Development). Skoro wszyscy robią mikroserwisy i to się sprzedaje, to my też. Jest to bardzo stara przypadłość.
  • Źródło biznesowe – dokładnie, bardzo prosta domena biznesowa. Jeżeli mamy dwa przypadki użycia na krzyż, albo niewielką ilość encji, a jednocześnie potrzebujemy pewnych cech, które dają mikroserwisy np. dynamicznej skalowalności, czy ciągłości działania.

Przy czym mam świadomość, że jak w przypadku wszelkich prób opisania świata przez pryzmat własnych doświadczeń, będzie to opis niepełny.

Pierwszy punkt jest oczywisty i co kilka lat przewija się w takiej czy innej formie. Znacznie ciekawszy jest punkt drugi. Wybór architektury mikroserwisowej wynika tu z braku znajomości innych rozwiązań np. mod_cluster (który zazwyczaj wystarcza do zapewnienia dostępności i skalowalności) czy wiary, że „mikroserwisy to same to ogarną”. Jednocześnie ignoruje się dwie cechy, które powinien spełniać projekt, by w ogóle podjąć próbę użycia tego rodzaju architektury:

  • Niezależność zespołów
  • Niezależność technologii

W obu przypadkach z naciskiem na liczbę mnogą. Pozatechniczną cechą architektury mikroserwisowej jest możliwość budowania aplikacji w oparciu o rozproszone zespoły, które pracują niezależnie od siebie. Zespoły te mogą używać różnych, całkowicie niekompatybilnych, technologii. Na przykład tworząc aplikację do e-learningu, użyję innych technologii do obsługi streamingu video, a innych do ogarnięcia płatności. Pierwsze zadanie opędzę np. Elixirem, a to drugie napiszę w Javie (w zasadzie wykorzystam jakiegoś gotowca). Całość oczywiście będzie gadać ze sobą po jakimś uniwersalnym protokole.
Z drugiej strony mając jeden niewielki zespół, do stworzenia takiej aplikacji nie mogę wymagać, że będzie on ogarniać kilku różnych technologii. Muszę wybrać takie rozwiązanie architektoniczne, które pozwoli mi na jak najlepszą wydajność pracy. Być może okaże się, że odpowiednio napisany monolit będzie tu lepszy. Co ciekawe (też z własnych obserwacji), nawet jeżeli mamy architekturę mikroserwisową, która poniekąd wymusza separację komponentów, to jeżeli mamy mały zespół, to i tak zacznie powstawać, albo jakieś dziwne spaghetti pomiędzy komponentami, albo zespół będzie kodował metodami Kopiego-Pasty wszędzie tam, gdzie kod się powtarza. Dokładnie w ten sam sposób jak ma to miejsce w przypadku monolitu.

UWAGA!

Dobrze napisany monolit wymaga świadomego zespołu, który potrafi wykorzystywać clean architecture oraz opiera się nieuprawnionym operacjom.

Inne problemy z mikroserwisami

Poza wyżej wymienionymi problemami, które wynikają z niedopasowania architektury do problemu, jest też jeszcze kilka innych, które zazwyczaj umykają przy wyborze architektury.

Koszty sprzętu

Na początku mało kto zdaje sobie sprawę ile może kosztować wdrożenie mikroserwisów. Cztery jednostki m4.large w amazonowym AWS kosztują obecnie 0,60$/h jednocześnie czterokrotnie większa jednostka m4.2xlarge (4x więcej rdzeni i pamięci RAM) kosztuje 1,44$/h. Często padające hasło „mikroserwisy można wdrażać na mniejszych i tańszych serwerach” może okazać się znacznie kosztowniejsze w porównaniu z wykorzystaniem monolitu na mocniejszej maszynie.

Wydajność

Bardzo ciekawy argument, bo często mikroserwisy przedstawia się jako remedium na problemy z wydajnością. A to nie tak. Oczywiście dzięki znacznie lepszej skalowalności można poprawić wydajność w pewnych rejonach np. można przyjąć więcej żądań. Z drugiej strony mogą pojawić się nowe problemy związane z tempem komunikacji. Wywołanie zdalne nie będzie tak szybkie jak wywołanie lokalne. Nawet jeżeli jest robione w obrębie pojedynczej maszyny. Po prostu komunikacja trwa.

Koszty technologii (i ludzi)

Wprowadzając architekturę mikroserwisową do naszego świata, musimy zapewnić odpowiednie zaplecze. Jak wcześniej wspomniałem, koszty sprzętowe mogą być znaczące, dlatego chcemy mieć kontrolę nad sprzętem. Tym samym wprowadzamy różnego rodzaju narzędzia, które pozwolą nam na atomizację komponentów i niezależne zarządzanie nimi. Poszczególne mikroserwisy pakujemy w kontenery dockerowe, te rozrzucamy na środowiska przy pomocy managera w rodzaju Kubernetes, na którym zmiany w boxach są zarządzane za pomocą Ansible, a same boxy są zwirtualizowane z użyciem Mesos, dzięki czemu możemy tworzyć „dziwne” konfiguracje sprzętowe (wirtualne). Do tego procesy deploymentu są zarządzane z poziomu Jenkinsa i jego pipelien-ów. BDD pełną gębą. A przecież, ktoś to musi ogarnąć.
Koszty technologii to nie tylko koszty licencji, ale przede wszystkim koszty ludzi, którzy będą je później utrzymywać. W dodatku nasi ludzie muszą mieć wiedzę z wielu dziedzin, które niekoniecznie idą w parze. Dla dużych organizacji zatrudnienie „gościa od buildów”, którego zadaniem będzie jedynie analizowanie zepsutych zadań jenkinsowych, pod kątem potencjalnych błędów w konfiguracji nie jest dużym kosztem. Dla małej firmy może okazać się to zabójcze. Zresztą tu pojawia się pojęcie DevOps, które oznacza metodykę, ale też pewien zestaw umiejętności i wiedzy. Te z natury muszą być bardzo rozległe. Obejmują znajomość narzędzi, technik programowania, zagadnień z zakresu testowania, bezpieczeństwa, ale też całą masę umiejętności miękkich potrzebnych do pracy z wieloma różnymi ludźmi począwszy od programistów, poprzez managerów, osoby ze wsparcia technicznego, a na klientach kończąc. Wierzę, że można to ogarnąć, ale nie wierzę, że ogarniesz to w przeciągu kilku tygodni czy nawet miesięcy. W dodatku jak je już opanujesz, to może okazać się, że po zmianie organizacji tylko wiedza techniczna (i ogólne umiejętności miękkie) będzie przydatna. Nowa firma, inne procesy, inne standardy.

Typowy system javowy

Spróbujmy popatrzeć na ten problem inaczej. Po pierwsze, jeżeli nasz system jest w całości napisany pod JVM-a, to możemy pokusić się o użycie natywnych dla JVM-a rozwiązań. Po drugie, jeżeli idziemy w mikroserwisy i potrafimy pociąć naszą domenę na mikroserwisy, to zazwyczaj będą one odpowiadać poszczególnym aplikacjom. Po trzecie potrafimy jako tako w CI/CD i umiemy zapewnić sobie jako taką stabilność, to zapewne efekt będzie następujący. Kilka aplikacji Spring Bootowych, które gadają za pomocą Http REST API, bo Spring ma zarówno klienta, jak i pozwala na generowanie końcówek (endpointów) REST-owych. Oczywiście to nie jest takie proste jak się wydaje. W systemach tego rodzaju możemy wyróżnić dwa rodzaje interfejsów komunikacyjnych. Pierwszy to interfejs zewnętrzny, który jest wykorzystywany przez naszych klientów. Ten prawie na pewno powinien być zestandaryzowany. Jakiś REST, może SOAP, może całość napędzane na CORBA/kę. Drugi interfejs to wewnętrzny interfejs komunikacji. Służy on do komunikacji pomiędzy mikroserwisami. Tu zazwyczaj używamy jakiegoś binarnego ustrojstwa w rodzaju gRPC albo Aerona.

I tu wchodzi JLupin…

Największą bolączką opisanego wyżej systemu jest komunikacja wewnątrz. Trzeba jakoś dogadywać mikroserwisy, udostępniać je w ramach systemu oraz monitorować ich stan. Dużo roboty, którą dobrze by było oddelegować. JLupin Next Server jest rozwiązaniem, które pomaga ogarnąć cały ten burdel. Jest niezależne od frameworków aplikacyjnych, ale out-of-box dostarcza nam wsparcie dla Springa, a to dlatego, że twórcy wyszli z założenia, że Spring jest najpopularniejszym rozwiązaniem i trzeba je wspierać by JLNS się przyjął. Co nam daje JLNS?

Po pierwsze ogarnia komunikację wewnątrz systemową. Takie rzeczy jak Service Discovery, wewnętrzna komunikacja w oparciu o szybki binarny protokół, czy nadzór nad stanem poszczególnych mikroserwisów mamy od ręki. Ponad. to mamy zapewniony pseudo hot swap, który pozwala zachować ciągłość działania aplikacji. Mamy narzędzia do monitorowania i zarządzania naszej mikroserwisowej infrastruktury. Mamy możliwość podzielenia naszego systemu na strefy, umieszczania mikroserwisów na konkretnych maszynach w określonych strefach (logicznych) oraz co za tym idzie możliwość zarządzania widocznością mikroserwisów. Do tego twórcy przygotowali plugin do mavena, który pozwala na automatyzację deploymentu oraz plugin do IntelliJ, który pozwala na ogarnięcie zależności generowanie powiązań.

Instalacja

Instalacja sprowadza się do pobrania odpowiedniej paczki i jej rozpakowania na dysku. W przypadku wersji community należy jeszcze wyczyścić zawartość katalogu application/, bo siedzi tam przykładowa aplikacja.

Uruchomienie

W katalogu start mamy skrypt start.sh, który po uruchomieniu nie daje śladów życia, ale w katalogu logs/server/main/start będzie sobie leżał plik server.out, a logs/server/main kilka wyspecjalizowanych logów.
Jeżeli w katalogu start uruchomimy skrypt control.sh, to dostaniemy się do konsoli administracyjnej naszego serwera. Tu mamy wgląd w stan naszego noda oraz możemy zarządzać mikroserwisami. Wygląda to tak:

No trochę vintage, ale zawsze lepiej niż konsola weblogica.

Community vs Enterprise

Różnice są trzy. Po pierwsze wersja community jest ograniczona do 15 mikroserwisów na node. Po drugie może być tylko jeden node. Po trzecie nie ma dostępnego narzędzia Control Center, do zarządzania nodami (bo i po co).

Dodatki

Jeżeli chcemy możemy jeszcze dociągnąć sobie plugin do IntelliJ, bo z jakiś nieznanych mi powodów nie ma go w repozytorium JetBrains. Możemy też pobrać plugin do mavena jeżeli nie chcemy łączyć się do repozytorium JLupina.

Podsumowanie

Na dziś starczy. W kolejnych wpisach pobawimy się w tworzenie prostego systemu bankowego, z wykorzystaniem mikroserwisów i JLupina.

BDD i kryptomonady

BDD, czyli Buzzword Driven Development, to rak projektów IT. Kupujemy technologię, bo ładnie brzmi, albo jest modna, a potem płaczemy jak przychodzi do utrzymania.

I o tym mówiłem na Lambda Days 🙂

A tu macie KryptomonadyW

O nazwach

Brak motywacji, by dokończyć pisanie o nazwach i rozwiązać „konkurs” dotyczący nazwy został zredukowany do zera „hejtem”, jaki pojawił sie ostatnio na kod ze strony pkp intercity.

Koledzy i koleżanki w kodzie. Powiem wam jedno.

JESTEŚCIE HIPOKRTAMI

I niech kurwa nikt nie mówi, że nie, bo każde z nas puściło nie jedną kiepską nazwę na produkcję. Do problemu PKP jeszcze wrócimy. Skupmy się na temacie nazw.

Nazywać po imieniu

Problem nazewnictwa przerabialiśmy już w cyklu ekstremalna obiektowość w praktyce, a dokładnie w części piątej. W tamtym wpisie nie wyczerpaliśmy tematu. Co więcej, tamten wpis dotyka trochę innego problemu, czyli odszukiwania potencjalnych problemów za pomocą kontekstu użycia danej metody.

Nazywamy nie tylko metody, ale też klasy, moduły, zmienne i dlatego warto zastanowić się jak robić to dobrze.

Zasady — ogólnie

Branżunia, wypracowała sobie pewne zasady, co do nazywania różnych rzeczy. Można je ująć w następujący sposób.

  • Klasy, moduły, struktury itp. obiekty, które reprezentują w kodzie pewne byty biznesowe nazywamy korzystając z rzeczowników.
  • Funkcje, metody, procedury reprezentują czynności i ich nazwy pochodzą od czasowników.
  • Wartości typów wyliczeniowych, stałe reprezentujące pewną cechę itp. są przymiotnikami, ale możemy je grupować np. w enum-a, którego nazwa jest rzeczownikiem.
  • Chcąc doprecyzować znaczenie danego elementu oraz umieścić go w przepływie sterowania możemy dodawać przedrostki w rodzaju Pre i Post, albo przyimki. W przypadku przyimków zazwyczaj stosujemy okoliczniki czasu, bo co do zasady używamy języka angielskiego.

Proste. Klasa rzeczownikiem, metoda czasownikiem. Jak pamiętacie coś z polskiego, z podstawówki, to jeszcze dołóżcie budowę zdania i mamy ładnie rozklepane wywołanie metody.

Listing 1. Przykładowe wywołanie metody

"12-345".matches(plPostCodePattern)
  • „12-345” — podmiot.
  • matches — orzeczenie.
  • plPostCodePattern — przydawka (not sure)

Istnieje też grupa zasad, które pozwalają na identyfikowanie problemów:

  • Jeżeli nazwa zawiera spójniki i, lub, albo, to mamy wiele odpowiedzialności.
  • Jeżeli nazwa zawiera generyczne bądź abstrakcyjne rzeczowniki, a kod, który reprezentuje jest konkretny, to mamy najprawdopodobniej do czynienia z nieprecyzyjną nazwą.
  • Jeżeli nazwa na wysokim poziomie abstrakcji zawiera szczegółową nazwę implementacji, to abstrakcja nam cieknie.
  • Jeżeli mamy powtórzenie w nazwie, to przekombinowaliśmy z architekturą.

Do tego dochodzi kilka „zabronionych” słów w rodzaju Listener, Handler czy Provider, czy niespójności w nazwie i typie zwracanym/przyjmowanym. I mamy komplet.

Problemy

Przyjmując takie proste zasady dość łatwo jest stworzyć idealną nazwę, prawda? No nie do końca. Po pierwsze nazwa powinna być opisowa, ale nie długa. Problem już omówiliśmy wcześniej. Nazwę można skracać, byle nie za bardzo, a nawet powiem więcej czasami stosujemy skróty w rodzaju DTO, DAO. To powoduje, że mamy ograniczone pole manewru. Po drugie nazwa powinna ułatwiać nam lokalizację kodu w architekturze, ale pewne nazwy występują w wielu miejscach w odmiennych znaczeniach. Po trzecie nazwa powinna być dostosowana do poziomu abstrakcji, na którym pracujemy, a nie zawsze jest to możliwe. Po czwarte, w końcu, nazywający może mieć ograniczony zasób słownictwa, który ogranicza mu możliwości zarówno w przypadku oczywistych jak i specyficznych nazw.

Dobra nazwa, czyli jaka?

Z dotychczasowej zabawy wiemy już, że nadanie dobrej nazwy może być problematyczne. Co więcej, im mniej wiemy o systemie, tym trudniej jest nam znaleźć odpowiednią nazwę, która będzie pasować do świata. Z pomocą przychodzi nam DDD, które wymusza do pewnego stopnia stosowanie odpowiedniego nazewnictwa. Użycie języka domenowego do opisu operacji i bytów przenosi ciężar szukania nazw z programistów na ekspertów domenowych. Nie jest to, niestety, idealne rozwiązanie, ponieważ eksperci domenowi mają trochę inną perspektywę. Zazwyczaj bardzo dobrze rozumieją poszczególne pojęcia i potrafią ich używać w odpowiednim kontekście. Jeżeli damy programiście dwa identycznie nazywające się byty, to mamy dużą szansę, że wyprodukuje potworka. Tu warto podrzucić przykład, a ten będzie wredny.

W naszej aplikacji mamy konto (ang. account) i rachunek (ang. account). Konto istnieje w kontekście księgi głównej, rachunek w kontekście klienta. Spotykają się one w kontekście księgowego (ang. accountant), który na podstawie operacji na rachunkach tworzy operacje na kontach.

Zastanówcie się nad nazwami. Przykład jest o tyle wredny, że obie te nazwy w kontekście operacji finansowej w języku angielskim są reprezentowane przez to samo słowo. Nie takie samo, ale to samo. No właśnie, kontekst i język.

Język ma znaczenie

Większość z nas używa języka angielskiego w kodzie. Przyjmujemy to jako oczywistość. Wszelkie odstępstwa są traktowane jako herezja. Czy jednak słusznie? Jeżeli popatrzymy na ten problem z perspektywy użyteczności, to kurczowe trzymanie się języka angielskiego nie ma sensu. Jeżeli środowisko pracy jest polskojęzyczne, to używanie języka polskiego może okazać się dobrym wyborem. Oczywiście musimy spełnić kilka warunków m.in. jakoś zagwarantować sobie, że wszyscy znamy język polski. Oznacza to też, że ograniczamy sobie możliwość zatrudniania, ale czasami i tak mamy to narzucone.
Inna sprawa to język dokumentacji i analizy. Tu często mamy do czynienia z językiem polskim, co prowadzi do podwójnego tłumaczenia nazw i w efekcie może prowadzić do nieporozumień. Jako ciekawostkę można potraktować projekty realizowane przez firmy z Niemiec, gdzie zarówno dokumentacja, jak i komunikacja są w języku niemieckim. Kod też jest w języku niemieckim, jeżeli nie wychodzi poza organizację.

Casus PKP, czyli podsumowanie

Wspomniany na początku kod w wykonaniu PKP Intercity jest, z punktu nazewnictwa, znośny. Problemem nie jest język polski, bo taka jest konwencja i naprawdę nie ma potrzeby, by kombinować z angielskim. Mamy tu do czynienia z dwoma innymi problemami.

Mieszanie języków

Do pewnego stopnia wymuszone przez konwencje stosowanie przedrostków is, valid itp. Jest to problem nie do rozwiązania, szczególnie w językach słabo lub niejawnie typowanych, które bazują na konwencji (patrz FORTRAN i implicit none). W językach silnie typowanych ten problem ma postać generycznych nazw metod w interfejsach np. handle, accept itp. Taki problem, nie problem.

Mieszanie konwencji

Rzeczywistym problemem w tym kodzie jest mieszanie różnych konwencji nazewniczych. Czasami camelCase rowerOferta, czasami snake_case is_bilet_rodzinny. Nazwy pełne kod_znizki, skróty kat_poc, obfuskacja v1, nazwy zawierające liczebniki porządkowe liczba_u_2.
Takie nietrzymanie konwencji jest jak nietrzymanie moczu. W pewnym momencie narobimy sobie wstydu przed obcymi ludźmi.

Sprawa jakości samego kodu nie jest tu istotna, ale będzie i o tym.

Rozwiązanie konkursu

W komentarzach było kilka propozycji co oznacza attrValPrnt. bestils, zaproponował attractvalueprint, która to nazwa też była naszym pierwszym rozwinięciem gdy mieliśmy problem i pośrednio przyczyniła się do powstania kilku ostatnich wpisów. Paweł zaproponował pointer, co jest bliższe prawdzie, ale kolejność liter się nie do końca zgadza.

Otóż moi mili. attrValPrnt to nic innego jak Attribute Value Parent. Jest to wskaźnik na słownik nadrzędny, w którym trzeba szukać wartości dla danego atrybutu pod takim samym kluczem. Fajne prawda?