Tygodniowe wyzwanie programistyczne — #1

By 14 October 2016 October 15th, 2016 ITlogy, Wydarzenia

Dzisi­aj pier­wsze zadanie pro­gramisty­czne — dla początku­ją­cych mamy sposób na przeko­nanie się do pewnych, cza­sem nieciekawych, czyn­noś­ci, a zaawan­sowanych skła­ni­amy do reflek­sji nad czymś, nad czym pewnie dawno się nie zas­tanaw­iałaś. Gotowa?

Wyzwanie dla początkujących

Testowanie kodu to PODSTAWA. Bez niej pro­gramowanie jest jak wróże­nie z fusów — a nuż napisany przez nas kod zadzi­ała. Choć ja bardziej trzy­małabym się wer­sji, że póki nie ma testów, to kod nie dzi­ała ;) No ale, z różnych powodów nie każdy lubi, nie każdy umie, nie każdy w końcu je pisze. Jako początku­ją­cy powinieneś jed­nak wyro­bić sobie nawyk: kod bez pokrycia tes­ta­mi to nie jest kod gotowy!

Jak to zro­bić najłatwiej? Sko­rzys­tać z pode­jś­cia Test Dri­ven Devel­op­ment (TDD) i pisanie swo­jego kodu zaczy­nać od testów. Taka prak­ty­ka ma jeszcze jeden super plus: w ten sposób możesz szy­bko i pros­to rozbić skom­p­likowaną funkcjon­al­ność na malutkie kroczki.
Więcej o tym, jak dzi­ała TDD przeczy­tasz w naszym wpisie na ten tem­at.

ZADANIE:

Korzys­ta­jąc z TDD zaim­ple­men­tuj rozwiązanie następu­jącego prob­le­mu: weźKanap­kę (get­Sand­wich)
Kanap­ka skła­da się z dwóch kawałków chle­ba i czegoś pomiędzy ;) Zwróć Stringa, który zna­j­du­je się pomiędzy pier­wszym i ostat­nim “chlebem” w danym Stringu, albo zwróć pusty String “” jeśli kanap­ka nie ma w sobie dwóch kawałków “chle­ba”.

Przyk­la­dy:

weźKanapkę("chlebjajkochleb") → zwraca "jajko"
weźKanapkę("xxchlebszynkachlebyy") → zwraca "szynka"
weźKanapkę("xxchlebyy") → zwraca ""

PS. Uży­wanie pol­s­kich nazw metod i zmi­en­nych nie jest dobrą prak­tyką, zostały one jed­nak przez nas zas­tosowane by ułatwić Ci zrozu­mie­nie przykładu. W rozwiąza­niu prosimy, abyś trzy­mała się kon­wencji i uży­wała ang­iel­s­kich nazw :)

Nasza odpowiedź

Rozwiązanie: https://gist.github.com/apietras/ce299f08c4d43e34f566e8d9741bc5df
Testy: https://gist.github.com/apietras/7df4437ab0b7da2fc13aaf5f57cb79de

Jak widzi­cie moje rozwiazanie to 3 metody, wyko­rzys­tu­jące różne możli­woś­ci Javy ;) Po pier­wsze, Pat­tern, po drugie, index­Of(), a po trze­cie split().

Jakie są korzyś­ci poszczegól­nych rozwiązań?

Uży­cie klasy Pat­tern (czy ogól­nie wyrażeń reg­u­larnych) to zde­cy­dowanie najprost­szy i najbardziej ele­ganc­ki sposób. Kod jest krót­ki, czytel­ny i nie ma wąt­pli­woś­ci, co się w nim dzieje. Wadą tego rozwiąza­nia jest to, że jest ono sto­sunkowo obciąża­jące (anal­iza wyraże­nia reg­u­larnego może być pra­cochłon­na dla kom­put­era — o czym boleśnie przekon­ał się kiedyś sam Stack­Over­flow ;) )

