R02-03.DOC

(303 KB) Pobierz
Szablon dla tlumaczy

Programowanie w C++Builderze

Tematyka tego rozdziału koncentruje się na czynności tworzenia kodu w języku C++.

Na początku zajmiemy się problematyką czytelności tworzonego kodu i zaprezentujemy środki prowadzące do jej optymalizacji. Kod czytelny to kod łatwiejszy do zrozumienia i konserwacji, a to przekłada się wprost na niższe koszty zarządzania projektem. Jednym z najważniejszych czynników, wpływających na czytelność kodu, jest wybór i konsekwentne stosowanie odpowiedniego stylu kodowania; w rozdziale tym przedstawimy kilka propozycji stylistycznych i wyjaśnimy, dlaczego niektóre style zapisu kodu lepsze są od innych.

W dalszej kolejności przedstawimy kilka wskazówek dotyczących wybranych konstrukcji języka C++ i ich stosowania w kodzie aplikacji tworzonej z użyciem C++Buildera. Niektóre z tych konstrukcji okazują się być niezrozumiałe dla programistów stawiających pierwsze kroki w C++, niektóre zaś bywają przez nich rozumiane opacznie; programiści ci znajdą tutaj kilka wskazówek, pozwalających im uporządkować wiedzę. Niektóre zaawansowane zagadnienia, jak np. pascalowe dziedzictwo biblioteki VCL C++Buildera, z pewnością zainteresują również programistów bardziej zaawansowanych. 

Style kodowania a czytelność programu

W podrozdziale tym zajmiemy się znaczeniem czytelności kodu i zaprezentujemy kilka metod przyczyniających się do jej poprawy. Trzeba stwierdzić, iż niezależnie od wyboru konkretnego stylu kodowania istotne jest jego konsekwentne stosowanie – zamieszczone dalej przykładowe fragmenty programów celowo wykorzystują różne style kodowania, a to pozwala lepiej zrozumieć, jak bardzo niepożądana jest wszelka niespójność w tej materii.

 

Proste i zwięzłe kodowanie

Jest niemal oczywiste, iż prostota z reguły przyczynia się do – szeroko rozumianej – efektywności, tak więc przystępując do kodowania programu, należy dążyć do tworzenia kodu krótkiego i prostego w interpretacji. Przynosi to dwojakiego rodzaju korzyści.

Po pierwsze, złożony problem zostaje niejako automatycznie rozbity na mniejsze fragmenty, z których każdy jest łatwy do zrozumienia i wykonuje dobrze określone zadanie. Złożoność kodu przekłada się wówczas jedynie na wyższy stopień jego abstrakcji, nie zaś na monstrualne niekiedy rozmiary.

Przyjrzyjmy się bliżej funkcjom na poniższym wydruku:

Wydruk 2.1. Złożoność kodu a stopień abstrakcji

 

#include <vector>

 

double GetMaximumValue(const std::vector<double>& Vector)

                       throw(std::out_of_range)

{

   double Maximum = Vector.at(0);

   

   for(int i=0; i<Vector.size(); ++i)

   {

      if(Vector[i] > Maximum)

      {

         Maximum = Vector[i];

      }

   }

  

   return Maximum;

}

 

void NormalizeVector(std::vector<double>& Vector)

{

   if(!Vector.empty())

   {

      double Maximum = GetMaximumValue(Vector);

 

      for(int i=0; i<Vector.size(); ++i)

      {

          Vector[i] -= Maximum;

      }

   }  

}

 

Obydwie funkcje zbliżone są do siebie pod względem złożoności kodu, tymczasem druga z nich wykonuje zagadnienie bardziej skomplikowane niż pierwsza; gdy jednak przyjrzeć się dokładniej ich treści, różnica w stopniu abstrakcyjności kodu staje się natychmiast widoczna – obliczenie maksymalnej wartości wśród elementów wektora wykonywane jest w drugiej funkcji jako operacja elementarna, podczas gdy operacje elementarne w ramach pierwszej funkcji ograniczają się do porównań i podstawień.

 

Pomijam tutaj TIP z górnej części strony 58 oryginału, bo jest kompletnie bez sensu

Druga ze wspomnianych korzyści wiąże się bezpośrednio z rozmiarem kodu – w kodzie o niewielkich rozmiarach deklaracje zmiennych lokalnych i parametrów funkcji dostępne są niemal natychmiast, bez konieczności nieustannego wertowania wielu nieraz stronic wydruku czy przewijania znacznych rozmiarów wydruku.

 

Akapitowanie kodu

