Projekt okładki i stron tytułowych Alicja Żarowska-Mazur

Wydawca Edyta Kawala

Redaktor prowadzący Jolanta Kowalczuk

Redaktor Jadwiga Witecka

Koordynator produkcji Anna Bączkowska

Skład wersji elektronicznej na zlecenie Wydawnictwa Naukowego PWN
Karol Ossowski/Woblink

Zastrzeżonych nazw firm i produktów użyto w książce wyłącznie w celu identyfikacji.

Książka, którą nabyłeś, jest dziełem twórcy i wydawcy. Prosimy, abyś przestrzegał praw, jakie im przysługują. Jej zawartość możesz udostępnić nieodpłatnie osobom bliskim lub osobiście znanym. Ale nie publikuj jej w internecie. Jeśli cytujesz jej fragmenty, nie zmieniaj ich treści i koniecznie zaznacz, czyje to dzieło. A kopiując jej część, rób to jedynie na użytek osobisty.

Szanujmy cudzą własność i prawo.

Więcej na www.legalnakultura.pl

Polska Izba Książki

Copyright © by Wydawnictwo Naukowe PWN SA

Warszawa 2018

ISBN 978-83-01-20322-1

eBook został przygotowany na podstawie wydania papierowego z 2018 r., (wyd. I)

Warszawa 2018

Wydawnictwo Naukowe PWN SA

02-460 Warszawa, ul. Gottlieba Daimlera 2

tel. 22 69 54 321, faks 22 69 54 288

infolinia 801 33 33 88

e-mail: pwn@pwn.com.pl; reklama@pwn.pl

www.pwn.pl

Spis treści

Wstęp

1. Wprowadzenie

1.1. Rozwój języków i metodologii programowania

1.2. Ewolucje systemów i ich struktury

1.3. Kierunki rozwoju metodologii programowania

1.4. Dbałość o jakość programowania

2. Styl programowania i właściwości programów

2.1. Reguły dobrego stylu programowania

2.1.1. Komentarze

2.1.2. Puste linie i odstępy

2.1.3. Wybór nazw i skróty

2.1.4. Rozmieszczenie instrukcji i akapity

2.1.5. Nawiasy i porządkowanie list według alfabetu

2.2. Właściwości programów i sposoby ich osiągania

2.2.1. Uniwersalność

2.2.2. Modyfikowalność

2.2.3. Niezawodność

2.2.4. Zrozumiałość

2.2.5. Efektywność

2.3. Uwagi do właściwości programów

3. Programowanie strukturalne

3.1. List Dijkstry (1968)

3.2. Charakterystyka zdania goto

3.3. Instrukcje strukturalne

3.3.1. Sekwencja

3.3.2. Instrukcje warunkowe (logiczne)

3.3.3. Powtarzanie

3.4. Zakończenie

3.4.1. Uwagi o programowaniu strukturalnym

3.4.2. Inne sposoby patrzenia na opis programów

4. Metody projektowania programów

4.1. Wprowadzenie

4.2. Zasada abstrakcji

4.2.1. Poziomy opisu

4.2.2. Przykłady zastosowania zasady abstrakcji

4.2.3. Opis za pomocą pseudokodu

4.3. Programowanie modułowe

4.3.1. Dekompozycja problemu

4.3.2. Moduł. Relacje między modułami

4.3.3. Kryterium dekompozycji i złożoność systemu

4.4. Metoda struktur danych

4.5. Projektowanie metodą syntetyczną

4.5.1. Opis metody syntetycznej

4.5.2. Porównanie metody syntetycznej z analityczną

4.6. Zakończenie

5. Graficzne przedstawianie programów

5.1. Schemat blokowy i diagram strukturalny

5.2. Podstawowe przyczyny niestrukturalności

5.3. Metody eliminowania niestrukturalności

5.4. Wnioski i uwagi

6. Kodowanie programu

6.1. Zasady kodowania strukturalnego

6.2. Wspomaganie programowania strukturalnego przez języki programowania

6.3. Zakończenie

Literatura

Załącznik

Specyfikacja wymagań

Wstęp