Uży­cie index­Of to opty­mal­ny sposób, ale też bard­zo mało elasty­czny — nawet pros­ta zmi­ana warunk­ów zada­nia mogła­by wyma­gać wielu zmi­an w kodzie. W kodzie zaczy­na­ją się pojaw­iać mag­iczne licz­by — np. tutaj musimy odjąć dłu­gość ‘chle­ba’, tam dodać ‘1′ żeby uzyskać właś­ci­we licz­by… Rozwiązanie nadal poprawne, ale czy­ta­jąc je za kil­ka miesię­cy możesz spędz­ić chwilę odszyfrowu­jąc, co znaczy ta ‘5′, a skąd jest ta ‘1’.

Sko­rzys­tanie z metody split() dodałam bardziej dla kom­plet­noś­ci niż jako fak­ty­czne rozwiązanie, które pole­cam ;) Teo­re­ty­cznie jest to możli­we, ale kod nie jest zbyt czytel­ny i wyma­ga od nas kilku mag­icznych trick­ów — np. zapewnie­nie, że split() zwró­ci także puste strin­gi na początku i końcu, na czym opar­ta jest dal­sza część imple­men­tacji. Dodatkowo ponieważ meto­da split() przyj­mu­je wyraże­nie reg­u­larne, a nie po pros­tu ciąg znaków, musimy uważać co jest naszym ‘chlebem’. Konieczne jest też ‘odbu­dowanie’ wyniku na końcu, co także nie popraw­ia czytel­noś­ci. Co więcej — ‘pod spo­dem’ meto­da ta korzys­ta z wyrażeń reg­u­larnych, więc nie będzie w żaden sposób szyb­sza od pier­wszego rozwiązania.

Moje testy pokry­wa­ją nie tylko te 3 przy­pad­ki, które były pokazane jako exam­ple. Tworząc je starałam się myśleć: co jeszcze, może być argu­mentem dla tej metody. Stąd mamy testy dla pustego Stringa, kanap­ki z jed­nym chlebem.

Uwa­gi co do samego kodu. Przeglą­da­jąc Wasze rozwiąza­nia chcielibyśmy zwró­cić uwagę na kil­ka kwestii związanych z Clean Code:

  • sto­su­j­cie nazwy, które coś mówią! Wiemy, że to tylko ćwicze­nie, ale czemu nie nazy­wać zmi­en­nych czy metod/funkcji w sposób, gdzie jas­no wiemy co będą robić (również mówimy tu o meto­dach testowych. Częs­to spo­tykaną prak­tyką jest zaczy­nanie ich nazwy od słowa test lub should, a dal­sza jej część powin­na starać się tłu­maczyć sens testu),
  • dobrą prak­tyką jest nazy­wanie pliku z tes­ta­mi tak samo jak pliku, który jest nimi testowany z dopiskiem Test na końcu. czyli klasę Sand­wich tes­tu­je­my klasą SandwichTest
  • oczeki­wany wynik i zmi­en­na jaką przyj­mu­je testowana meto­da powin­ny być wyciąg­nięte do zmi­en­nych lokalnych metody — dzię­ki temu w łatwy sposób zmody­fiku­jesz test, jeśli będzie taka potrze­ba (oraz napiszesz kole­jne, po pros­tu zmieni­a­jąc te dwa parametry),
  • warto korzys­tać ze stałych, u mnie wyciągnęłam w ten sposób „chleb”, ponown­ie ułatwia to utrzy­manie kodu
  • zazwyczaj nie ma sen­su robić com­mi­ta z zako­men­towanym kodem — wprowadza to zamieszanie, a potem taki kod jest trochę cmen­tarzem — nikt nie chce go usunąć, bo a nuż to się kiedyś przy­da i wisi sobie niewiado­mo ile ;)

Wyzwanie dla praktyków