O czytelności kodu decyduje również jego przejrzysty układ graficzny. W odniesieniu do kodu programu w języku C++ przejrzystość taką osiągnąć można przede wszystkim dzięki przestrzeganiu następującej zasady: każdy z nawiasów {} ograniczających zawartość bloku powinien być jedynym znakiem w swym wierszu, para odpowiadających sobie nawiasów powinna znajdować się w tej samej kolumnie, zaś zawartość bloku powinna być wcięta o kilka kolumn w stosunku do ograniczających go nawiasów (zazwyczaj stosuje się wcięcia o dwie lub cztery kolumny).

Upewnij się, że w edytorze kodu wyłączona jest funkcja automatycznej konwersji ciągów spacji na znaki tabulacji (decyduje o tym opcja Use tab character na karcie General opcji edytora – Tools|Editor Options). Ze względu na potencjalnie odmienne traktowanie znaku tabulacji w różnych edytorach wykorzystywanie go do akapitowania kodu może spowodować, iż układ kodu poprawny w jednym środowisku stanie się karykaturalny w innym.

Układ taki ogromnie ułatwia identyfikację poszczególnych bloków i strukturę ich zagnieżdżania. Widoczne stają się zakresy obowiązywania deklaracji zmiennych lokalnych i ewentualne przypadki błędnego ich użycia poza zakresem. Nie należy zapominać, iż z każdym blokiem związania jest pewna „instrukcja nagłówkowa”, na przykład instrukcja for; jest zrozumiałe, iż instrukcja ta powinna rozpoczynać się w tej samej kolumnie, w której znajdują się nawiasy {} ograniczające podporządkowany jej blok, gdyż w ten sposób łatwo widoczny staje się jej koniec.

 

Do przesuwania w poziomie zaznaczonych bloków tekstu służą w IDE kombinacje klawiszy Ctrl+Shift+I oraz Ctrl+Shift+U. Wielkość pojedynczego „skoku” (jako liczba kolumn) określona jest za pomocą opcji Block indent na wspomnianej karcie General opcji edytora; ustawienie jej na 1 zapewnia oczywiście największą elastyczność.

Największe jednak korzyści z opisanego sposobu akapitowania kodu widoczne są w przypadku zagnieżdżonych instrukcji if-else. Oto fragment programu, badający, czy trzy liczby całkowite A, B i C mogą być długościami boków trójkąta i jeżeli tak, to jakiego rodzaju trójkąt utworzą:

 

#include <ostream>

 

// zakładamy, że A <= B <= C

if (A + B > C)

   if ((A==B) || (B==C))

   if ((A==B) && (B==C)) std::cout << "Trójkąt równoboczny";

else

if ((A*A + B*B) == C*C)

       std::cout << "Równoramienny trójkąt prostokątny";

   else std::cout << "Trójkąt równoramienny";

else

    if ( (A*A + B*B) == C*C )std::cout << "Trójkąt prostokątny";

    else std::cout << "Taki sobie trójkąt";

    else std::cout << "Odcinki o podanych długościach nie tworzą trójkąta";

 

 

Poza chaotycznym sposobem zapisu bezsensowne namiastki akapitowania mogą tu tylko wprowadzić w błąd nieuważnego czytelnika. Oto ten sam fragment kodu zapisany zgodnie ze wspomnianymi zasadami:

 

 

#include <ostream>

 

// zakładamy, że A <= B <= C

 

if (A + B > C)

