Teraz, gdy znasz już pewne podstawy o programach, przyjrzyjmy się bliżej jak projektowaniu programu.
Kiedy siadasz do pisania programu, zazwyczaj masz jakiś rodzaj pomysł, dla którego chcesz napisać program. Nowi programiści często mają problem ze znalezieniem sposobu na przekształcenie tego pomysłu w rzeczywisty kod. Okazuje się jednak, że wiele potrzebnych umiejętności rozwiązywania problemów nabyłeś już w życiu codziennym.
Najważniejszą rzeczą do zapamiętania (i najtrudniejszą do zrobienia) jest zaprojektowanie programu zanim zaczniesz kodować. Pod wieloma względami programowanie przypomina architekturę. Co by się stało, gdybyś próbował zbudować dom bez przestrzegania planu architektonicznego? Są szanse, że jeśli nie będziesz bardzo utalentowany, skończysz z domem, który będzie miał wiele problemów: nierówne ściany, nieszczelny dach itp. Podobnie, jeśli spróbujesz programować, zanim będziesz miał dobry plan działania na przyszłość, prawdopodobnie odkryjesz, że Twój kod ma wiele problemów i będziesz musiał spędzić dużo czasu na naprawianiu problemów, których można było uniknąć, przy odrobinie myślenia do przodu.
Odrobina planowania z góry pozwoli zaoszczędzić zarówno czas, jak i frustrację w dłuższej perspektywie.
W tej lekcji przedstawimy uogólnione podejście do przekształcania pomysłów w proste programy funkcjonalne.
Krok 1 projektowania: zdefiniuj swój cel
Aby napisać udany program, musisz najpierw określić, jaki jest twój cel. Idealnie byłoby, gdybyś był w stanie to wyrazić w zdaniu lub dwóch. Często przydatne jest wyrażenie tego jako wyniku skierowanego do użytkownika. Na przykład:
- Pozwól użytkownikowi organizować listę nazw i powiązanych numerów telefonów.
- Generuj losowe lochy, w których powstaną ciekawie wyglądające jaskinie.
- Wygeneruj listę rekomendacji akcji dla akcji, które zapewniają wysokie dywidendy.
- Modeluj, ile czasu zajmuje piłka zrzucona z wieży, aby trafić w uziemienie.
Chociaż ten krok wydaje się oczywisty, jest również bardzo ważny. Najgorsze, co możesz zrobić, to napisać program, który w rzeczywistości nie robi tego, czego chciałeś Ty (lub Twój szef)!
Krok 2 projektowania: Zdefiniuj wymagania
Chociaż zdefiniowanie problemu pomaga określić co pożądany wynik, nadal jest on niejasny. Następnym krokiem jest przemyślenie wymagań.
Wymagania to fantazyjne określenie zarówno ograniczeń, jakie musi spełniać Twoje rozwiązanie (np. budżet, oś czasu, przestrzeń, pamięć itp.), jak i możliwości, jakie musi wykazywać program, aby spełnić potrzeby użytkowników. Pamiętaj, że Twoje wymagania powinny podobnie skupiać się na „co”, a nie na „jak”.
Na przykład:
- Numery telefonów powinny być zapisywane, aby można było je później przywołać.
- Losowy loch powinien zawsze zawierać sposób dotarcia od wejścia do wyjścia.
- Zalecenia dotyczące zapasów powinny opierać się na historycznych danych cenowych.
- Użytkownik powinien mieć możliwość wprowadzenia wysokości tower.
- Potrzebujemy wersji testowej w ciągu 7 dni.
- Program powinien generować wyniki w ciągu 10 sekund od przesłania żądania przez użytkownika.
- Program powinien ulegać awarii podczas mniej niż 0,1% sesji użytkownika.
Pojedynczy problem może wiązać się z wieloma wymaganiami, a rozwiązanie nie jest „ukończone”, dopóki nie spełni wszystkich je.
Krok 3 projektowania: Zdefiniuj narzędzia, cele i plan tworzenia kopii zapasowych
Jeśli jesteś doświadczonym programistą, na tym etapie zwykle wykonuje się wiele innych kroków, w tym:
- Definiowanie docelowej architektury i/lub systemu operacyjnego, na którym będzie działał Twój program.
- Określenie, na jakim zestawie narzędzi będziesz pracować za pomocą.
- Określenie, czy będziesz pisać swój program sam, czy w zespole.
- Definiowanie strategii testowania/opinie zwrotnej/wydawania.
- Określanie sposobu tworzenia kopii zapasowej kodu.
Jednakże dla nowego programisty odpowiedzi na te pytania są zazwyczaj proste: piszesz program na własny użytek, sam, we własnym systemie, używając pobranego IDE, a Twój kod prawdopodobnie nie jest używany przez nikogo innego poza Tobą. Dzięki temu wszystko staje się proste.
To powiedziawszy, jeśli zamierzasz pracować nad czymś o nietrywialnej złożoności, powinieneś mieć plan utworzenia kopii zapasowej kodu. Nie wystarczy spakować lub skopiować katalogu źródłowego do innej lokalizacji na tym samym urządzeniu pamięci masowej — jeśli urządzenie pamięci masowej ulegnie awarii lub ulegnie uszkodzeniu, stracisz wszystko. Kopiowanie lub zipowanie na nośnik wymienny (np. dysk flash) jest lepsze, choć nadal istnieje ryzyko utraty wszystkiego w przypadku kradzieży, pożaru lub znaczącej klęski żywiołowej.
Najlepsza strategia tworzenia kopii zapasowych polega na przeniesieniu kopii kodu na maszynę, która znajduje się w innej lokalizacji fizycznej. Można to zrobić na wiele prostych sposobów: spakuj plik i wyślij go do siebie e-mailem, prześlij do usługi przechowywania w chmurze (np. Dropbox), użyj protokołu przesyłania plików (np. SFTP), aby przesłać go na kontrolowany przez siebie serwer lub użyj systemu kontroli wersji znajdującego się na innym komputerze lub w chmurze (np. Github). Systemy kontroli wersji mają tę dodatkową zaletę, że mogą nie tylko przywracać pliki, ale także przywracać je do poprzedniej wersji.
Krok 4 projektowania: Podziel trudne problemy na łatwe
W prawdziwym życiu często musimy wykonywać bardzo złożone zadania. Próba wymyślenia, jak wykonać te zadania, może być bardzo trudna. W takich przypadkach często stosujemy odgórną metodę rozwiązywania problemów. Oznacza to, że zamiast rozwiązywać jedno złożone zadanie, dzielimy je na wiele podzadań, z których każde jest indywidualnie łatwiejsze do rozwiązania. Jeśli te podzadania są nadal zbyt trudne do rozwiązania, można je dalej rozbić. Dzięki ciągłemu dzieleniu złożonych zadań na prostsze możesz w końcu dojść do punktu, w którym każde pojedyncze zadanie będzie wykonalne, jeśli nie trywialne.
Przyjrzyjmy się temu przykładowi. Powiedzmy, że chcemy posprzątać nasz dom. Nasza hierarchia zadań wygląda obecnie tak:
- Posprzątaj dom
Sprzątanie całego domu to dość duże zadanie do wykonania podczas jednego posiedzenia, dlatego podzielmy je na podzadania:
- Posprzątaj dom
- Odkurz dywany
- Wyczyść łazienki
- Posprzątaj kuchnię
To łatwiejsze w zarządzaniu, ponieważ mamy teraz podzadania, na których możemy skupić się indywidualnie. Jednak niektóre z nich możemy rozłożyć jeszcze bardziej na czynniki:
- Posprzątaj dom
- Odkurz dywany
- Wyczyść łazienki
- Wyszoruj toaletę (fuj!)
- Umyj zlew
- Posprzątaj kuchnię
- Posprzątaj blaty
- Umyj blaty
- Wyszoruj zlew
- Wynieś śmieci
Teraz mamy hierarchię zadań, żadne z nich nie jest szczególnie trudne. Wykonując każdy z tych stosunkowo łatwych do wykonania elementów składowych, możemy wykonać trudniejsze ogólne zadanie, jakim jest sprzątanie domu.
Innym sposobem utworzenia hierarchii zadań jest zrobienie tego od od dołu do góry. W tej metodzie zaczniemy od listy łatwych zadań i skonstruujemy hierarchię, grupując je.
Na przykład wiele osób musi chodzić do pracy lub szkoły w dni powszednie, więc powiedzmy, że chcemy rozwiązać problem „pójścia do pracy”. Gdyby zapytano Cię, jakie zadania wykonywałeś rano, aby dostać się z łóżka do pracy, możesz otrzymać następującą listę:
- Wybierz ubrania
- Ubierz się
- Zjedz śniadanie
- Podróż do praca
- Umyj zęby
- Wstań z łóżka
- Przygotuj się śniadanie
- Wsiądź na rower
- Weź prysznic
Korzystając z metody oddolnej, możemy uporządkować je w hierarchię elementów, szukając sposobów na pogrupowanie elementów o podobieństwach:
- Idź z łóżka do pracy
- Sprawy w sypialni
- Wyłącz alarm
- Wstań z łóżka
- Wybierz ubrania
- Rzeczy w łazience
- Weź prysznic
- Ubierz się
- Umyj zęby
- Rzeczy śniadaniowe
- Zaparz kawę lub herbatę
- Zjedz płatki
- Rzeczy w transporcie
- Wsiądź na rower
- Podróż do praca
- Sprawy w sypialni
Jak się okazuje, te hierarchie zadań są niezwykle przydatne w programowaniu, ponieważ gdy już masz hierarchię zadań, zasadniczo zdefiniowałeś strukturę całego programu. Zadanie najwyższego poziomu (w tym przypadku „Posprzątaj dom” lub „Idź do pracy”) staje się funkcją main() (ponieważ jest to główny problem, który próbujesz rozwiązać). Podelementy stają się w programie funkcjami.
Jeśli okaże się, że jeden z elementów (funkcji) jest zbyt trudny do wdrożenia, po prostu podziel go na wiele podpozycji/podfunkcji. W końcu powinieneś osiągnąć punkt, w którym implementacja każdej funkcji w programie będzie banalna.
Krok 5 w projektowaniu: Ustal sekwencję zdarzeń
Teraz, gdy Twój program ma strukturę, czas określić, jak połączyć wszystkie zadania w całość. Pierwszym krokiem jest określenie sekwencji zdarzeń, które zostaną wykonane. Na przykład, kiedy wstajesz rano, w jakiej kolejności wykonujesz powyższe zadania? Może to wyglądać tak:
- Sprawy w sypialni
- Rzeczy w łazience
- Rzeczy śniadaniowe
- Rzeczy w transporcie
Gdybyśmy pisali kalkulator, moglibyśmy zrobić rzeczy w następującej kolejności:
- Pobierz pierwszą liczbę od użytkownika
- Uzyskaj operację matematyczną od użytkownik
- Pobierz drugi numer od użytkownika
- Oblicz wynik
- Wydrukuj wynik
W tym momencie jesteśmy gotowi do wdrożenia.
Krok wdrożenia 1: Nakreślenie głównej funkcji
Teraz jesteśmy gotowi, aby rozpocząć wdrażanie. Powyższe sekwencje można wykorzystać do zarysowania programu głównego. Na razie nie martw się o wejścia i wyjścia.
int main()
{
// doBedroomThings();
// doBathroomThings();
// doBreakfastThings();
// doTransportationThings();
return 0;
}Lub w przypadku kalkulatora:
int main()
{
// Get first number from user
// getUserInput();
// Get mathematical operation from user
// getMathematicalOperation();
// Get second number from user
// getUserInput();
// Calculate result
// calculateResult();
// Print result
// printResult();
return 0;
}Zauważ, że jeśli zamierzasz używać tej „zarysowej” metody do konstruowania programów, Twoje funkcje nie zostaną skompilowane, ponieważ definicje jeszcze nie istnieją. Jednym ze sposobów rozwiązania tego problemu jest komentowanie wywołań funkcji do czasu, aż będziesz gotowy do zaimplementowania definicji funkcji (i sposób, który tutaj pokażemy). Alternatywnie możesz wyłączyć swoje funkcje (utworzyć funkcje zastępcze z pustymi treściami), aby program się skompilował.
Krok implementacji 2: Zaimplementuj każdą funkcję
W tym kroku dla każdej funkcji wykonasz trzy rzeczy:
- Zdefiniujesz prototyp funkcji (wejścia i wyjścia)
- Napisz funkcję
- Przetestuj funkcję
Jeśli Twoje funkcje są wystarczająco szczegółowe, każda funkcja powinna być dość prosta i bezpośrednia. Jeśli dana funkcja nadal wydaje się zbyt skomplikowana w implementacji, być może należy ją podzielić na podfunkcje, które można łatwiej zaimplementować (lub możliwe, że zrobiłeś coś w niewłaściwej kolejności i musisz ponownie sprawdzić kolejność zdarzeń).
Zróbmy pierwszą funkcję z przykładu kalkulatora:
#include <iostream>
// Full implementation of the getUserInput function
int getUserInput()
{
std::cout << "Enter an integer: ";
int input{};
std::cin >> input;
return input;
}
int main()
{
// Get first number from user
int value{ getUserInput() }; // Note we've included code here to test the return value!
std::cout << value << '\n'; // debug code to ensure getUserInput() is working, we'll remove this later
// Get mathematical operation from user
// getMathematicalOperation();
// Get second number from user
// getUserInput();
// Calculate result
// calculateResult();
// Print result
// printResult();
return 0;
}Najpierw ustaliliśmy, że funkcja getUserInput nie przyjmuje żadnych argumentów i będzie działać zwróć wartość int z powrotem do osoby wywołującej. Znajduje to odzwierciedlenie w prototypie funkcji, który zwraca wartość int i nie ma parametrów. Następnie napisaliśmy ciało funkcji, które składa się z 4 prostych instrukcji. Na koniec zaimplementowaliśmy tymczasowy kod w funkcji głównego aby przetestować, czy funkcja getUserInput (w tym jej wartość zwracana) działa poprawnie.
Możemy uruchomić ten program wiele razy z różnymi wartościami wejściowymi i upewnić się, że w tym momencie program zachowuje się tak, jak tego oczekujemy. Jeśli znajdziemy coś, co nie działa, wiemy, że problem leży w kodzie, który właśnie napisaliśmy.
Gdy jesteśmy przekonani, że program do tego momentu działa zgodnie z założeniami, możemy usunąć tymczasowy kod testowy i przystąpić do implementacji kolejnej funkcji (funkcja getMathematicalOperation). Na tej lekcji nie zakończymy programu, ponieważ musimy najpierw omówić kilka dodatkowych tematów.
Pamiętaj: nie wdrażaj całego programu za jednym razem. Pracuj nad tym etapami, testując każdy krok przed kontynuowaniem.
Powiązana treść
Testowanie omówimy bardziej szczegółowo w lekcji 9.1 -- Wprowadzenie do testowania kodu.
Krok wdrożenia 3: Testowanie końcowe
Gdy program zostanie „ukończony”, ostatnim krokiem jest przetestowanie całego programu i upewnienie się, że działa zgodnie z zamierzeniami. Jeśli to nie działa, napraw to.
Słowa rad podczas pisania programów
Zachowuj łatwość uruchamiania programów. Często nowi programiści mają wspaniałą wizję wszystkich rzeczy, które chcą, aby ich program robił. „Chcę napisać grę RPG z grafiką i dźwiękiem, z losowymi potworami i lochami, z miastem, które możesz odwiedzić, aby sprzedać przedmioty znalezione w lochach”. Jeśli spróbujesz napisać coś zbyt skomplikowanego, aby zacząć, poczujesz się przytłoczony i zniechęcony brakiem postępów. Zamiast tego uczyń swój pierwszy cel tak prostym, jak to tylko możliwe, czymś, co z pewnością będzie w Twoim zasięgu. Na przykład „Chcę móc wyświetlać na ekranie dwuwymiarowe pole”.
Dodawaj funkcje z czasem. Gdy już Twój prosty program będzie działał i będzie działał dobrze, możesz dodać do niego funkcje. Na przykład, gdy będziesz mógł wyświetlić swoje pole, dodaj postać, która może chodzić. Kiedy już będziesz mógł chodzić, dodaj ściany, które mogą utrudniać twój postęp. Kiedy już będziesz mieć mury, zbuduj z nich proste miasto. Kiedy już będziesz mieć miasto, dodaj kupców. Dodając stopniowo każdą funkcję, Twój program będzie coraz bardziej złożony, nie przytłaczając Cię w tym procesie.
Skoncentruj się na jednym obszarze na raz. Nie próbuj kodować wszystkiego na raz i nie dziel swojej uwagi na wiele zadań. Skoncentruj się na jednym zadaniu na raz. O wiele lepiej jest mieć jedno działające zadanie i pięć, które jeszcze nie zostały rozpoczęte, niż sześć częściowo działających zadań. Jeśli podzielisz swoją uwagę, istnieje większe prawdopodobieństwo, że popełnisz błąd i zapomnisz o ważnych szczegółach.
Testuj każdy fragment kodu na bieżąco. Nowi programiści często piszą cały program w jednym przebiegu. Następnie, gdy kompilują go po raz pierwszy, kompilator zgłasza setki błędów. Może to być nie tylko onieśmielające – jeśli Twój kod nie działa, może być trudno dowiedzieć się, dlaczego. Zamiast tego napisz fragment kodu, a następnie natychmiast go skompiluj i przetestuj. Jeśli to nie zadziała, będziesz dokładnie wiedział, gdzie leży problem i łatwo będzie go naprawić. Kiedy już będziesz mieć pewność, że kod działa, przejdź do następnego fragmentu i powtórz. Zakończenie pisania kodu może zająć więcej czasu, ale kiedy już skończysz, wszystko powinno działać i nie będziesz musiał tracić dwa razy więcej czasu na zastanawianie się, dlaczego tak się nie dzieje.
Nie inwestuj w doskonalenie wczesnego kodu. Pierwsza wersja robocza funkcji (lub programu) rzadko jest dobra. Co więcej, programy mają tendencję do ewolucji w miarę upływu czasu, w miarę dodawania nowych możliwości i znajdowania lepszych sposobów strukturyzacji. Jeśli zainwestujesz zbyt wcześnie w dopracowanie kodu (dodanie dużej ilości dokumentacji, pełna zgodność z najlepszymi praktykami, dokonanie optymalizacji), ryzykujesz utratę całej tej inwestycji, gdy konieczna będzie zmiana kodu. Zamiast tego spraw, aby funkcje działały minimalnie, a następnie przejdź dalej. W miarę jak nabierzesz zaufania do swoich rozwiązań, nałóż kolejne warstwy lakieru. Nie dąż do doskonałości — nietrywialne programy nigdy nie są doskonałe i zawsze można zrobić coś więcej, aby je ulepszyć. Osiągnij poziom „wystarczająco dobry” i kontynuuj.
Optymalizuj pod kątem łatwości konserwacji, a nie wydajności. Istnieje słynny cytat (Donalda Knutha), który mówi, że „przedwczesna optymalizacja jest źródłem wszelkiego zła”. Nowi programiści często spędzają zbyt dużo czasu na myśleniu o tym, jak mikrooptymalizować swój kod (np. próbując dowiedzieć się, która z dwóch instrukcji jest szybsza). To rzadko ma znaczenie. Większość korzyści związanych z wydajnością wynika z dobrej struktury programu, użycia odpowiednich narzędzi i możliwości w zależności od danego problemu oraz przestrzegania najlepszych praktyk. Należy przeznaczyć dodatkowy czas na poprawę łatwości konserwacji kodu. Znajdź nadmiarowość i usuń ją. Podziel długie funkcje na krótsze. Zamień niewygodny lub trudny w użyciu kod na coś lepszego. Efektem końcowym będzie kod, który będzie łatwiejszy do późniejszego ulepszenia i optymalizacji (po ustaleniu, gdzie faktycznie potrzebna jest optymalizacja) i mniej błędów. Na lekcji oferujemy kilka dodatkowych sugestii 3.10 — Znajdowanie problemów, zanim staną się problemami.
Złożony system, który działa, niezmiennie okazuje się, że wyewoluował z prostego systemu, który działał
Wnioski
Wielu nowych programistów skraca proces projektowania (ponieważ wydaje się to dużo pracy i/lub nie daje tyle frajdy, co pisanie kodu). Jednak w przypadku każdego nietrywialnego projektu wykonanie tych kroków pozwoli Ci na dłuższą metę zaoszczędzić dużo czasu. Małe planowanie z góry pozwala zaoszczędzić dużo debugowania na końcu.
Kluczowa informacja
Poświęcenie odrobiny czasu na zastanowienie się nad strukturą programu doprowadzi do lepszego kodu i skrócenia czasu na wyszukiwanie i naprawianie błędów.
Powiedziałbym, że jest to prawdopodobnie najważniejsza rzecz w programowaniu i niektórzy z nas, tak jak ja na początku, przyjęli to za oczywistość.
Gdy oswoisz się z tymi koncepcjami i wskazówkami, zaczną one przychodzić Ci bardziej naturalnie. W końcu dojdziesz do punktu, w którym będziesz mógł pisać całe funkcje (i krótkie programy) przy minimalnym wstępnym planowaniu.