Na przełomie lat sześćdziesiątych i siedemdziesiątych XX wieku ukonstytuowała się dziedzina informatyki zwana inżynierią oprogramowania. Inżynieria oprogramowania intensywnie rozwija się oraz odpowiada na ciągle rosnące wymagania i oczekiwania użytkowników. Zajmuje się problemami budowania oprogramowania wysokiej jakości z uwzględnieniem licznych uwarunkowań i ograniczeń, takich jak ekonomiczne, czasowe, prawne, oraz priorytetowym traktowaniem potrzeb użytkownika − człowieka. Problemy te są przedmiotem wielu monografii, podręczników i artykułów.

W latach pięćdziesiątych i na początku sześćdziesiątych tworzono prawie wyłącznie małe programy. Wynikało to z niewielkich możliwości ówczesnych komputerów oraz z braku zapotrzebowania na duże oprogramowanie. Wtedy tworzono oprogramowanie głównie do celów naukowych.

Sytuacja zmieniła się w połowie lat sześćdziesiątych, ponieważ rozwój sprzętu komputerowego i języków programowania umożliwił tworzenie znacznie bardziej złożonych systemów. W związku z tym podjęto liczne próby budowy dużych systemów informatycznych, których realizacja wymagała współpracy wielu osób. Ale znaczna część tych przedsięwzięć nie została nigdy zrealizowana, ponieważ stało się jasne, że rozwój technik tworzenia oprogramowania nie nadąża za szybkim rozwojem komputerów i sprzętu.

W 1981 roku B.W. Böhm analizował i zauważył wykładniczy wzrost zapotrzebowania na oprogramowanie w ciągu ostatnich 25 lat. Produktywność programistów, którzy mieli spełniać te wymagania programowe, zwiększyła się w ciągu ostatnich 30 lat najwyżej dziesięciokrotnie. Większą część tego wzrostu produktywności należy przypisać wprowadzeniu języków wysokiego poziomu, a pozostałą część zapotrzebowania na oprogramowanie można zaspokoić tylko w wyniku zatrudniania dodatkowych programistów.

Postęp w zakresie programowania podczas jego krótkiej historii jest ogromny. Nie możemy przypisać tego niczemu innemu, jak rozwojowi bazy i wdrażaniu lepszych metod programowania. Spotykamy dzisiaj programistów o małym stażu i doświadczeniu, a mimo to projektujących dość dobre programy. Wynika to stąd, że nie tylko ulepszono metody programowania, lecz także istnieje pewna wiedza o tych metodach, którą można przekazać początkującym programistom, oszczędzając im czas przejścia przez trudny okres prób i błędów. Programowanie komputerów wymaga pewnej dozy inwencji. Ale programowanie jest także zadaniem konstrukcyjnym, dla którego można sformułować właściwe techniki. Nasza praca jest właśnie próbą przedstawienia takich reguł technicznych wraz z przyświecającymi im motywacjami.

Celem pracy jest przedstawienie wybranych metod programowania strukturalnego, tzn. takich, które prowadzą do poprawnej struktury, poprawy jakości oprogramowania oraz zwiększenia efektywności programistów. Warto podkreślić, że postępowanie w początkowej fazie programowania jest w znacznym stopniu niezależne od tego, jakim konkretnym językiem posługuje się programista do wyrażenia swoich myśli. W pracy zwrócono uwagę na wpływ wielkości zadania na techniki programowania oraz czynniki wpływające na jakość programu. Problemy te są ważne z punktu ogólnego spojrzenia na programowanie.

Niniejsza książka jest przeznaczona głównie dla studentów kierunków informatycznych. Może z niej korzystać każdy czytelnik z ogólną wiedzą o programowaniu i niewielkim doświadczeniem w zakresie podstaw programowania. Zamieszczone w książce przykłady lub dyskutowane instrukcje są zapisane najczęściej w trzech językach: Pascal, C++, Python. Uwagi w niej zawarte mają charakter uniwersalny, w większości niezależny od stosowanego języka.

1. Wprowadzenie

1.1. Rozwój języków i metodologii programowania

