Zakładana funkcjonalność skończona, projekt działa i co teraz? Po studencku, nie ruszamy jak działa? Oczywiście, że nie. Kolejnym etapem projektu jest refaktoryzacja i tym też się dzisiaj zajmiemy!

Przez ostatnie dziesięć tygodni tworzyłem bibliotekę easyGALib i powoli kończy się mój udział w konkursie Daj się poznać, bo to już przedostatni post. Trzeba więc zbliżać się w kierunku zamykania pierwszej wersji biblioteki, na razie z implementacją dla jednego rodzaju chromosomu, ale z całym gotowym mechanizmem bazowym. Dzisiaj więc zajmiemy się kolejnym etapem pracy nad projektem programistycznym, czyli refaktoryzacją.

Refaktoryzacja

Co to właściwie jest ta refaktoryzacja, o której na studiach to słyszy się tylko w legendach?

Jest to proces poprawy kodu w taki sposób, aby stał się bardziej czytelny, przejrzysty i prostszy w edycji na dalszych etapach pracy. W trakcie refaktoryzacji robimy więc wszystko co w naszej mocy, aby kod czytać jak książkę, w której w każdej chwili, bez większych nakładów pracy, możemy coś zmienić. Warto tutaj też zwrócić uwagę, że w trakcie całego procesu w żaden sposób nie zmieniamy funkcjonalności, a raczej sam kod i zależności w nim występujące.

Dlaczego w ogóle występuje potrzeba refaktoryzacji?

W trakcie pisania kodu zawsze należy się starać stosować zasady SOLID i inne dobre praktyki programowania, jednak nie zawsze od razu coś zobaczymy lub akurat nie będziemy mieć czasu dopracować jakiegoś szczegółu i zostawimy go sobie na później, po czym kompletnie o tym zapomnimy. Warto więc po zakończeniu pewnego etapu prac oglądnąć się za siebie i jeszcze raz przeanalizować, czy czegoś nie można by zrobić jeszcze lepiej.

Na co powinniśmy zwracać uwagę w czasie refaktoryzacji?

Wracając do wcześniej napisanego kodu, powinniśmy zwrócić szczególną uwagę na następujące elementy:

  • Prostota kodu. Czy czegoś nie można zrobić prościej, bez zbędnych kombinacji.
  • Nazewnictwo. Czy nazwy zmiennych i metod faktycznie od razu mówią nam czego dany obiekt dotyczy? Skoro mamy czytać kod jak książkę, to wywołanie metody NewMethod(); nie za wiele nam powie.
  • Powtórzenia. Ja w pisaniu kodu stosuję tutaj zasadę do trzech razy sztuka. Pierwszy raz piszemy fragment kodu – OK. Drugi raz to samo, możemy skopiować. Trzeci raz, stosujemy refaktoryzację, bo to więcej niż pewne, że dany fragment kodu jeszcze gdzieś się pojawi.
  • Zgodność z dobrymi praktykami. Sprawdzamy, czy napisany kod jest zgodny z dobrymi praktykami, czy może warto pozmieniać jakieś zależności, zastosować interfejs zamiast klasy, itp.
  • Struktura i długość kodu. Z kodem jest jak z lekturami w podstawówce, jak widziało się taką grubaśną, to od razu odechciewało się czytać i tutaj to samo – jedna metoda czy klasa nie powinna zajmować setek linijek kodu, jeżeli jesteśmy to w stanie rozbić na kilka mniejszych jednostek. Analogicznie, jeżeli w naszym kodzie występują liczne zagnieżdżenia, warto wyciągnąć ich zawartość do osobnych funkcji, bo inaczej doprowadzimy do programistycznej incepcji. Ja przeważnie stosuję zasadę maksymalnie trzech zagnieżdżeń.
  • Komentarze. Wydaje mi się, że dobrze napisany kod w ogóle nie powinien ich zawierać, bo jest na tyle czytelny, że po prostu obejdziemy się bez nich. Występują jednak czasem sytuacje, gdzie jesteśmy zmuszeni wstawić jakąś linijkę kodu, która niewiele powie komuś, kto będzie czytał nasz kod. W takim przypadku warto zadbać o rozszerzenie informacji na jej temat poprzez odpowiedni komentarz.

Pewnie można tutaj jeszcze dopisać kilka innych rzeczy, jednak te wydaje mi się, że występują najczęstszej. Warto jeszcze tutaj zaznaczyć, żeby w trakcie zmian upewniać się co jakiś czas, czy nie były na tyle drastyczne, że kod przestał działać 😉

Refaktoryzacja easyGALib

Idąc za ciosem, zerknijmy co w czasie refaktoryzacji udało się zmienić w easyGALib. Co prawda przez cały czas pracy nad projektem starałem się nie rozleniwiać i nie zostawiać czegoś na później, ale może akurat coś do poprawy się znajdzie.

public List<IChromosome> CurrentGeneration { get; set; }
public IChromosome BestChromosome { get; set; }
public List<IChromosome> NextGeneration { get; set; }
public List<IChromosome> InitChromosomes { get; set; }
protected Random _rdm;

Powyższy kod moglibyśmy zamienić na taki:

public List<IChromosome> CurrentGeneration { get; set; }
public List<IChromosome> NextGeneration { get; set; }
public List<IChromosome> InitGeneration { get; set; }
public IChromosome BestChromosome { get; set; }

protected Random _rdm;

Po pierwsze mamy teraz porządek w typach właściwości, a po drugie skoro wszystkie listy przechowują chromosomy, które stają się później jakąś generacją, to warto było tutaj ujednolicić nazwy.

Idąc dalej, zobaczmy ciało metody realizującej algorytm genetyczny:

PopulationInit();

while (!IsFinalGeneration(generation))
{
    CalculateFitness();
    SelectChromosomes();
    Crossover();
    Mutate();
    AddBestChromosome();

    generation++;
}

Patrząc na taki kod od razu widać co dzieje się pod spodem. W tym przypadku akurat widzimy samo serce działania algorytmu genetycznego tak, jakbyśmy po kolei przechodzili po bloczkach podanych na początku realizacji projektu. Myślę, że jeżeli ktoś będzie patrzył kiedyś na kod źródłowy, nie będzie musiał się zastanawiać co poeta miał na myśli.

Kolejną zmianą jest wrzucenie metod w odpowiednie regiony. Prywatne grupujemy razem, te odpowiedzialne za implementację danego interfejsu razem i publiczne razem. Tym sposobem zyskujemy na czytelności kodu bez potrzeby scrollowania przez dziesiątki linijek.

Z grubsza to by było na tyle, bo tak jak pisałem wcześniej, wszystkie strukturalne rzeczy starałem się na bieżąco zmieniać. Tym sposobem myślę, że powstał kod biblioteki łatwy i przyjazny do edycji.


Tym sposobem zakończyliśmy pracę nad podstawową wersją biblioteki. Powstały w ten sposób kod będzie punktem wyjścia do implementacji kolejnych rodzajów chromosomów. W następnym poście pewnie przyjdzie już pora na podsumowanie całej pracy 🙂