Logowanie to zagad­nie­nie, które przewi­ja się w prak­ty­cznie każdej aplikacji — od najm­niejszej , pier­wszej aplikacji webowej po najwięk­sze aplikac­je rozpros­zone. Choć zagad­nie­nie wyda­je się bła­he, z całą pewnoś­cią takie nie jest — szczegól­nie w przy­pad­ku sys­temów wielowątkowych, współ­bieżnych lub obsługu­ją­cych dużą ilość zapy­tań. Głównie z tego powodu nawet pro­gramiś­ci szczy­cą­cy się ‘pisaniem wszys­tkiego od zera’ korzys­ta­ją ze sprawd­zonych bib­liotek — w przy­pad­ku Javy Log4J czy Log­back to obec­nie najpop­u­larniejsze z nich (więcej o nich możesz przeczy­tać w jed­nym z naszych wpisów).

Twoim zadaniem jest napisanie włas­nej bib­liote­ki do logowa­nia. Oczy­wiś­cie nie będzie ona tak zaawan­sowana, jak dostęp­ne na rynku rozwiąza­nia, ale spróbuj osiągnąć pod­sta­wowe funkcjon­al­noś­ci — Two­ja ‘bib­liote­ka’ powin­na zapewniać:

  • możli­wość tworzenia osob­nych log­gerów (w przy­pad­ku języków obiek­towych) lub przekazy­wa­nia nazwy log­gera (w przy­pad­ku pozostałych języków)
  • możli­wość logowa­nia na kilku poziomach (INFO, WARN, ERROR)
  • możli­wość uży­wa­nia para­metrów w logowanym tekś­cie (w dowol­ny sposób — możesz wyko­rzys­tać ist­niejące w języku narzędzia)
  • bib­liote­ka ma wyp­isy­wać logi na kon­solę, w for­ma­cie {czas} {nazwa} [{poziom}]: {wiado­mość}

Po jej zaim­ple­men­towa­niu zas­tanów się nad poniższy­mi kwestiami:

  • Jak bib­liote­ka zachowa się w aplikacji wielowątkowej (jeśli język, którego uży­wasz, wspiera takie aplikacje)
  • Co jest najwięk­szą słaboś­cią tej bib­liote­ki i jak moż­na ją wyeliminować?
  • Czy Two­ja bib­liote­ka w jak­iś sposób wpłynie na dzi­ałanie i wyda­jność aplikacji?
  • W czym jest lepsza/gorsza od ist­nieją­cych rozwiązań?
  • Jakie zmi­any były­by potrzeb­ne, jeśli zami­ast do kon­soli chci­ałbyś zapisy­wać logi do pliku?
  • Jakie decyz­je pod­jęłaś w trak­cie imple­men­tacji? Dlaczego wybrałaś tą a nie inną drogę? Czy zmieniłabyś swo­ją decyzję?
  • Jak przetestowałabyś swo­ją implementację?

Pamię­taj, aby ukończyć zadanie w mniej więcej 40–50 min­ut — nie chodzi o to, żeby spędz­ić nad nim cały dzień i stworzyć najlep­szy loger świa­ta, ale o to aby zauważyć i zas­tanow­ić się nad pewny­mi prob­le­ma­mi, które na codzień bib­liote­ki rozwiązu­ją za nas. Nie ma też prob­le­mu w tym, żeby imple­men­tac­ja była np. niewyda­j­na itp — najważniejsze, żebyś zauważyła te prob­le­my i zas­tanow­ił się nad ich przy­czyną i możli­wy­mi rozwiązaniami.

Pytanie, z jakim Cię dzisi­aj zostaw­iamy to z jakiej bib­liote­ki korzys­tasz najczęś­ciej i nie wyobrażasz sobie życia bez niej? Czy wiesz jak ona dzi­ała? Jak Ty byś ją zaimplementowała?

Nasza odpowiedź

Moje rozwiązanie zada­nia zna­jdziesz pod adresem https://gist.github.com/jderda/4a8dba539d3471e1a5fec08128705c8a .

