Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Podsumowanie.
Po odłożeniu kodu na weekend, jego przeglądzie i uwzględnieniu komentarzy, można uznać proces refaktoryzacji za zakończony. Pisząc „proces” mam tu na myśli wykonanie refaktoryzacji o ściśle określonym celu – w tym przypadku było to stworzenie mechanizmu importu, który zastąpi używany obecnie. Nie wchodziły zatem w ten proces ani kwestie związane z mechanizmami składowania zaimportowanych danych (w tym budowanie XML-a i sposób jego zapisu), nie wchodziły też kwestie związane z zachowaniem się interfejsu użytkownika (o czym na końcu). Wykonując reafaktoryzację zawsze należy mieć na uwadze jej cel i w żadnym wypadku nie zbaczać na manowce. Wszelkie inne potencjalne cele refaktoryzacji zauważone podczas wykonywania tej konkretnej należy sobie zapisać i po skończeniu obecnej zdecydować czy należy je wykonać czy nie (mogą być np. nieopłacalne i lepiej wymienić mechanizm na inny).
Refaktoryzacja to porządkowanie kodu – zapewne nie raz robiliście porządki, np. sprzątaliście mieszkanie, (jeśli macie dzieci – z pewnością ;)). Jest to zadanie wymagające konsekwencji i nie należy go robić chaotycznie, bo wówczas można zapomnieć o tym, że miało się jeszcze gdzieś posprzątać. Jeżeli podczas porządkowania danego obszaru mieszkania zauważamy nieporządek w innym jego obszarze, to jego uporządkowanie zostawiamy na później – aż do chwili dotarcia do tego obszaru. Możemy najwyżej zabrać jakąś rzecz, która w tamtym obszarze się znajduje, a powinna się znaleźć w aktualnie porządkowanym obszarze.
Jeśli nie będziemy zachowywać dyscypliny trzymania się celu refaktoryzaji, to możemy jej nie skończyć, albo zakończyć ją po o wiele dłuższym, niż przewidywany, czasie. Możemy też błądząc po manowcach zapomnieć zrefaktoryzować tych fragmentów kodu, których reafaktoryzacja była celem.
Refaktoryzacja nie jest nigdy skończona kompletnie. W wyniku jej przeprowadzenia powinniśmy otrzymać kod wystarczająco dobry – zgodny z oczekiwaniami. Należy refaktoryzować mając na uwadze potrzebę projektów, w których kod występuje, a nie przyszłych – hipotetycznych. Owszem, można stwierdzić, że tak naprawdę powinno się zrobić jeszcze to i to, bo to zuniwersalizuje kod jeszcze bardziej i w przyszłości będzie łatwiej. Ale nie wiadomo czy owa przyszłość nastąpi, zatem taka zmiana może być nieopłacalna. Oczywiście, jeśli mamy nadmiar czasu, to możemy takie coś zrealizować – choćby w celach treningu własnych umiejętności.
By daleko nie szukać – również w przypadku kodu z tej refaktoryzacji można by pójść dalej. Wystarczy zauważyć, że tak naprawdę metoda szablonowa jest niedaleką kuzynką wzorca strategii, można zatem jej kod rozbić na dwie klasy: jedną implementująca wzorzec strategii i realizującą jedynie jedną odpowiedzialność – scenariusz importu. Resztę jej kodu wydzielić do innej klasy i uczynić ją odpowiedzialną za pobieranie danych skądkolwiek (tak, tak, nie tylko z plików). Taka abstrakcja pozwoli potem pobierać dane nie tylko z plików, ale także z innych źródeł – wystarczy inaczej zaimplementować metodę Read(). Wzorzec strategii będzie opierał się na abstrakcji pobierania danych, pod którą będzie podstawiona konkretna implementacja. Rozwiązanie będzie jeszcze bardziej uniwersalne i elastyczne. Jednakowoż – jak napisałem na wstępie poprzedniego akapitu – z punktu widzenia potrzeb tego projektu obecnie otrzymany kod jest wystarczający. Nie ma sensu brnąć dalej, jeżeli nie ma na to czasu.
Przedstawiony w cyklu proces refaktoryzacji może wydawać się bardzo uporządkowany. Wynika to – niestety – z potrzeby jak najlepszego przekazania materiału. Chaotyczność by temu nie sprzyjała, choć troszeczkę starałem się pokazywać ślepe zaułki (czasami pojawiały się one spontanicznie). W rzeczywistości refaktoryzacja może momentami przypominać gonienie własnego cienia. Ale jeśli będzie się sporządzać plan działania (jak pokazano w cyklu), zerkać krytycznie na jego wykonanie, konfrontować z testami – takich pogoni być nie powinno.
Kolejna rzecz – refaktoryzacja tego samego kodu różnym osobom może wyjść zupełnie inaczej. Zazwyczaj jednak jeden z wyników będzie lepszy niż pozostałe. Może się jednak zdarzyć i tak, że otrzyma się remis – proszę cały czas pamiętać, że chodzi o optymalny kod dla danych warunków brzegowych czyli potrzeb projektu. Ja sam, przystępując do pisania tego cyklu, miałem już gotowy zestaw kolejnych kroków refaktoryzacji – okazało się jednak, że w pewnym momencie przestał on być wystarczająco dobry i końcowy rezultat rozminął się z tym otrzymanym przed napisaniem cyklu. Trzeba zresztą przyznać, że był to niejako szkic – nie posiadał bowiem w ogóle testów jednostkowych – i to jest bardzo dobry argument na to, że reafaktoryzacja bez testów, to jak człowiek bez głowy ;).
Patrząc na kod pierwotny i końcowy rezultat, kontestatorzy mogą stwierdzić, że przedtem wszystko było w jednym miejscu i można się było szybciej zorientować, o co chodzi. Obecnie ten spójny zapis został rozbity na wiele kawałków i weź tu teraz człowieku dojdź do tego, jak to jest czytane, jak parsowane. Owszem, kod rozszedł się po wielu plikach, ale to nie znaczy, że nic już nie wiadomo. Wiadomo, ale na różnych poziomach szczegółowości. Metoda przetwarzania pliku (Process()) demonstruje, w jaki sposób odbywa się sam odczyt, nie wnika w to, jak jest interpretowana odczytana zawartość. Poprzednio, aby się tego dowiedzieć, trzeba było także dowiedzieć się, jak treść jest interpretowana. Obecnie zainteresowany sposobem interpretacji nie musi wnikać w to, jak plik jest odczytywany – nie zaprząta mu to głowy, może sobie wg ustalonego szablonu (jakim jest interfejs IValuesExtractor) implementować kolejne interpretery i nie martwić się o to czy zostaną właściwie użyte – poprawne użycie (w kontekście importu) jest pewne. To tak jak z nakrętką – będzie pasować tylko na konkretną śrubę i dokręcić będzie ją można tylko konkretnym kluczem.
A skoro już mowa o podziale na osobne pliki – taki podział jest jak najbardziej wskazany. Rzadko obecnie tworzy się programy indywidualnie – zazwyczaj robi to zespół. Im mniejszy plik – taki, który zawiera tylko jedną klasę – tym mniejsze prawdopodobieństwo, że więcej niż jedna osoba będzie musiała go zmieniać w tym samym momencie. Wielkość pliku wynika z wielkości klas – te zaś są małe nie dlatego, że starano się je takimi uczynić, są takie w wyniku wydzielenia kodu – jest w nich go tylko tyle ile trzeba. Oczywiście będzie i tak, że klasy będą duże (więc również i pliki), ale przy dobrej separacji kodu także w tych klasach będzie tylko to co trzeba – wystarczy, aby spełniały zasadę pojedynczej odpowiedzialności.
Pozostając w temacie wydzielania kodu, warto zauważyć, że był on nieodłącznym procesem refaktoryzacji. Już jej pierwszy krok – wydzielenie kodu do Method Object pozwoliło pominąć jego obecność w klasie UI, która mogła odtąd żyć swoim życiem (czytaj: zmieniać się) – o ile wykorzystywała metody z klasy Method Object. W tym samy czasie można było przeprowadzać na kopii Method Object refaktoryzację, w wyniku której wydzielano kolejne fragmenty kodu, itd. Koniec końców wywołania w UI właściwie się nie zmieniły, a to co się pod nimi kryło, zmieniło się diametralnie.
Interesujące jest też – w kwestii wydzielania kodu – że klasy pochodne klasy FileOfValuesReader czyli TextFileOfValuesReader i BinaryFileOfValuesReader powstały nie wskutek przemyśleń typu: „o mamy dwa typy plików tekstowe i binarne, to muszą im odpowiadać dedykowane klasy”, bo choć rzeczywiście istnieje taki podział plików (ze względu na strukturę), to klasy te powstały wyłącznie dlatego, że nie dało się dla nich stworzyć klasy wspólnej. Okazało się, że mamy zmienność (zmienia się sposób odczytu) i zmienność ta została zhermetyzowana (ukryta) w dwóch dedykowanych klasach.
Na koniec przyjrzyjmy się jeszcze raz niektórym fragmentom kodu, ot choćby użyciu interfejsu IFileSelector (w metodach klasy Imports). Choć pole prywatne filteredFileSelector jest typu IFilteredFileSelector, to metoda SelectFile() zwraca interfejs IFileSelector. Jest to bowiem wystarczający interfejs na potrzeby konstruktorów pochodnych klasy FileOfValuesReader. Samo pole filteredFileSelector musi być tego typu, aby można było zdefiniować wymagany filtr, ale potem można po prostu korzystać z samych podstawowych mechanizmów wyboru pliku.
Czy inspekcja kodu jakiej dokonałem po weekendzie, przyniosła jakieś rezultaty? Owszem. Przekształcając bowiem początkowy kod, niechcący dokonałem uproszczeń. Pierwotna klauzula using zabezpieczała otwarty plik przed brakiem zamknięcia. Obecny kod tego nie realizuje. Czy można byłoby to jakoś wyłapać bez inspekcji kodu? Owszem – wystarczyłby odpowiedni test sprawdzający czy plik jest zamykany niezależnie od wystąpienia wyjątku (tak, takie testy też powinny być utworzone). Koniec końców rzeczona metoda będzie wyglądać następująco (dodałem try i uwzględniłem w niej uwagi zgłoszone w komentarzach):
public void Process() { Open(); try { if (Size > 0) { InitializeProgress(); while (SuccessfulReading()) { Extract(); Transfer(); RefreshProgress(); } } } finally { Close(); } } private bool SuccessfulReading() { Read(); return (Readed > 0); }
Na koniec muszę jeszcze wywiązać się z obietnicy danej na początku wpisu, czyli wyjaśnić pewien mankament związany z interfejsem użytkownika. Otóż w pierwotnym rozwiązaniu tak naprawdę nie działał pasek postępu – tj. nie widać było postępu, a jedynie jego końcowy wygląd – w pełni wypełniony pasek. Wynika to z faktu, że interfejs na czas importu jest de facto zamrożony. Ponieważ jednak naszym celem nie była refaktoryzacja funkcjonowania interakcji z użytkownikiem, ten problem pozostał nierozwiązany. Może to być cel kolejnej refaktoryzacji, ale to być może innym razem (zainteresowanym podsunę tylko, że trzeba będzie dokonywać importu w wątku lub – gorsze rozwiązanie – w wątku odświeżać pasek postępu).
Pozostało już tylko opublikować kompletne rozwiązanie (solution). Jest ono zorganizowane następująco: pierwszy z projektów to oryginalny kod (projekt Oryginal), kolejne projekty przedstawiają kolejne kroki refaktoryzacji (Phase#) oraz testy niektórych z nich (Phase#Test). Wynik refaktoryzacji znajduje się w projektach Refactored i RefactoredUI (projekt interfejsu użytkownika wykorzystujący zrefaktoryzowany kod).