Backend

Czysty kod, czyli jaki? 4 zasady czystego kodu w C#

Każdy programista kiedyś był lub będzie w sytuacji, gdzie musi naprawić w czyimś kodzie jakiś błąd. Otworzy wtedy plik w ulubionym IDE, przeczyta kod i… nic z niego nie zrozumie. Przeczyta go więc jeszcze raz i pomyśli, że to bez sensu, kompletnie niezrozumiałe! Jeszcze raz — ok, to chyba działa w taki sposób… Powoli i do przodu, uda mu się naprawić błąd. Zapisze, zamknie i zapomni o tym, co widzieliśmy. Czy musi tak być?

Piotr Penar. Game Developer specjalizujący się w aplikacjach i grach VR oraz AR. Swoją przygodę z tworzeniem gier zaczynał od Unity3D w 2014 roku, od niedawna pracuje również w Unreal Engine. Założyciel Pixel Perfect – studia VR & AR zlokalizowanego w Gliwicach. Szczególnie zainteresowany kwestiami czystego kodu, generacji proceduralnej oraz game designem.


Artykuł ten jest poświęcony pisaniu czystego kodu. Omawia podstawowe zasady, którymi powinien kierować się programista podczas pisania swojego kodu tak, aby był on również zrozumiały dla innych. Jest to coś, czego uczelnie wyższe często nie uczą, a co jest wymagane w pracy w IT, ponieważ nic tak nie mówi o profesjonalizmie programisty jak czysty, zrozumiały kod. Poprzez poświęcanie części swojej uwagi na to jak piszemy oraz w jakim stanie utrzymujemy nasz kod, można w łatwy sposób uniknąć wielu przyszłych problemów wynikających z np. ze złego nazewnictwa.

Zasada 1: Jedna konwencja na cały projekt

Reguła ta jest dość prosta, ale często pomijana w hobbystycznych projektach. Choć trzeba przyznać, że czasami nawet w komercyjnych projektach konwencja zmienia się w trakcie powstawania programu. Zdarza się, że programiści nie są poinformowani o istnieniu konwencji, więc zaczynają pisać kod “po swojemu”, co w miarę rozrastania się projektu prowadzi do coraz większych niespójności i problemów.

Częstą praktyką jest też trzymanie się starych konwencji “w imię zasady”. Jest to problematyczne podejście, ponieważ blokuje rozwój oraz ulepszanie jakości kodu. O ile w przypadku projektów, w których terminy realizacji są bardzo napięte, może być to trudne lub wręcz niemożliwe, jednak należy spróbować przeznaczyć chociaż trochę czasu na uporządkowanie kodu. Na rynku istnieją wspaniałe narzędzia ułatwiające i przyspieszające ten proces: przykładowo, do C# oraz do C++ istnieje rozszerzenie do Visual Studio o nazwie Resharper, które swoją drogą polecam. Pozwala ono na szybki i łatwy refactor całych plików, a nawet solucji.

Konwencja tyczy się również języka. Generalna zasada mówi, że zarówno kod, jak i komentarze, czy też nazwy plików powinny być pisane w pełni po angielsku, nawet jeśli cały zespół jest na przykład polskojęzyczny, albo projekt pisany jest dla samego siebie. Ma to wiele plusów — do takiego kodu może usiąść programista z dowolnego kraju, możemy taki kod spokojnie umieścić w naszym portfolio, łatwiej się go czyta, a przy okazji pisząc wszystko po angielsku ćwiczymy język, który jest nieodzowny w IT — w końcu to nazywanie zmiennych jest jedną z najtrudniejszych czynności, które wykonuje programista.

function getZbiorczaIloscOsob() {

            var liczba_n = $("#liczba_n");
            var liczba_osob = 0;

            var liczba_u_arr = ["liczba_u", "liczba_u_2", "liczba_u_3"];
            var kod_znizki_arr = ["kod_znizki", "kod_znizki_2", "kod_znizki_3"];

            liczba_osob = parseInt(liczba_n.val());

            $.each(liczba_u_arr, function (idx, liczba_) {
                liczba_osob += parseInt($("#" + liczba_).val());
            });

            return liczba_osob;

        }

        function czyWybranoPsa() {

            var isPies = false;
            var bil_dod_psy_arr = [17, 18, 19]; // psa, psa-asystenta, psa-przewodnika

            $(".bilet_dodatkowy").each(function (idx, elem) {
                if (bil_dod_psy_arr.indexOf(parseInt($(elem).val())) > -1) {
                    isPies = true;
                }
            });

            return isPies;
        }

        return  {
            czyNieWybranoCalegoPrzedzialuDlaPsa: czyNieWybranoCalegoPrzedzialuDlaPsa
        }

    } // end BilDodatkoweService

