Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!

Refaktoryzacja – jak tworzyć krótsze funkcje i czytelniejszy kod

Poradnik dla początkujących

Jak podzielić kod tworzony linijka po linijce? Podczas nauki programowania łatwiej nam zrozumieć kolejne funkcjonalności, gdy piszemy kod wers po wersie. W pracy programisty jednak stawiamy na czytelność i reużywalność. Dzięki temu materiałowi wdrożysz się w ideę refaktoryzacji i zaczniesz tworzyć lepszy kod.

Spis treści

 Co to jest refaktoryzacja

To wprowadzanie w kodzie ulepszeń, które nie powodują zmiany działania (funkcjonalności) programu. Nie obejmuje więc dopisywania rozwiązań, lecz jedynie edytowanie kodu w taki sposób, by był on np. bardziej zrozumiały dla programisty, szybciej wykonywany czy łatwiejszy w obsłudze (w utrzymywaniu, rozszerzaniu i ponownym użyciu).

Czy zmiana sposobu usuwania elementu ze strony (np. przycisku „usuń” na opcję przeciągnięcia elementu do kosza) to refaktoryzacja? Nie, ponieważ zmienia się funkcjonalność.

Czy zamiana pętli for na metodę forEach() to refaktoryzacja? Tak, jeśli przyczynia się to do ulepszenia kodu, np. poprawia czytelność.

 Założenia tego projektu

Artykuł jest przeznaczony dla osób początkujących (znających podstawy JavaScriptu), w związku z czym:

  • rozwiązanie jest proste i krótkie, aby nie zakłócać głównego przekazu
  • przy refaktoryzacji skupiamy się na podziale kodu na mniejsze części, poprawie jego czytelności i reużywalności
  • kod zawiera komentarze tylko na potrzeby zrozumienia projektu – nie zaleca się stosowania komentarzy w rozwiązaniach komercyjnych, chyba że nie ma innego wyjścia.

Projekt zakłada wykonanie dwóch zadań na podstawie danych miast i województw powiązanych numerami ID.

  1. Stworzyć spis treści:
    1. województwa mają w sobie zagnieżdżone miasta
    2. elementy spisu przekierowują do sekcji (tworzonych w drugiej części zadania).
  2. Stworzyć sekcje:
    1. województwa to osobne elementy <articles />
    2. miasta są potomkami województwa i tworzą elementy listy <ul />
    3. nazwy województw i miast to odpowiednio nagłówki H3 i H4
    4. województwa i miasta mają opisy (jeśli opisy istnieją w danych).

Jeśli jeszcze trudno Ci to sobie wyobrazić, możesz podejrzeć gotowe rozwiązanie dzięki GitHub Pages. Cały kod natomiast znajdziesz w repozytorium (kod po refaktoryzacji jest w pliku app-after.js – jeżeli chcesz zobaczyć jego działanie, pamiętaj o zmianie nazwy pliku w index.html w tagu <script />).

 Struktura danych

Najpierw zapoznajmy się z danymi, na podstawie których ma powstać nasze rozwiązanie. Jest to osobny plik assets/list.js, który zawiera tablicę obiektów i ją eksportuje.

Obiekty przechowują dane województw i miast. Zauważ, że obiekty miast nie są zagnieżdżone w obiektach województw. Zalety płaskiej struktury omawiałem na przykładzie magazynu.

Dodatkowo każdy z elementów posiada swoją pełną nazwę we właściwości text oraz nazwę dla odniesień (ref), która posłuży do stworzenia linkowania ze spisu treści do konkretnych sekcji.

Niektóre obiekty, lecz nie wszystkie, posiadają też właściwość description z opisem miejsca.

 Rozwiązanie przed refaktoryzacją

Chcemy, aby nasz kod JavaScript uruchamiał się dopiero po załadowaniu drzewa DOM (kodu HTML z naszego pliku index.html), dlatego umieszczamy go w funkcji przypisanej do eventu DOMContentLoaded.

Uwaga: kod po refaktoryzacji jest w pliku app-after.js – jeżeli chcesz zobaczyć jego działanie, pamiętaj o zmianie nazwy pliku w index.html w tagu <script />.

 Część 1 – spis treści dla województw z zagnieżdżonymi miastami