Jed­no z głównych założeń, które przyjąłem pod­czas tworzenia tego rozwiąza­nia było wspar­cie dla aplikacji wielowątkowych (np. aplikacji webowych) oraz względ­na wyda­jność (szczegól­nie pod kątem pamię­ci). Obie te rzeczy wynika­ją z moich doświad­czeń — kilkukrot­nie w przeszłoś­ci pisząc na szy­bko jak­iś PoC zami­ast nor­mal­nego log­gera lub zapisu do pliku wyp­isy­wałem tekst na kon­soli. Jak to się dzieje z każdym PoC, po drob­nych poprawkach na szy­bko robionych przez inną osobę trafi­ał on na pro­dukcję, po czym okazy­wało się, że ‚logi’ są ze sobą poprzepla­tane i zupełnie nieczytelne. Dru­ga kwes­t­ia była prob­le­mem, z jakim się spotkałem w jed­nej z poprzed­nich prac — wąskim gardłem aplikacji były właśnie logi (zapisy­wane w zde­cy­dowanie nad­miernej iloś­ci, przy uży­ciu bib­liote­ki ‚wynalezionej’ przez poprzed­nich pro­gramistów tej aplikacji).

Całość jest dość pros­ta — mamy kole­jkę w pamię­ci (Pri­or­i­ty­Block­ingQueue), która prze­chowu­je nam wpisy ‚do wyp­isa­nia’ oraz wątek, który pobiera kole­jne wpisy z tej kole­j­ki i wyp­isu­je je na kon­sole. Wybrałem tutaj najprost­szą i najsen­sown­iejszą drogę jeśli chodzi o wybór struk­tu­ry danych:

  • Pri­or­i­ty­Block­ingQueue jest bez­piecz­na w środowisku wielowątkowym (mówi o tym doku­men­tac­ja API)
  • Nie potrze­bu­je logi­ki związanej ze sprawdzaniem, czy kole­j­ka jest pus­ta i/lub czekaniem na kole­jny ele­ment — ponieważ uży­wamy Block­ingQueue, wywołanie metody ‚pobierz ele­ment’ samo poczeka, jeśli taki ele­ment nie jest jeszcze dostępny
  • Ponieważ jest to też kole­j­ka pri­o­ry­te­towa, rozwiązu­je nam z automatu wszelkie ewen­tu­alne sytu­acje ‚wyś­cigu’ (ang. race con­di­tion) — gdy­byśmy użyli zwykłej listy, teo­re­ty­cznie możli­we jest, że wpisy nie wyświ­et­lały by się w kole­jnoś­ci (oczy­wiś­cie mówimy tutaj o skra­jnych przy­pad­kach i wpisach z prak­ty­cznie tego samego momentu)

Resz­ta kodu to właś­ci­wie tylko metody do uproszc­zonego tworzenia nowych obiek­tów loga — obsłu­ga para­metrów opiera się na Javowej skład­ni metod ze zmi­en­ną iloś­cią argu­men­tów, a ich ‚dołaczanie’ do ciągu znaków na metodzie String.format (dzi­ała ona niemal iden­ty­cznie jak meto­da sprintf w językach C‑podobnych).

Odpowiedzmy sobie na pyta­nia postaw­ione w zadaniu:

Jak bib­liote­ka zachowa się w aplikacji wielowątkowej (jeśli język, którego uży­wasz, wspiera takie aplikacje)

Dzię­ki uży­ciu Pri­or­i­ty­Block­ingQueue, aplikac­ja może być uży­wana w środowisku wielowątkowym. Dodawanie wpisów logu jest niebloku­jące, dzię­ki czemu nie powin­na wpłynąć negaty­wnie na wyda­jność aplikacji.

Co jest najwięk­szą słaboś­cią tej bib­liote­ki i jak moż­na ją wyeliminować?