Tak wygląda kod w projekcie o nieustalonej konwencji – oprócz mieszania dwóch języków, poszczególne zmienne mają również różne zasady nazewnictwa. Szczególnie urokliwa jest tutaj zmienna o wdzięcznej nazwie “isPies”. Źródło: Kod zakupu biletu na stronie pkp intercity

Zasada 2: Odpowiednie nazewnictwo

Skoro już przy nazwach jesteśmy — zapewne każdy z nas miał okazję pracować przy kodzie innej osoby i nie do końca rozumiał co robi dana metoda, lub też co dokładnie przechowuje dana zmienna. Nazwy zmiennych typu “time”, “service”, “mass” , “numberOfThings” etc. są dość popularnym i niekoniecznie pozytywnym zjawiskiem. Posługując się wcześniej podanym przykładem zmiennej o nazwie “time” możemy zapytać “czas, ale czego? Od początku istnienia klasy? Od początku jego zliczania gdzieś w kodzie? A może jest to czas do jakiegoś wydarzenia?“.

Czysty kod to taki, który czyta się jak książkę — osoba czytająca dobry kod nie musi się zastanawiać nad tym, co oznacza dana nazwa, co robi dana metoda czy też klasa. Nazwy zmiennych, metod oraz struktur nie powinny zostawiać wątpliwości do czego służą. Nazwy typu gi, gm, rb, a, b, skróty nazw czy też jednosłowne oszczędzą parę znaków w kodzie, ale kosztem czytelności. Dlatego proponuję utrzymywanie nazw jako zestawu 2-4 słów, które opisują cel danego elementu w kodzie. Są oczywiście wyjątki od tej reguły, jak np. iteratory w pętli for, ale myśl, która powinna nam towarzyszyć podczas nazywania poszczególnych elementów to:

Nazwa zmiennej, funkcji lub klasy powinna w jak najkrótszy, najprostszy, a jednocześnie dokładny sposób tłumaczyć jej przeznaczenie.

Brzmi łatwo, ale tak naprawdę jest to trudna sztuka, która wymaga uwagi podczas pisania kodu. Są ludzie, którzy mówią wprost — dobra nazwa świadczy o dobrej organizacji, przemyślanej strukturze programu oraz o dobrych nawykach podczas pracy nad kodem.

Zasada 3: Jedna Klasa/Metoda = Jedna odpowiedzialność

Bardzo często zdarza się, że początkujący programiści, podejmując się jakiegoś projektu, piszą jedną główną klasę, która to owija w sobie całą logikę aplikacji — jest to oczywiście podejście, które bardzo utrudnia usuwanie ewentualnych błędów i zmniejsza możliwości łatwego rozwijania kodu. Bardzo długie i wielofunkcyjne klasy znacznie zmniejszają też czytelność naszego programu.

Jeśli podejrzewamy, że klasa lub też metoda robi za dużo rzeczy na raz — można bardzo łatwo to sprawdzić. Wystarczy spróbować opisać co klasa robi. Przykładowo “Klasa zarządza inicjalizacją podsystemów, pobiera informacje o aktualizacjach, oraz zarządza procesem aktualizacji aplikacji“. Taki opis od razu mówi nam, że coś jest nie tak — mamy 3 obowiązki w 2 kategoriach (inicjalizacja oraz aktualizacje). Najłatwiejszym rozwiązaniem będzie oddzielenie mechanizmu aktualizacji do osobnej klasy, a jeśli to wciąż będzie się nam wydawać za dużo, możemy oddzielić również pobieranie informacji o aktualizacjach od samego procesu aktualizacji.