Uwaga: spis treści dla płaskiej struktury danych można stworzyć z pomocą rekurencji lub pętli do…while, lecz my skupimy się na rozwiązaniu, które powinno być łatwiejsze w zrozumieniu dla osób początkujących.

Województwa mają ustawioną właściwość parentId na null, natomiast miasta w tym miejscu przechowują ID rodzica – województwa. Właśnie z tych wartości skorzystamy do posortowania danych:

document.addEventListener('DOMContentLoaded', function () {
    // porządkuję dane (podział na rodziców i dzieci)
    const voivodeships = list.filter(function (element) {
        return element.parentId === null;
    });
    const cities = list.filter(function (element) {
        return element.parentId !== null;
    });
    // ...
}

W ten sposób uzyskujemy dwie tablice obiektów – z danymi województw i z danymi miast – które wykorzystamy zarówno w pierwszej, jak i drugiej części zadania.

Spis treści tworzymy teraz za pomocą dużego fragmentu kodu. Poniżej w komentarzach opisałem Ci, co po kolei się w nim dzieje:

// część 1 zadania

//wyszukuję docelowy spis treści (patrz: plik index.html)
const contentsEl = document.querySelector('.contents');

// wyszukuję miasta dla każdego województwa
voivodeships.forEach(function (voivodeship) {
    const citiesOfVoivodeship = cities.filter(function (city) {
        return city.parentId === voivodeship.id;
    });

    // tworzę element <li> województwa oraz element <a> kierujący do sekcji
    const voivodeshipListItem = document.createElement('li');
    const voivodeshipLink = document.createElement('a');
    voivodeshipLink.innerText = voivodeship.text;
    voivodeshipLink.setAttribute('href', '#' + voivodeship.ref);

    voivodeshipListItem.appendChild(voivodeshipLink);
    contentsEl.appendChild(voivodeshipListItem);

    // jeśli województwo ma miasta, tworzę element <ul>
    if (citiesOfVoivodeship.length > 0) {
        const citiesList = document.createElement('ul');
        voivodeshipListItem.appendChild(citiesList);

        // tworzę elementy <li> oraz <a> dla miast (zwróć uwagę, że tworzymy podobny kod jak dla województwa)
        citiesOfVoivodeship.forEach(function (city) {
            const cityListItem = document.createElement('li');
            const cityLink = document.createElement('a');
            cityLink.innerText = city.text;
            cityLink.setAttribute('href', '#' + city.ref);

            cityListItem.appendChild(cityLink);
            citiesList.appendChild(cityListItem);
        });
    }
});

Jak widzisz, prócz rzeczy charakterystycznych dla województwa, jak wyszukanie przynależnych do niego miast, musimy wykonać działania, które za chwilę powielają się w przypadku miast: stworzenie listy <ul />, dodanie do niej elementów z nazwami i linkami.

 Część 2 – sekcje dla województw z miastami i opisami

Tutaj mamy jeszcze dłuższy blok kodu. Najpierw zobaczmy, jak wygląda tworzenie sekcji dla województw:

// część 2 zadania

//wyszukuję docelowy element z opisami
const descriptionsEl = document.querySelector('.descriptions');

// dla każdego województwa dodaję tytuł i opis oraz szukam jego miast
voivodeships.forEach(function (voivodeship) {
    const citiesOfVoivodeship = cities.filter(function (city) {
        return city.parentId === voivodeship.id;
    }); // (powtórzony kod!)

    // tworzę kontener dla województwa i jego miast
    const container = document.createElement('article');

    // tworzę element <h3> dla województwa
    const parentElTitle = document.createElement('h3');
    parentElTitle.innerText = voivodeship.text;

    // nadaję ID zgodne z linkiem, który do tego elementu kieruje
    parentElTitle.setAttribute('id', voivodeship.ref);

    //dodaję tytuł do kontenera
    container.appendChild(parentElTitle);

    // jeśli opis istnieje w bazie, dodaję go pod tytułem
    if (voivodeship.description) {
        const parentElDesc = document.createElement('p');
        parentElDesc.innerText = voivodeship.description;

        // umieszczam opis zaraz pod tytułem
        parentElTitle.insertAdjacentElement('afterend', parentElDesc);
    }

    // dodaję kontener do rodzica na stronie
    descriptionsEl.appendChild(container);

    // ...
}

Zauważ, że tworzenie nagłówków z odpowiednimi ID (żeby link ze spisu treści kierował właśnie do tej sekcji) oraz dodawanie opisów, jeśli istnieją, za chwilę powieli się w kodzie dla miast. Fragmenty takie zaznaczyłem komentarzem (powtórzony kod!).

voivodeships.forEach(function (voivodeship) {
    //...
    if (citiesOfVoivodeship.length > 0) {
        // tworzę element <ul> dla miast z opisami
        const citiesList = document.createElement('ul');
        // dodaję tę listę do województwa - ustawi się jako ostatnie dziecko
        container.appendChild(citiesList);

        citiesOfVoivodeship.forEach(function (city) {
            // tworzę element <li> dla każdego miasta
            const cityLiEl = document.createElement('li');

            citiesList.appendChild(cityLiEl);

            // tworzę tytuł <h4> razem z ID (powtórzony kod!)
            const cityElTitle = document.createElement('h4');
            cityElTitle.innerText = city.text;

            cityElTitle.setAttribute('id', city.ref);

            // dodaję tytuł do <li>
            cityLiEl.appendChild(cityElTitle);

            // jeśli opis istnieje w bazie, to tworzę go na stronie (powtórzony kod!)
            if (city.description) {
                const cityElDesc = document.createElement('p');
                cityElDesc.innerText = city.description;

                // umieszczam opis zaraz pod tytułem
                cityElTitle.insertAdjacentElement('afterend', cityElDesc);
            }
        });
    }
});

 Problemy projektu przed refaktoryzacją i możliwe rozwiązania

  • Nie wiemy, za co ten kod odpowiada, póki nie zaczniemy wgryzać się w niego linijka po linijce.
    Rozwiązanie: to, co jest istotne (zmienne i nadrzędne funkcje) przenosimy na górę kodu.
  • Powielamy kod.
    Rozwiązanie: wyodrębnić go do osobnych funkcji, by móc używać wielokrotnie.
  • Mamy bardzo długie funkcje, przez co kod jest nieczytelny Rozwiązanie: przenieść go do mniejszych funkcji i nazwać je tak, by opisywały to, co się dzieje.

 Refaktoryzacja krok po kroku

Zaczynaliśmy od tego, że cały kod JS umieściliśmy w funkcji uruchamianej po załadowaniu drzewa DOM (event DOMContentLoaded). Teraz zostawimy tu tylko kluczowe „informacje”, a resztę przeniesiemy do mniejszych funkcji.

 Przeniesienie kodu do osobnych funkcji

Nasz projekt dzieli się na dwa zadania: wygenerowanie spisu treści oraz stworzenie sekcji. Możemy wykorzystać to do nazwania dwóch największych funkcji, dzięki czemu od razu będzie widać, za co odpowiada kod w naszym pliku.

document.addEventListener('DOMContentLoaded', function () {
    // porządkuję dane (podział na rodziców i dzieci)
    const voivodeships = list.filter(function (element) {
        return element.parentId === null;
    });
    const cities = list.filter(function (element) {
        return element.parentId !== null;
    });

    // część 1 zadania
    createTableOfContents(voivodeships, cities);

    // część 2 zadania
    createSections(voivodeships, cities);
});

 Poprawa czytelności kodu oraz nazywanie funkcji i zmiennych

Spójrz teraz na naszą nową funkcję createTableOfContents(), odpowiedzialną za stworzenie spisu treści. Czy jesteś w stanie zrozumieć jej rolę bez pomocy komentarzy?

// przenoszę część 1 zadania do osobnej funkcji
function createTableOfContents(voivodeships, cities) {
    //wyszukuję docelowy spis treści
    const contentsEl = document.querySelector('.contents');

    // wyszukuję miasta dla każdego województwa i tworzę z nich elemenety <li>
    voivodeships.forEach(function (voivodeship) {
        const citiesOfVoivodeship = getCitiesOfVoivodeship(cities, voivodeship);

        // tworzę element <li> województwa oraz element <a> i <ul> wewnątrz niego (jeśli ma miasta)
        const voivodeshipListItem = createLinkedNavItem(
            voivodeship.text,
            voivodeship.ref
        );
        contentsEl.appendChild(voivodeshipListItem);

        if (citiesOfVoivodeship.length > 0) {
            createNestedList(citiesOfVoivodeship, voivodeshipListItem);
        }
    });
}

Ja przeczytałbym ją w ten sposób (podaję też numery linii):

  • 22 – wyszukaj element HTML spisu treści
  • 25 – dla każdego województwa:
    • 26 – uzyskaj jego miasta
    • 27 – stwórz podlinkowany element nawigacji, który jest elementem listy
    • 33 – dodaj ten element do spisu treści
    • 35-37 – jeśli uzyskano miasta danego województwa, to stwórz z nich zagnieżdżoną listę.

Zauważyłeś może, że nie byłem w stanie z tego wywnioskować wszystkich informacji o kodzie. Na przykład komentarz tworzę element <li> województwa oraz element <a> i <ul> wewnątrz niego (jeśli ma miasta) zawiera wiadomości o rodzaju powstałych elementów HTML.

Czy to źle, że nie wyczytałem tego z funkcji createTableOfContents()? Nie! Jeżeli będę potrzebował takich szczegółów, to przejdę do funkcji createLinkedNavItem() oraz createNestedList() i dowiem się, co się pod nimi kryje. W VS Code do deklaracji danej funkcji przejdziesz szybko za pomocą skrótu Ctrl + lewy przycisk myszy (klikając na nazwę funkcji).

W tym miejscu skupiamy się na czytelności, więc poproszę teraz Ciebie, abyś wrócił do tej części kodu sprzed refaktoryzacji. Czy jest on dla Ciebie tak samo czytelny i jesteś w stanie w miarę szybko zrozumieć, o co w nim chodzi? Ja nie 😉

Z krótszymi funkcjami użytymi w createTableOfContents() zapoznasz się w pliku app-after.js.

 Niepowielanie i reużywanie kodu (zasada DRY)

Omówimy to na przykładzie drugiego zadania, które przenieśliśmy do funkcji createSections(). Częściowo korzystamy w niej z rozwiązań (mniejszych funkcji) stworzonych na potrzeby pierwszego zadania: jest to uzyskanie miast należących do danego województwa (getCitiesOfVoivodeship) oraz szybsze tworzenie elementów należących do podanego rodzica (createElementOfParent) – to ostatnie rozwiązanie mogłeś podejrzeć przy zapoznawaniu się z refaktoryzacją pierwszego zadania w pliku app-after.js.

// przenoszę część 2 zadania do osobnej funkcji
function createSections(voivodeships, cities) {
    //wyszukuję na stronie docelowy element z opisami
    const descriptionsEl = document.querySelector('.descriptions');

    // dla każdego województwa dodaję tytuł i opis oraz szukam jego miast
    voivodeships.forEach(function (voivodeship) {
        const citiesOfVoivodeship = getCitiesOfVoivodeship(cities, voivodeship); // mam już tę funkcję, więc używam

        // tworzę kontener dla województwa i jego miast – od razu korzystam z funkcji dodającej do rodzica
        const container = createElementOfParent('article', descriptionsEl);

        // tworzenie nagłówków z ID przenoszę do funkcji
        const parentElTitle = createReferenceHeader(
            'h3',
            voivodeship,
            container
        );

        // dodaję opis pod tytułem województwa
        if (voivodeship.description) {
            addDescription(voivodeship.description, parentElTitle);
        }

        // tworzę elementy dla miast, jeśli województwo je posiada
        if (citiesOfVoivodeship.length > 0) {
            createCitiesInfo(citiesOfVoivodeship, container);
        }
    });
}

Mamy też dwie nowe funkcje, które z powodzeniem wykorzystamy zaraz przy tworzeniu informacji o miastach: createReferenceHeader() oraz addDescription(). Zobacz, jak ich ponowne użycie skróciło nam proces pisania funkcji createCitiesInfo():

function createCitiesInfo(citiesOfVoivodeship, parentEl) {
    // tworzę kontener dla listy miast
    const citiesList = createElementOfParent('ul', parentEl);

    citiesOfVoivodeship.forEach(function (city) {
        // tworzę element <li> dla każdego miasta
        const cityLiEl = createElementOfParent('li', citiesList);

        // wykorzystuję stworzoną wcześniej funkcję dla nagłówków z ID
        const cityElTitle = createReferenceHeader('h4', city, cityLiEl);

        // tworzę opis i dodaję go zaraz pod tytułem – wykorzystuję stworzoną funkcję
        if (city.description) {
            addDescription(city.description, cityElTitle);
        }
    });
}

 Projekt po refaktoryzacji – zalety ulepszania kodu

Rozwiązaliśmy problemy wymienione na początku. Dzięki temu:

  • łatwiej się zorientować w działaniu programu, wiemy, za co odpowiada – mamy krótsze fragmenty kodu oraz odpowiednie nazwy funkcji i zmiennych
  • stosujemy się do zasady DRY, czyli nie powielamy kodu tam, gdzie nie jest to potrzebne (czasem czytelność wygrywa z reużywalnością i lepiej coś powtórzyć, niż zmuszać siebie lub innego programistę to szperania w odległych funkcjach)
  • mamy krótsze funkcje, co pozytywnie wpływa na czytelność kodu.

Czy jest to już kod idealny? Nie – zawsze można coś ulepszyć, znaleźć trafniejszą nazwę, wydzielić kod do jeszcze mniejszych funkcji, przenieść część kodu np. do osobnego pliku, klasy, helpera itd.

Tak czy inaczej nasze ulepszone rozwiązanie już teraz ułatwia nam kolejne działania – zapraszam do fragmentu poniżej.

 Łatwe dodanie nowej funkcjonalności do kodu po refaktoryzacji

Chcemy umożliwić użytkownikowi wykreślanie nagłówków województw i miast, z którymi się już zapoznał. W naszym rozwiązaniu sprzed refaktoryzacji musielibyśmy dopisać kod w dwóch miejscach.
Teraz możemy wykorzystać fakt, że wszelkie nagłówki, do których linkuje spis treści, tworzymy w funkcji createReferenceHeader().

Tam dodamy fragment odpowiedzialny za możliwość przekreślania nagłówków:

function createReferenceHeader(headerType, data, parent) {
    // tworzę nagłówek
    const titleEl = createElementOfParent(headerType, parent);
    titleEl.innerText = data.text;

    // dodaję ID
    titleEl.setAttribute('id', data.ref);

    // jeśli chcę dodać coś do wszystkich nagłówków, nie muszę wprowadzać zmian w dwóch miejscach
    titleEl.addEventListener('click', function (e) {
        e.target.style.textDecoration = 'line-through';
    });

    return titleEl;
}

W ramach przećwiczenia refaktoryzacji wydziel ten fragment do osobnej funkcji 🙂

 

Zapewne zauważyłeś, że korzyści z refaktoryzacji się zazębiają: mniejsze, dobrze nazwane funkcje poprawiają czytelność kodu, a jednocześnie umożliwiają ponowne użycie tych rozwiązań w innych częściach projektu lub w innych projektach (mówimy tu np. o elastycznie napisanych klasach czy modułach). Nie spisuj swoich starszych projektów na straty – możesz wykonać ich refaktoryzację i zaprezentować ten proces w swoim portfolio!

Udostępnij ten artykuł:

Mentoring to efektywna nauka pod okiem doświadczonej osoby, która:

  • przekazuje Ci swoją wiedzę i nadzoruje Twoje postępy w zdobywaniu umiejętności,
  • uczy Cię dobrych praktyk i wyłapuje złe nawyki,
  • wspiera Twój rozwój i zwiększa zaangażowanie w naukę.