W latach siedemdziesiątych XX wieku zwrócono uwagę, że dyskusja o językach programowania nie ogranicza się tylko do samego języka. W rzeczywistości dotyczy czegoś zupełnie innego − określonej metodologii projektowania, ponieważ każdy język wspiera pewną metodologię. Choć danego języka można używać także w innych metodologiach, jego wykorzystanie wtedy nie jest optymalne. Dlatego zamiast mówić o językach programowania, lepiej jest skupić uwagę na metodologiach projektowania i implementacji oprogramowania. W latach siedemdziesiątych i osiemdziesiątych uważano, że projektowanie języka programowania, łącznie z opracowaniem kompilatorów i odpowiedniego środowiska programowego oraz wyszkoleniem tysięcy programistów i kadr innych specjalistów, trwa 10−15 lat (obecnie przyjmuje się, że ten okres wynosi 2−3 lata). Wskutek tego nawet takie języki, jak Modula 2 lub Ada, już po 6−8 latach od wprowadzenia do praktyki są częściowo przestarzałe i nie odzwierciedlają najnowszych osiągnięć w dziedzinie metodologii programowania. Typową formą rozwoju języków programowania może być ich ewolucyjne ulepszanie. Dobrym przykładem jest ewolucja języka Algol (58, 60, 68). Można tu wyróżnić dwa kierunki dalszego rozwoju tego języka:

- kierunek reprezentowany przez prof. N. Wirtha: Algol, Pascal (1974), Modula 2, Oberon, Modula 3;

- kierunek rozwijany przez firmę Borland: Algol, Pascal, Turbo Pascal 1.0, ...3.0 (1985), Turbo Pascal 4.0 (moduły) (1987), Turbo Pascal 5.5 (obiekty), ..., Turbo Pascal 7.0.

Dalszym efektem rozwoju języka Turbo Pascal kontynuowanego przez firmę Borland jest język Delphi, który już ma wersje 1.0, ..., 7.0. Również język C++ posiada kolejne, rozwijane wersje (C++03, C++11, C++14 i C++17). Język Python został utworzony przez Guido van Rossum jako następca języka ABC. Obecnie w użyciu są wersje 2.x oraz 3.x, niebędące w pełni zgodne z sobą; ta pierwsza przestanie być wspierana w 2020 r.

Najważniejszym wnioskiem z tej dyskusji jest stwierdzenie, że praktycznie nigdy nie będziemy mieli języka programowania reprezentującego najnowszy stan wiedzy w dziedzinie metodologii oprogramowania. Dlatego należy rozważyć problem, jak wspomagać nowsze metodologie, stosując języki, które nie były przeznaczone dla tej metodologii.

Historycznie rzecz biorąc, większość konstrukcji w językach, które opracowano do 1975 roku, dotyczyła właściwego formułowania instrukcji do wyrażania algorytmów i struktur danych na poziomie proceduralnym. Dla takiego podejścia do programowania wprowadzono termin programowanie w małej skali (ang. programming-in-the-small). Dzisiaj programowanie w małej skali odgrywa ważną rolę w początkowym nauczaniu programowania i w obliczeniach na komputerach osobistych. Można powiedzieć, że stanowi podstawowy zakres wiedzy programistycznej, którą powinien opanować każdy informatyk. Sytuację w tej dziedzinie charakteryzują najlepiej języki takie jak Pascal, Delphi, C++, Java, C#.

Dla dużych systemów należy rozwiązać problem właściwej organizacji i współpracy wielu ludzi, stosowania różnych algorytmów i doboru struktur danych. Poszczególne fragmenty systemu mogą mieć różnych autorów, mogą pochodzić z bibliotek i mogą nie być dostosowane w szczegółach do przewidywanych zastosowań. W takiej sytuacji dokonuje się wielu kontroli zgodności. Dla metodologii i rozwiązań w tej klasie zagadnień zaproponowano nazwę programowanie w wielkiej skali (ang. programming-in-the-large).

W procesie rozwoju języków wprowadzono pojęcie abstrakcyjnego typu danych jako funkcjonalnego opisu sprzężenia struktury danych i skojarzonych z nią operacji. Wprowadzono pojęcie modułu, będącego w różnym stopniu realizacją procedury. Stan wiedzy w dziedzinie programowania w wielkiej skali reprezentują takie języki, jak Modula 2, Ada 83, 95 (sukcesor języków Algol i Pascala) i C, C++, Java, C# i Python.

