Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!
⛔ Potrzebujesz wsparcia? Oceny CV? A może Code Review? ✅ Dołącz do Discorda!
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.
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ść.
Artykuł jest przeznaczony dla osób początkujących (znających podstawy JavaScriptu), w związku z czym:
Projekt zakłada wykonanie dwóch zadań na podstawie danych miast i województw powiązanych numerami ID.
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 />).
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.
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 />.
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.
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); } }); } });
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.
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); });
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):
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.
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); } }); }
Rozwiązaliśmy problemy wymienione na początku. Dzięki temu:
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.
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ł:
Potrzebujesz cotygodniowej dawki motywacji?
Zapisz się i zgarnij za darmo e-book o wartości 39 zł!
PS. Zazwyczaj rozsyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu email.
Chcesz zostać (lepszym) programistą i lepiej zarabiać?
🚀 Porozmawiajmy o nauce programowania, poszukiwaniu pracy, o rozwoju kariery lub przyszłości branży IT!
Umów się na ✅ bezpłatną i niezobowiązującą rozmowę ze mną.
Chętnie porozmawiam o Twojej przyszłości i pomogę Ci osiągnąć Twoje cele! 🎯