Oczywiście nie należy popadać ze skrajności w skrajność. Niektóre mechanizmy w naszym programie mogą rzeczywiście być na tyle krótkie i proste, że nie ma sensu ich przenosić do osobnych klas czy też systemów. Chodzi nam w końcu o zwiększenie czytelności kodu programu, a nie wydzielanie podsystemów “na siłę”.

Zasada 4: Nie każ mi myśleć

Oprócz wydzielenia odpowiedzialności z klas oraz metod, powinniśmy również zwrócić uwagę na to, czy możemy wydzielić część kodu do osobnych metod. Często podczas tworzenia instrukcji warunkowych w naszym programie zdarza się, że warunek, który stworzymy składa się z 2 lub więcej wyrażeń boolowskich. Przy wyższych stopniach skomplikowania, problem pojawia się wtedy, kiedy ktoś próbuje zrozumieć, w jakich okolicznościach dany warunek może mieć wartość True. O ile warunek złożony z dwóch części jest łatwy do zrozumienia, jakiekolwiek dodatkowe wyrażenie logiczne powoduje, że zrozumienie jego działania staje się znacznie trudniejsze i zmniejsza jego czytelność podczas czytania kodu.

Zamiast tego znacznie lepiej wyekstraktować wyrażenie logiczne do metody, która będzie zwracać jego wartość — wtedy metodę możemy odpowiednio nazwać, a wyrażenie z zagadki, staje się elementem kodu o jasno określonym znaczeniu i roli. Oprócz tego, tak wyekstraktowanego wyrażenia możemy użyć w innych miejscach naszego kodu bez potrzeby powtarzania się.

Przed ekstrakcją kodu możemy mieć problem ze zrozumieniem kiedy warunek jest spełniony, oraz tak naprawdę, co to wyrażenie oznacza:

private void foo()
{
    if (isDataReady || currentLerpValue > 0.8f && bar.isActive)
   {
       //do calculations...
   }
}

Natomiast tak może wyglądać kod po ekstrakcji warunku do osobnej metody:

private void foo()
{
   if (IsLoadingComplete())
   {
       //do calculations...
   }
}

private bool IsLoadingComplete()
{
   return isDataReady || currentLerpValue > 0.8f && bar.isActive;
}

Od razu wiadomo od czego zależy dalsze wykonywanie metody foo() — od tego czy ładowanie (pewnych danych) jest zakończone. Poza tym, jeśli w przyszłości potrzebowalibyśmy sprawdzać ten sam warunek w przyszłości — wystarczy wywołać tę metodę, dzięki czemu unikniemy kopiowania kodu.

Inną rzeczą, która często jest praktykowana przez początkujących programistów, jest tworzenie bardzo dużej ilości zagnieżdżonych bloków kodu — np. warunek w warunku w pętli w pętli w metodzie. Taka struktura programu nie sprzyja jego czytaniu i zrozumieniu, a wręcz powoduje, że nasz kod jest bardziej podatny na różnego rodzaju błędy wynikające z złego umieszczenia klamry zamykającej dany segment. Aby tego uniknąć wystarczy wyekstraktować kod, który jest zagnieżdżony więcej niż 3 razy (nie licząc klamr metody, w której się on znajduje), do osobnej metody.

Inną strategią, którą możemy zastosować jest odwrócenie zależności — przykładowo, jeśli nasz kod wygląda następująco:

private void foo()
{
   if (bar != null)
   {
       if (bar.value == True)
       {
           // do calculations
       }
   }
}

Możemy go zapisać w inny sposób:

private void foo()
{
   if (bar == null)
   {
       return;
   }

   if (bar.value == True)
   {
        // do calculations
   }
}

Dzięki tym metodom, w miarę rozrastania się kodu oraz metod, utrzymamy niski stopień zagnieżdżenia poszczególnych elementów kodu.

Podsumowanie

Sposobów na poprawienie czytelności naszego kodu jest wiele — w tym artykule omówiliśmy tylko kilka z nich. Zanim pisanie czystego kodu wejdzie nam w nawyk, musimy poświęcić temu zagadnieniu trochę uwagi, ale w zamian znacznie podwyższymy jakość kodu, który piszemy na co dzień. Aby nie przegapić następnych zasad dotyczących czystego kodu, odwiedzaj nas regularnie.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/czysty-kod-czyli-4-zasady-czystego-kodu-c/" order_type="social" width="100%" count_of_comments="8" ]