Jednak postępy poczynione w dziedzinie metodologii programowania nie są odzwierciedlone w odpowiednich właściwościach tych języków (obecnie już wieloletnich). Największy postęp dotyczy poglądów na temat wielokrotnego wykorzystania modułów (ang. reusability of modules). Już teraz można by osiągnąć znaczne zmniejszenie wysiłku koniecznego do napisania programu, gdyby zamiast wytwarzać każdy system oprogramowania od początku, można było brać większą liczbę modułów z bibliotek lub korzystać z wyników wcześniejszych przedsięwzięć programistycznych. Jednak, jak wskazuje doświadczenie, przy projektowaniu modułów do wielokrotnego wykorzystania należy wziąć pod uwagę wiele specyficznych czynników (ich uwzględnienie jest konieczne, nawet gdyby miało spowolnić proces rzeczywistego projektowania). Wielokrotne wykorzystanie modułu będzie znaczne wtedy, gdy jego możliwości sprzęgania będą dostosowane do dużej liczby języków. Programiści rzadko zgadzają się między sobą, jakie powinny być takie sprzężenia. Dlatego, aby wytwarzać moduły wieloużywalne i wprowadzać je do użytku, należy mieć duże doświadczenie i dobry przegląd całej klasy modułów pokrewnych, a ponadto kierownicy zespołów powinni posiadać rozeznanie i umiejętności w posługiwaniu się bibliotekami oprogramowania do specjalnych zastosowań. Byłoby korzystne, gdyby klasa modułów o standardowych sprzężeniach była ciągle rozszerzana, a zasady sprzęgania były wykładane na uczelniach. Nawet jednak, jeśli tak się stanie, to upłynie sporo czasu, zanim tego rodzaju nauczanie przyniesie praktyczne i widoczne skutki. Języki takie jak Modula −2, Ada w wielu wypadkach są niewystarczające do przedstawienia na odpowiednim poziomie abstrakcji modułów wielokrotnego wykorzystania.

Znaczącym krokiem czynionym w tym kierunku jest tworzenie pewnych ogólnie przyjętych standardów, które pozwalają na wielokrotne wykorzystanie modułów utworzonych za pomocą różnych narzędzi, technik i języków programowania. Z najistotniejszych można wymienić specyfikowanie interfejsów binarnych aplikacji (ang. application binary interface, w skrócie ABI), które określają sposób współpracy skompilowanych programów i bibliotek między sobą i z systemem operacyjnym. Określają one między innymi sposób wywoływania podprogramów (przekazywanie argumentów i zwracanie wartości) i formaty plików obiektowych tworzonych przez kompilatory.

Podobne zastosowanie mają interfejsy programowania aplikacji (ang. application programming interface, w skrócie API), określające sposób komunikacji i wywołań programów między sobą na poziomie kodu źródłowego (np. nagłówki funkcji bibliotecznych w C++). Przykładami API są Windows API dla systemów operacyjnych Microsoftu, POSIX (od ang. Portable Operating System Interface for UNIX) dla systemów UNIX i Linux oraz COCOA dla OS X firmy Apple.

Pewne znaczenie mają też tzw. FFI (ang. foreign function interface), które wspierają wywoływanie procedur napisanych w jednym języku (np. Common Lisp lub Haskell) z poziomu innego języka (np. C++). Z kolei języki z rodziny .NET są kompilowane do wspólnej formy CIL (ang. Common Intermediate Language) i ułatwiają wymienne użytkowanie modułów w różnych językach.

Te rozważania prowadzą do wniosku, że przyszłe języki programowania będą miały wiele cech, które obecnie kojarzymy z językami specyfikacji. Końcowy, wykonywalny program będzie wynikiem transformacji zastosowanych do specyfikacji i jej parametrów.

1.2. Ewolucje systemów i ich struktury

Wiele współczesnych systemów programowych ma odmienne zastosowanie. Nie zakłada się, że wytwarzają one ustalony zbiór wyników w odpowiedzi na określone wartości wejściowe. Wymaga się natomiast, aby działały nieskończenie długo i sterowały pewnymi działaniami zachodzącymi na zewnątrz, tj. sterowały jednym lub wieloma „procesami zewnętrznymi”. Ta klasa zastosowań pod wieloma względami powoduje zmianę poglądów na właściwości oprogramowania.