{

    if ((A==B) || (B==C))

    {

        if ((A==B) && (B==C))

        {

            std::cout << "Trójkąt równoboczny";

        }

        else if ((A*A + B*B) == C*C)

        {

            std::cout << "Równoramienny trójkąt prostokątny";

        }

        else std::cout << "Trójkąt równoramienny";

    {

    else if ( (A*A + B*B) == C*C )

    {

        std::cout << "Trójkąt prostokątny";

    }

    else

    {

        std::cout << "Taki sobie trójkąt";

    }

}

else

{

    std::cout << "Odcinki o podanych długościach nie tworzą trójkąta";

}

 

 

Jeżeli już mowa o instrukcjach if-else, przy znacznym ich zagnieżdżeniu[1] korzystne może się okazać przekształcenie całej instrukcji if w instrukcję switch (o ile oczywiście jest to możliwe).

Pewną barierą ograniczającą swobodne akapitowanie jest ograniczona długość wiersza. Zbyt długie wiersze skutecznie utrudniają przeglądanie tekstu, zmuszając do ciągłego przewijania ekranu w lewo i prawo, stając się prawdziwym problemem w sytuacji drukowania tekstu. Wiersze przekraczające szerokość strony wydruku mogą być obcięte albo zawinięte – w obydwu przypadkach czytelność wydruku ulega niepotrzebnemu pogorszeniu. Edytor kodu zawiera orientacyjny wskaźnik prawego marginesu, którego przekroczenie może zwiastować problemy z wydrukiem. Położenie tego wskaźnika ustalić można za pomocą opcji edytora (Tools|Editor Options|Display|Right margin);  dla wydruku na kartce formatu A4 przy braku prawego marginesu (opcja File|Print|Left margin ustawiona na 0) maksymalna bezpieczna wartość dla prawego marginesu to 94. Wiersze wykraczające poza tak ustawiony margines mogą być obcinane lub zawijane (zależnie od opcji File|Print|Wrap lines).

Najpewniejszym sposobem radzenia sobie ze zbyt długimi wierszami jest oczywiście ich unikanie. Sprowadza się ono do umiejętnego dzielenia (w sensie przejścia do nowego wiersza) długich list parametrów funkcji, dzielenia długich łańcuchów na dwa (lub więcej) łańcuchy konkatenowane za pomocą operatora + itp. Niekiedy wystarczająca staje się drobna zmiana stylu kodowania – poniższą instrukcję:

 

switch(Key)

{

  case 'a' : // tutaj długa linia kodu...

             break;                    

  case 'b' : // tutaj długa linia kodu...

             break;

  default  : // tutaj długa linia kodu...

             break;

}

 

można przepisać do następującej postaci:

 

switch(Key)

{

  case 'a'

 

  : // tutaj długa linia kodu...

    break;                    

 

  case 'b'

  : // tutaj długa linia kodu...

    break;

 

  default 

: // tutaj długa linia kodu...

  break;

}

przez co oszczędza się kilka znaków na szerokości wiersza.

Dla instrukcji for i if identyczny efekt osiągnąć można, lokując jej blok wykonawczy w osobnym wierszu, czyli np. zamiast:

 

for(int i=0; i<10; ++i) // tutaj długa linia kodu...

...

if( Key == 'a' || Key == 'A') // inna długa linia kodu...

 

napisać można tak:

 

for(int i=0; i<10; ++i)

{

  // tutaj długa linia kodu...

}

 

...

 

if( Key == 'a' || Key == 'A')

{

// inna długa linia kodu...

}

 

Takie łamanie wierszy daje jeszcze jedną dodatkową korzyść: śledzenie programu, zorientowane jak wiadomo na poszczególne wiersze kodu, staje się dzięki temu bardziej selektywne – w jednym wierszu znajduje się teraz bowiem mniej kodu.

Długie wyrażenia warunkowe w instrukcjach if również powinny być łamane pomiędzy kilka wierszy, zawierających być może po jednym z warunków składowych, na przykład:

 

if(   Key == VK_UP

   || Key == VK_DOWN

   || Key == VK_LEFT

   || Key == VK_RIGHT

   || Key == VK_HOME

   || Key == VK_END

{

   instrukcje uwarunkowane

}

 

 

To nie przypadek, iż operatory || znajdują się na początku poszczególnych wierszy: po prostu czytając tekst od strony lewej do prawej możemy od razu stwierdzić, iż mamy do czynienia z alternatywą warunków. Umieszczenie operatora || na końcu wiersza informuje co prawda, iż wiersz ten nie jest kompletny i posiada kontynuację, jednakże informacja taka nie jest raczej przydatna dla ludzi, którzy (w przeciwieństwie do kompilatorów) zwyczajowo postrzegają całe bloki tekstu, nie zaś jego poszczególne wiersze.

Podobnie do złożonych wyrażeń warunkowych powinny być dzielone długie listy parametrów funkcji, na przykład:

 

Wydruk 2.2. Łamanie długiej listy parametrów funkcji

 

void DrawBoxWithMessage(const AnsiString &Message,

                        int Top,

                        int Left,

                        int Height,

                        int Width);

 

W przeciwieństwie jednak do łamania długich wyrażeń logicznych, przecinki oddzielające poszczególne parametry znajdują się na końcu poszczególnych wierszy. Mają one znaczenie wyłącznie dla kompilatora i nie niosą poza tym żadnej użytecznej informacji; umieszczenie przecinka na początku wiersza – czyli tam, gdzie zwyczajowo spodziewane jest słowo kluczowe lub identyfikator – powodowałoby tylko zbędne utrudnienia dla użytkownika czytającego tekst.

Tak samo podzielić można długi łańcuch na konkatenowane podłańcuchy:

 

Wydruk 2.3. Łamanie długiego łańcucha na konkatenowane podłańcuchy

 

 

AnsiString FilePath = "";

AnsiString FileName = "TestFile";

 

FilePath = "C:\\RootDirectory"

           + "\\"

           + "Branch\\Leaf"

           + FileName

           + ".txt";

 

 

Znak +, jako operator, znajduje się na początku każdego wiersza.

Dwa ostatnie przykłady wiążą się z wcięciami tekstu, co przez niektórych programistów może być traktowane jako zbędna fatyga; można wówczas zrezygnować z tak rygorystycznego wyrównywania poszczególnych wierszy, pisząc po prostu:

 

void DrawBoxWithMessage(const AnsiString &Message,

   int Top,

   int Left,

   int Height,

   int Width);

 

czy też:

 

FilePath = "C:\\RootDirectory"

  + "\\"

  + "Branch\\Leaf"

  + FileName

  + ".txt";

 

co – jakkolwiek nieco mniej czytelne – stanowi bądź co bądź niezły kompromis.

Znaczącą pomoc w kompletowaniu kodu, oszczędzającą wysiłek programisty, stanowią szablony kodu (code templates). Po wpisaniu (w edytorze kodu) charakterystycznej dla danego szablonu frazy początkowej i naciśnięciu Ctrl+J spowodujemy  kompletne wygenerowanie tegoż szablonu. Szablony dostarczane standardowo przez C++Buildera niosą ze sobą duży ładunek funkcjonalności, niemniej jednak dla utrzymania spójności kodowania własnych programów konieczne bywa zazwyczaj uzupełnienie tego repertuaru o własne szablony – można to zrobić, przechodząc na kartę Code Insight opcji edytora (Tools|Editor Options) i dokonując niezbędnych uzupełnień; edytując szablon należy zwrócić uwagę na to, iż znak pionowej kreski (|) w jego treści oznacza pozycję, w której ma się ustawić kursor bezpośrednio po wygenerowaniu kodu.
Edycję i uzupełnianie dostępnych szablonów kodu można też przeprowadzić w sposób bardziej bezpośredni – ich definicje znajdują się w postaci tekstowej w pliku bcb.dci zlokalizowanym w podkatalogu Bin katalogu, w którym zainstalowano C++Buildera.

Sugestywne nazewnictwo elementów programu

Kolejnym czynnikiem wpływającym na czytelność kodu programów jest tworzenie nazw elementów tegoż programu – typów, zmiennych i funkcji – w sposób odzwierciedlający przeznaczenie tych elementów. Analizując np. standardowe typy C++Buildera nie mamy przecież wątpliwości, iż typ int reprezentuje liczby całkowite (ang. integers), zaś pod typem TFont ukrywają się elementy definicji czcionki. Podobnie przykładowa definicja użytkownika

 

int LiczbaStron;

 

 

sugeruje wyraźnie, iż pod deklarowaną zmienną ukrywa się liczba stron jakiegoś tekstu, będąca z natury liczbą całkowitą.

 

Nazewnictwo zmiennych odzwierciedlające ich przeznaczenie

 

Zmienne o sugestywnych nazwach łatwiejsze są do zidentyfikowania w kodzie programu niż beznamiętne skrótowce: użycie do przechowywania nazwiska pracownika zmiennej NazwiskoPrac jest z pewnością bardziej celowe niż użycie w tej roli zmiennej o nazwie (na przykład) S, nic nie mówiącej o typie ani o przeznaczeniu zmiennej.

Wobec oczywistego faktu, iż oryginalne nazwy reprezentowanych zjawisk i obiektów ze świata rzeczywistego (np. „kwota podlegająca odrębnemu opodatkowaniu”) są zwykle zbyt długie, by można je było (sensownie) użyć w roli nazw zmiennych, istotne stają się sugestywne zasady skracania nazw. Istnieje kilka przyjętych praktyk w tym względzie – rozpoczynanie każdego członu nazwy wielką literą, oddzielanie poszczególnych członów znakami podkreślenia itp., tak, że wspomnianą „kwotę podlegającą odrębnemu opodatkowaniu” można by przechowywać pod zmienną opatrzoną np. jedną z następujących nazw:

 

 

KwotaOdrOpod

 

Kwota_Odr_Opod

 

kwotaOdrOpod

 

Kwota_odr_opod

 

 

Wszystko zależy od przyjętego sty...

Zgłoś jeśli naruszono regulamin