Obec­nie bib­liote­ka uniemożli­wia automaty­czne zamknię­cie JVM (jako że posi­a­da dzi­ała­ją­cy wątek) — konieczne było­by dodanie kodu pozwala­jącego na obsługę ‚powiadomień’ o kończe­niu dzi­ała­nia aplikacji lub zrezyg­nowanie z zapisy­wa­nia w tle za pomocą jed­nego wątku i uży­wanie zadań do poje­dynczych wpisów logu.

Aplikac­ja ta może być też obciąża­ją­ca dla pro­ce­so­ra — założe­niem było to, że pamięć jest cen­niejsza od mocy obliczeniowej. Aby zmniejszyć obciąże­nie pro­ce­so­ra moż­na zrezyg­nować z for­ma­towa­nia z uży­ciem metody String.format na rzecz prostego ‚pod­staw­ia­nia’ (znanego np. z Slf4j).

W przy­pad­ku dużej iloś­ci logów, w pamię­ci pozostanie sporo obiek­tów Log­Item, uży­wanych tylko przez chwilę, powodu­jąc częste uruchami­an­ie Garbage Col­lec­to­ra, w efek­cie spowal­ni­a­jąc aplikację. Ten prob­lem moż­na by wye­lim­i­nować korzys­ta­jąc z wzor­ca Pyłek (po pros­tu ponown­ie uży­wa­jąc tych samych obiek­tów do prze­chowywa­nia innych informacji)

Czy Two­ja bib­liote­ka w jak­iś sposób wpłynie na dzi­ałanie i wyda­jność aplikacji?

Bib­liote­ka, w szczegól­noś­ci w przy­pad­ku logowa­nia dużej iloś­ci infor­ma­cji, może zwięk­szyć wyko­rzys­tanie CPU. W takiej sytu­acji prob­le­mem będzie też tworze­nie dużej iloś­ci obiek­tów w pamię­ci, powodu­jąc częste uruchami­an­ie Garbage Col­lec­to­ra. Dla aplikacji o takim pro­filu będzie obciąża­ją­ca i spowalniająca.

W czym jest lepsza/gorsza od ist­nieją­cych rozwiązań?

Bib­liote­ka ta nie ma przewag nad więk­szoś­cią dostęp­nych imple­men­tacji. Jest min­i­mal­nie szyb­sza w specy­ficznych przy­pad­kach od np. Log4J, ale wyni­ka to z małej iloś­ci funkcji i odpowied­nio dobranych warunk­ów testowych. Obszary, w których jest gorsza od dostęp­nych rozwiązań:

  • możli­woś­ci kon­fig­u­ra­cyjne (np. zapis do pliku, różne wzorce logów itp, kon­fig­u­rac­ja poziomów logowania)
  • opty­mal­iza­c­je (np. pomi­janie niek­tórych czyn­noś­ci, jeśli dany poziom logowa­nia jest ignorowany)
  • wyko­rzys­tanie pamięci
  • ogól­na wyda­jność i wpływ na aplikację

Jakie zmi­any były­by potrzeb­ne, jeśli zami­ast do kon­soli chci­ałbyś zapisy­wać logi do pliku?

Ponieważ kwest­ię współ­bieżnoś­ci rozwiąza­l­iśmy na wcześniejszym etapie, wystar­czy zami­ana wywoła­nia System.out na zapis do pliku. Plik może pozostawać otwarty przez dłuższy czas z uwa­gi na to, że zawsze będzie tylko jeden wątek zapisu­ją­cy logi w obec­nym rozwiązaniu.

Jakie decyz­je pod­jęłaś w trak­cie imple­men­tacji? Dlaczego wybrałaś tą a nie inną drogę? Czy zmieniłabyś swo­ją decyzję?