Obecnie zakończenie wykonywania programu jest uważane raczej za awarię, chociaż np. program interpretowany jako funkcja matematyczna musi się zakończyć. Dlatego w ogólności poprawność oprogramowania nie można określić metodą sprawdzania wartości wyjściowych (po zakończeniu programu), ponieważ program nie musi się zakończyć. Jednocześnie tzw. podejście funkcyjne (ang. functional programming) do programowania z góry narzuca taką konstrukcję kodu źródłowego, które równoważne jest wyliczeniu pewnej funkcji matematycznej. Pierwszym językiem funkcyjnym był LISP opracowany w 1958 roku przez Johna McCarthy’ego. Współcześnie stosuje się języki takie jak czysto funkcyjny Haskell lub wieloparadygmatowy F#.

W systemach dominującą rolę zaczynają odgrywać zagadnienia niezawodności, a nawet bezpieczeństwa. Oprogramowanie powinno zawsze wykazywać akceptowalne działanie, nawet wtedy, gdy pewna część systemu lub używany sprzęt ulegnie awarii albo gdy zostaną dostarczone niedozwolone lub sprzeczne dane. W żadnym wypadku, z wyjątkiem zaniku zasilania lub innych poważnych zdarzeń, oprogramowanie nie powinno dopuścić do gwałtownego zaprzestania działania systemu, lecz umożliwić mu stopniową utratę funkcjonalności (ang. graceful degradation of performance).

System ma działać nieskończenie długo. Dlatego powinien mieć możliwość dynamicznej rekonfiguracji, aby umożliwiać poprawianie potencjalnych defektów i adaptację do zmiennych wymagań. Innym rozwiązaniem tego problemu jest stosowanie upgrade, tzn. korzystanie z nowej wersji programu dostarczonej użytkownikowi przez producenta za mniejszą opłatą. Przykładowo bieżąca polityka aktualizacji Microsoftu nazwana Windows-as-a-service narzuca automatyczne regularne aktualizowanie systemu. Przyszłość pokaże, że przedstawione poglądy z pewnością nie są jedynymi, jakie można mieć na temat oprogramowania.

Następny etap ewolucji można określić mianem „oprogramowanie jako składowa systemów niejednorodnych”. Część systemu mogą stanowić komputery z oprogramowaniem włącznie, inną częścią mogą być urządzenia techniczne, a nawet ludzie. Rozwiązanie problemu nie jest już określone wyłącznie przez oprogramowanie. Jego część jest przekazywana ludziom lub urządzeniom technicznym, a rozłożenie zadań może się zmieniać stosownie do możliwości jednostek uczestniczących. Takie systemy mają interesujące właściwości:

- Nie można w nich dokładnie określić części rozwiązania problemu wykonywanej przez oprogramowanie, ponieważ zastosowany algorytm nie obejmuje całości problemu i potrafi poprawnie rozwiązać tylko pewne przypadki.

- Istnieje kilka wariantów przyjęcia odpowiedzialności za poprawność wyników takiego systemu, np. oprogramowanie nie powinno mieć możliwości podejmowania ostatecznej decyzji np. o życiu lub śmierci w przypadkach medycznych, wojskowych lub w systemach komunikacji, nawet jeśli system oprogramowania wykonuje większą część zadania. W wielu zastosowaniach podejmowanie decyzji bez wspomagającego oprogramowania nie jest możliwe. Jednocześnie decyzje te są na tyle trudne, że system może nie potrafić ich podjąć. Powstaje tutaj swoista interakcja człowieka z komputerem, która z punktu widzenia zastosowania stanowi nierozerwalną całość, przykładowo taką całość tworzą: pilot współczesnego samolotu współpracujący równocześnie z wieloma systemami pokładowymi, zespół chirurgów przeprowadzających skomplikowaną operację wspomaganą komputerem.

Powyższe rozważania mogą posłużyć jako wskazówka do właściwego spojrzenia na współpracę człowiek−maszyna w środowiskach programowych. Dlatego można teraz podsumować, jaki jest stan wiedzy w dziedzinie języków programowania i ich środowisk oraz jakie problemy są wciąż nierozwiązane.