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 naszej społeczności na Discordzie!
Czy zdarza Ci się tworzyć wiele instrukcji warunkowych? Nie wiesz, jak skracać rozbudowane bloki else-if? Jeśli jedyne, co przychodzi Ci do głowy, to instrukcja switch – koniecznie przeczytaj ten artykuł i dowiedz się, jak ulepszać kod za pomocą obiektów!
Przyjmijmy, że mamy napisać prostą grę, w której dziecko odczytuje kolor i ma wskazać owoc lub warzywo, który jest właśnie tego koloru. Dla uproszczenia rozważmy trzy zestawy:
Za każdym razem, gdy dziecko kliknie dany produkt, sprawdzamy, czy należy on do wskazanej grupy. Na przykład:
📍 Jeżeli chcesz testować poniższe przykłady na żywym kodzie, to zapraszam Cię do pobrania repozytorium color-game-before (działanie gry już teraz możesz sprawdzić tutaj).
W programowaniu stworzenie działającego rozwiązania jest priorytetem. Możesz więc z czystym sumieniem napisać kod na piechotę – tak jak my robimy to poniższej z if-else – a potem dopiero go optymalizować.
Oczywiście jeśli miałbyś 15 kolorów i 100 produktów, to nie zachęcałbym Cię do pisania każdego warunku z osobna, lecz nadal mógłbyś stworzyć na podstawie tych danych kilka linijek.
Dzięki temu poznajesz działanie swojego programu i już na tym etapie możesz wychwycić pierwsze błędy, luki i powtarzające się schematy. Te ostatnie pomogą Ci w stworzeniu bardziej elastycznych rozwiązań.
Teoretycznie możemy więc stworzyć wiele warunków za pomocą if, else if oraz else, np.
function isProductCorrect(color, product) { if (color === 'red' && (product === 'cranberry' || product === 'cherry')) { alert('Hurray!'); } else if ( color === 'yellow' && (product === 'banana' || product === 'melon') ) { alert('Hurray!'); } else if ( color === 'green' && (product === 'cucumber' || product === 'avocado') ) { alert('Hurray!'); } else { alert('Wrong answer'); } }
Program działa, osiągnęliśmy cel. Jednak jakie „zagrożenia” wiążą się z takim rozwiązaniem?
Przy rozbudowywaniu gry o kolejne kolory i produkty utoniemy w ilości danych: będzie je trzeba dopisywać w odpowiednich linijkach, mnożąc warunki. To nie jest dobre również dla wydajności aplikacji.
Przy okazji łamiemy zasadę DRY (Don't Repeat Yourself), ponieważ ciągle powtarzamy ten sam fragment kodu: alert('Hurray!').
Czy z else-if mamy przerzucić się na instrukcję switch? Niekoniecznie – tam również będziemy mieć wiele linijek niepotrzebnego kodu. Spójrz na przykład poniżej:
function isProductCorrect(color, product) { switch (true) { case color === 'red' && (product === 'cranberry' || product === 'cherry'): alert('Hurray!'); break; case color === 'yellow' && (product === 'banana' || product === 'melon'): alert('Hurray!'); break; case color === 'green' && (product === 'cucumber' || product === 'avocado'): alert('Hurray!'); break; default: alert('Wrong answer'); } }
Takie dane najlepiej gromadzić w postaci tablic i obiektów, a potem korzystać z nich za pomocą odpowiednich, wbudowanych w JavaScript metod.
Jak moglibyśmy uporządkować nasze dane? Być może zestawienie kolorów i produktów z początku tego artykułu już nasunęło Ci pewien pomysł.
Spróbujmy z tablicami:
function isProductCorrect(color, product) { const redProducts = ['cranberry', 'cherry']; const yellowProducts = ['banana', 'melon']; const greenProducts = ['cucumber', 'avocado']; if (color === 'red' && redProducts.includes(product)) { alert('Hurray!'); } else if (color === 'yellow' && yellowProducts.includes(product)) { alert('Hurray!'); } else if (color === 'green' && greenProducts.includes(product)) { alert('Hurray!'); } else { alert('Wrong answer'); } }
Co zyskaliśmy?
Czego nadal nie mamy? Optymalizacji dodawania nowych kolorów. Każdy nowy kolor to potrzeba stworzenia nowej zmiennej z tablicą produktów i dopisania warunku w funkcji isProductCorrect(). Tym samym nadal łamiemy zasadę DRY – nie pozbyliśmy się powtarzania alert('Hurray!').
Spróbujmy dopracować nasze rozwiązanie za pomocą obiektu.
function isProductCorrect(color, product) { const products = { red: ['cranberry', 'cherry'], yellow: ['banana', 'melon'], green: ['cucumber', 'avocado'], }; if (products[color].includes(product)) { alert('Hurray!'); } else { alert('Wrong answer'); } }
Wygląda to o wiele lepiej!
Co zyskaliśmy?
Moglibyśmy zrobić jeszcze coś, dzięki czemu zaplusujemy w oczach rekrutera technicznego oraz sami ułatwimy sobie i naszym współpracownikom życie.
Zasada pojedynczej odpowiedzialności mówi o tym, by funkcja, klasa czy metoda odpowiadała za jedną rzecz. Przy okazji dobrze jest zadbać o adekwatne nazewnictwo funkcji i zmiennych.
Zwróć uwagę, że w tej chwili funkcja isProductCorrect() jest odpowiedzialna za wyświetlanie alertów, przy czym jej nazwa sugeruje, że powinniśmy spodziewać się zwrotu wartości true lub false. Nazwa funkcji to pytanie „Czy produkt jest poprawny?”, a odpowiedź to „tak” lub „nie”.
Zmodyfikujmy więc kod w taki sposób, aby funkcja isProductCorrect() rzeczywiście była odpowiedzialna tylko za sprawdzanie poprawności wyboru produktu i zwracała true lub false. Logikę wyświetlania alertów przeniesiemy do funkcji przypisanej do zdarzenia click (jak widzisz, nasłuchiwanie zdarzenia kliknięcia jest ustawione na listę produktów).
function isDOMContentLoaded() { const colors = ['red', 'green', 'yellow']; const randomColor = chooseRandomColor(colors); displayRandomColor(randomColor); // pobieramy listę produktów z drzewa DOM const productsList = document.querySelector('.productsList'); productsList.addEventListener('click', e => { const product = e.target.innerText; if (isProductCorrect(randomColor, product)) { alert('Hurray!'); } else { alert('Wrong answer'); } }); } // (...) function isProductCorrect(color, product) { const products = { red: ['cranberry', 'cherry'], yellow: ['banana', 'melon'], green: ['cucumber', 'avocado'], }; return products[color].includes(product); //zwracamy true lub false }
Gotowe: funkcja odpowiada za jedną rzecz i zachowuje się tak, jak sugeruje to jej nazwa.
Co jeszcze możemy zrobić? Teoretycznie możemy skrócić instrukcję if-else za pomocą operatora warunkowego, lecz nie jest to niezbędne.
// instrukcja warunkowa if if (isProductCorrect(randomColor, product)) { alert('Hurray!'); } else { alert('Wrong answer'); } // operator warunkowy isProductCorrect(randomColor, product) ? alert('Hurray!') : alert('Wrong answer');
📍 Cały kod po zmianach znajdziesz na repozytorium color-game-after.
Aby nie utrudniać zrozumienia kodu, nie zabezpieczałem kalkulatora przed błędnym wprowadzeniem danych (możesz więc dzielić przez 0 czy uzyskać wynik, nawet jeśli nie wpiszesz danych lub wpiszesz np. litery).
Skupmy się na działaniach:
📍 Jeżeli chcesz testować poniższe przykłady na żywym kodzie, to zapraszam Cię do pobrania repozytorium calculator-before (jego działanie już teraz możesz sprawdzić tutaj).
Potrzebujemy więc logiki odpowiedzialnej za obliczenia. Musimy ją też uzależnić od wyborów użytkownika. Pierwsze działające rozwiązanie mogłoby wyglądać tak:
function getResult(e) { e.preventDefault(); // zmienne number1 i number2 przechowują wartości odebrane od użytkownika: const number1 = Number(e.target.elements.number1.value); const number2 = Number(e.target.elements.number2.value); // z pola formularza o nazwie "operation" pobieramy informację o typie operacji: const operationType = e.target.elements.operation.value; let result; if (operationType === '+') { result = number1 + number2; } if (operationType === '-') { result = number1 - number2; } if (operationType === '*') { result = number1 * number2; } if (operationType === '/') { result = number1 / number2; } // wyświetlamy rezultat użytkownikowi: const resultEl = document.querySelector('.result'); resultEl.innerText = result; }
Jakie są minusy tego rozwiązania?
W przykładzie z grą nasze dane umieściliśmy w obiekcie: właściwościom, czyli kolorom, przypisaliśmy wartości w postaci stringów – nazw owoców i warzyw. Czy możemy użyć obiektu w przypadku funkcji kalkulatora? Tak. Zdecydowanie uprości nam to funkcję getResult() i uczyni nasze rozwiązanie bardziej elastycznym.
Najpierw tworzymy obiekt o nazwie operations. Właściwościami są wartości inputów („przycisków” kalkulatora), a wartościami – nazwy funkcji, które wykonują obliczenia. Funkcje te zaraz napiszemy.
WAŻNE: Zwróć szczególną uwagę na to, że w obiekcie mamy tylko nazwy funkcji, a nie ich wywołanie, np. add().
const operations = { '+': add, '-': subtract, '*': multiply, '/': divide, };
Ciała funkcji mamy tak naprawdę niemal gotowe. Wystarczy z naszych instrukcji warunkowych przenieść działania przypisywane do zmiennej result.
const operations = { '+': add, '-': subtract, '*': multiply, '/': divide, }; function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } function multiply(a, b) { return a * b; } function divide(a, b) { return a / b; }
Czas użyć powyższego kodu i usprawnić funkcję getResult(). Zauważ, że nie musimy teraz nawet korzystać z żadnej metody wbudowanej w JavaScript. Wystarczy, że odwołamy się do przechowywanej w obiekcie nazwy funkcji i tę funkcję wywołamy (stąd użycie nawiasów okrągłych i przekazanie argumentów).
Schemat ten wygląda tak:
obiekt[właściwość]()
W naszym przykładzie zastosowaliśmy go tutaj:
const result = operations[operationType](number1, number2);
Zobacz, jak bardzo skróciło to naszą funkcję getResult()! Cały blok instrukcji warunkowych zamieniliśmy na jedną linijkę kodu. Co więcej, linijka ta się nie zmieni, nawet jeśli rozbudujemy kalkulator o dodatkowe operacje!
const operations = { '+': add, '-': subtract, '*': multiply, '/': divide, }; function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } function multiply(a, b) { return a * b; } function divide(a, b) { return a / b; } function getResult(e) { e.preventDefault(); // zmienne number1 i number2 przechowują wartości odebrane od użytkownika: const number1 = Number(e.target.elements.number1.value); const number2 = Number(e.target.elements.number2.value); // z pola formularza o nazwie "operation" pobieramy informację o typie operacji: const operationType = e.target.elements.operation.value; const operationFunc = operations[operationType]; if(typeof operationFunc === 'function') { const result = operationFunc(number1, number2); // wyświetlamy rezultat użytkownikowi: const resultEl = document.querySelector('.result'); resultEl.innerText = result; } else { console.error('Selected operation is not a function') } }
Do funkcji getResult() warto dodać jeszcze warunek sprawdzający, czy podana operacja jest funkcją. Jeśli właściwość obiektu z operacjami nie miałaby odpowiednika w postaci funkcji (bo np. inny programista dopisał właściwość, a zapomniał dodać funkcję), to w konsoli otrzymamy adekwatną informację o błędzie.
Chcesz się przekonać, że nasze rozwiązanie stało się bardziej elastyczne? Dopisz operację wyznaczania reszty z dzielenia, czyli modulo (nie zapomnij uzupełnić o ten element pliku HTML). Widzisz? Nie musieliśmy modyfikować funkcji getResult()!
const operations = { '+': add, '-': subtract, '*': multiply, '/': divide, '%': modulo, }; function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } function multiply(a, b) { return a * b; } function divide(a, b) { return a / b; } function modulo(a, b) { return a % b; } function getResult(e) { e.preventDefault(); // zmienne number1 i number2 przechowują wartości odebrane od użytkownika: const number1 = Number(e.target.elements.number1.value); const number2 = Number(e.target.elements.number2.value); // z pola formularza o nazwie "operation" pobieramy informację o typie operacji: const operationType = e.target.elements.operation.value; const operationFunc = operations[operationType]; if(operationFunc === 'function') { const result = operationFunc(number1, number2); // wyświetlamy rezultat użytkownikowi: const resultEl = document.querySelector('.result'); resultEl.innerText = result; } else { console.error('Selected operation is not a function') } }
📍 Cały powyższy kod po modyfikacjach znajdziesz na repozytorium calculator-after.
Wyjaśnienie: Jeżeli to nie są Twoje pierwsze kroki w JavaScripcie, to zapewne zauważasz, że moglibyśmy jeszcze bardziej usprawnić nasz kod. W tej chwili najistotniejsze jednak jest zrozumienie możliwości wykorzystania obiektów w radzeniu sobie z rozbudowanymi instrukcjami warunkowymi. Umieszczanie danych w obiektach to świetny wstęp do refaktoryzacji kodu oraz do klas w JavaScripcie i korzystania z bibliotek takich jak React.
Zobacz, jaką drogę przeszliśmy: od kodu nieczytelnego, rozbudowanego i mogącego sprawiać problemy przy rozszerzaniu implementacji, po kod czytelny, elastyczny i łatwy w obsłudze! Czy ulepszanie rozwiązania (refaktoryzacja) zawsze wiąże się ze skróceniem liczby linii kodu? Nie – zwykle istotniejsza jest czytelność i prostota (np. przeniesienie części logiki do osobnej funkcji). O tym przekonasz się w artykule o refaktoryzacji, który pojawi się niebawem!
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.
Mam coś dla Ciebie!
W każdy piątek rozsyłam motywujący do nauki programowania newsletter!
Dodatkowo od razu otrzymasz ode mnie e-book o wartości 39 zł. To ponad 40 stron konkretów o nauce programowania i pracy w IT.
PS Zazwyczaj wysyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu e-mail.