Pod­czas imple­men­tacji pod­jąłem kil­ka istot­nych decyzji:

  • opar­cie imple­men­tacji o ist­niejącą klasę Pri­or­i­ty­Block­ingQueue, co poz­woliło na znaczne uproszcze­nie całoś­ci — zde­cy­dowanie nie zmieniłbym tej decyzji
  • uży­cie staty­cznej kole­j­ki z jed­nym wątkiem, który stale zapisuje/czeka na wpisy — wybrane rozwiązanie ma swo­je zale­ty (nie musimy się martwić o różne instanc­je log­gerów, sytu­acje wyś­cigu itp), ale niesie za sobą też pewne kon­sek­wenc­je (np. jeśli log­ger jest uży­wany tylko w przy­pad­ku jed­nego obiek­tu, który żyje krótko w pamię­ci, wszys­tkie zaso­by takie jak wątek, kole­j­ka itp nie zostaną usunięte z pamię­ci aż do zamknię­cia JVM) — najpraw­dopodob­niej rozważyłbym zmi­anę tego rozwiąza­nia na inne (np. poje­dyncze zada­nia z wątkiem twor­zonym tylko w razie potrze­by), porów­nał dzi­ałanie różnych opcji i wybrał najlep­szą z punk­tu widzenia pro­jek­tu (lub stworzył kon­fig­u­rację wokół tego?)
  • staty­cz­na meto­da get­Log­ger vs kon­struk­tor — meto­da staty­cz­na pozwala na zmi­anę logi­ki i drob­ne opty­mal­iza­c­je w przyszłoś­ci (np. jeśli mamy już utwor­zony log­ger o danej nazwie, może­my go po pros­tu wyko­rzys­tać ponown­ie zami­ast tworzyć nowy), uży­cie kon­struk­to­ra powodowało­by, że każde stworze­nie log­gera powodu­je utworze­nie nowego obiek­tu (a trze­ba mieć na uwadze, że użytkown­i­cy najpraw­dopodob­niej nie zawsze zas­to­su­ją się do Twoich wskazówek i dokumentacji)

Jak przetestowałabyś swo­ją implementację?

Najważniejsza kwes­t­ia to ‚jak sprawdz­ić, czy na kon­solę został wyp­isany odpowied­ni tekst’? Na szczęś­cie Java jako jeden z niewielu języków pozwala na ‚przekierowanie’ stan­dar­d­owego wyjś­cia w inne miejsce lub po pros­tu prze­ję­cie go (zain­tere­sowanych odsyłam do Stack Over­flow, gdzie jest przykład jak moż­na to osiągnąć). W przy­pad­ku innych języków pozosta­je nam albo dołącze­nie logi­ki pozwala­jącej przekierować logi zami­ast do kon­soli to do zmi­en­nej, albo osiąg­nię­cie tego samego poprzez polimor­fizm. W niek­tórych jezykach mamy też metody na ‚obe­jś­cie’ tego prob­le­mu (np. w PHP ma to nazwę out­put buffer­ing).

Dru­ga kwes­t­ia to jakie przy­pad­ki testowe należy rozważyć, mi do głowy przyszły następujące:

  • uży­cie log­gera żeby zapisać 10 wiadomości
  • uży­cie log­gera żeby zapisać 10 tysię­cy wiadomości
  • uży­cie log­gera współ­bieżnie w 2 wątkach (aby możli­wie zasy­mu­lować sytu­ację wyś­cigu) — uwa­ga, ten test jest niedeterministyczny!
  • stworze­nie kilku log­gerów o innych nazwach i uży­cie ich do zapisa­nia określonej wiadomości

Pamię­taj, że nowe zada­nia będą się pojaw­iać codzi­en­nie o godzinie 11. Rozwiąza­nia będziemy umieszczać pod zada­ni­a­mi kole­jnego dnia o godzinie 18. Nie zapom­nij podzielić się swoi­mi odpowiedzi­a­mi i prze­myśle­ni­a­mi na wydarze­niu na face­booku, a jak masz ochotę to też w komentarzu ;)!

Lin­ki do wszys­t­kich zadań zna­jdziesz w innym wpisie na naszym blogu. Powodzenia!