Spustit prezentaci

Doporučené postupy v programování

Práce s primitivy strukturovaného programování

Lubomír Bulej, David Majda

KDSS MFF UK

Základní primitiva

Sekvence

Selekce

Iterace

Steve McConnell o strukturovaném programování:

V jádru strukturovaného programování je jednoduchá myšlenka, že program by měl používat pouze řídící konstrukce s jedním vstupem (single-entry) a jedním výstupem (single-exit). Taková konstrukce představuje blok kódu, do kterého se vstupuje pouze v jednom místě, a ze kterého se také v jednom místě vystupuje.

Strukturovaný program postupuje vpřed uspořádaným a disciplinovaným způsobem, narozdíl od nekontrolovaného skákání tam a zpátky. Takový program se dá číst od shora dolů a v podstatě stejným způsobem je i vykonáván. To umožňuje lépe chápat co program dělá, zvyšuje to čitelnost a v důsledku vede ke kvalitnějšímu programu.

Základní tezí strukturovaného programování je, že řízení toku libovolného výpočtu je možné dosáhnout kombinací základních primitiv: sekvence, výběru a iterace. Přestože dnes existuje celá řada jazykových obratů pro zvýšení pohodlnosti, většiny pokroku v programování bylo dosaženo omezováním toho, co je programátorům povoleno.

Proto je nutné použití jiných než základních primitiv, které často porušují princip single-entry/single-exit, vnímat velmi kriticky a být schopen použití takových primitiv zdůvodnit.

Sekvence

Uspořádání sekvenčního kódu

Sekvenční kód ...

... to je prostě posloupnost příkazů ...

Sekvenční kód ...

... to je prostě posloupnost příkazů ...

... na tom přece není co zkazit ...

Příklad: sekvenční kód

Příklad 1

data = ReadData ();
results = CalculateResultsFromData (data);
PrintResults (results);

Příklad 2

revenue.ComputeMonthly ();
revenue.ComputeQuarterly ();
revenue.ComputeAnnual ();

Příklad 3

ComputeMarketingExpense ()
ComputeSalesExpense ()
ComputeTravelExpense ()
ComputePersonnelExpense ()
DisplayExpenseSummary ()

Příklad: sekvenční kód

Na pořadí záleží, závislost je vidět

data = ReadData ();
results = CalculateResultsFromData (data);
PrintResults (results);

Na pořadí záleží, ale závislost můžeme pouze tušit

revenue.ComputeMonthly ();
revenue.ComputeQuarterly ();
revenue.ComputeAnnual ();

Na pořadí záleží, ale závislost je skrytá

ComputeMarketingExpense ()
ComputeSalesExpense ()
ComputeTravelExpense ()
ComputePersonnelExpense ()
DisplayExpenseSummary ()
Jak poznáte, že ComputeMarketingExpense() také inicializuje data, do kterých ostatní procedury ukládají své výsledky, které nakonec použije DisplayExpenseSummary()?

Příklad: sekvenční kód

Na pořadí záleží, práce se stejnými daty je explicitní

InitializeExpenseData (expenseData)
ComputeMarketingExpense (expenseData)
ComputeSalesExpense (expenseData)
ComputeTravelExpense (expenseData)
ComputePersonnelExpense (expenseData)
DisplayExpenseSummary (expenseData)

Jak uspořádat sekvenčního kód?

Pokud na pořadí příkazů záleží

Pokud na pořadí příkazů nezáleží

Příklad: sekvenční kód

Závislost na pořadí se dá předpokládat

ComputeMarketingExpense (marketingData)
ComputeSalesExpense (salesData)
ComputeTravelExpense (travelData)
ComputePersonnelExpense (personnelData)
DisplayExpenseSummary (marketingData, salesData, travelData, personnelData)

Pořadí vynucené deklarací proměnných

Expense marketingExpense = ComputeMarketingExpense (marketingData)
Expense salesExpense = ComputeSalesExpense (salesData)
Expense travelExpense = ComputeTravelExpense (travelData)
Expense personnelExpense = ComputePersonnelExpense (personnelData)
DisplayExpenseSummary (
  marketingExpense, salesExpense, travelExpense, personnelExpense)

Selekce

Jednoduché větvení · Vnořené větvení · Vícenásobné větvení

Jednoduché větvení: if-then

Varianta if-then

Kdy použít větvení if-then?

Příklad: jednoduché větvení if-then

Kód nad rámec běžné činnosti

...
if (log.isDebugEnabled ()) {
  log.debug (...);
}
...
Item item = itemCache.get (key);
if (item == null) {
  item = createItem (key);
  itemCache.put (key, item);
}
...     

Příklad: jednoduché větvení if-then

Kód nad rámec běžné činnosti

...
if (log.isDebugEnabled ()) {
  log.debug (...);
}
...
Item item = itemCache.get (key);
if (item == null) {
  item = createItem (key);
  itemCache.put (key, item);
}
...     

Opravdu není else potřeba?

void setSamplePeriod (long samplePeriod) {
  if (samplePeriod > 0) {
    this.samplePeriod = samplePeriod;
  }
}       

Jednoduché větvení: if-then-else

Varianta if-then-else

Který případ do větve if?

Ternární operátor

Příklad: jednoduché větvení if-then-else

Kód bližší primární funkci

if (snapshot.sampleCount != 0) {
  snapshot.average = runningTotal / snapshot.sampleCount;
} else {
  snapshot.average = 0;
}       
probe = probeFactory.create (probeKind);
if (probe != null) {
  registerProbe (probe);
} else {
  log.error (..., probeKind);
}       

Příklad: jednoduché větvení if-then-else

Kód bližší primární funkci

if (snapshot.sampleCount != 0) {
  snapshot.average = runningTotal / snapshot.sampleCount;
} else {
  snapshot.average = 0;
}       
probe = probeFactory.create (probeKind);
if (probe != null) {
  registerProbe (probe);
} else {
  log.error (..., probeKind);
}       

Ošetření chyb v if větvi

probe = probeFactory.create (probeKind);
if (probe == null) {
  log.error (..., probeKind);
} else {
  registerProbe (probe);
}       

Vnořená větvení

Kombinace if-then a if-then-else

...
if () {
  ...
  if () {
    ...
  }
}
...       
...
if () {
  ...
  if () {
    ...
  } else {
    ...
  }
} else {
  ...
}
...       

Příklad: vnořené větvení

Nesystematické zpracování chyb (VisualBasic)

OpenFile (inputFile, status)
If (status = Status_Error) Then
  errorType = ErrorType_FileOpenError
Else
  ReadFile (inputFile, fileData, status)
  If (status = Status_Success) Then
    SummarizeFileData (fileData, summaryData, status)
    If (status = Status_Error) Then
      errorType = ErrorType_DataSummaryError
    Else
      PrintSummary (summaryData)
      SaveSummaryData (summaryData, status)
      If (status = Status_Error) Then
        errorType = ErrorType_SummarySaveError
      Else
        UpdateAllAccounts ()
        EraseUndoFile ()
        errorType = ErrorType_None
      End If
    End If
  Else
    errorType = ErrorType_FileReadError
  End If
End If  

Příklad: vnořené větvení

Systematické zpracování chyb (VisualBasic)

OpenFile (inputFile, status)
If (status = Status_Success) Then
  ReadFile (inputFile, fileData, status)
  If (status = Status_Success) Then
    SummarizeFileData (fileData, summaryData, status)
    If (status = Status_Success) Then
      PrintSummary (summaryData)
      SaveSummaryData (summaryData, status)
      If (status = Status_Success) Then
        UpdateAllAccounts ()
        EraseUndoFile ()
        errorType = ErrorType_None
      Else
        errorType = ErrorType_SummarySaveError
      End If
    Else
      errorType = ErrorType_DataSummaryError
    End If
  Else
    errorType = ErrorType_FileReadError
  End If
Else
  errorType = ErrorType_FileOpenError
End If  

Vícenásobné větvení: switch

Rozvětvení jednom bodě

Odlišná síla a bezpečnost v různých jazycích

Pozor na switch

Switch v C/C++

switch (inputVar) {

case 'A': if (test) {
            // statement 1
            // statement 2
case 'B':   // statement 3
            // statement 4
          }

          break;
}         

Typická realizace příkazu switch

Větve by měly být oddělené

Příklad: kumulativní operace

Akceptovatelné použití propadávání

switch (errorDocumentationLevel) {
case DocumentationLevel.FULL:
    displayErrorDetails (errorNumber);
    // Fall through: FULL level also prints summary

case DocumentationLevel.SUMMARY:
    displayErrorSummary (errorNumber);
    // Fall through: SUMMARY level also prints error number

case DocumentationLevel.NUMBER_ONLY:
    displayErrorNumber (errorNumber);
    break;

default:
    throw new AssertionError ("Invalid documentation level.");
}

Nicméně...

Použití konstrukce switch

Řazení případů

Akce pro jednotlivé případy

Řídící proměnná/výraz

Příklad: neúplná řídící data

Nevhodné použití slabé konstrukce switch

action = userCommand [0];
switch (action) {
case 'c':
    Copy ();
    break;

case 'd':
    DeleteCharacter ();
    break;

case 'f':
    Format ();
    break;

case 'h':
    Help ();
    break;

...
default:
    HandleUserInputError (ErrorType.InvalidUserCommand);
}       

Použití default případu

Nepoužívejte falešný default

Vždy mějte default případ

Příklad: použití default případu

Detekce opomentých případů

switch (transaction.type) {

case TransactionType.DEPOSIT:
    ProcessDeposit (transaction);
    break;

case TransactionType.WITHDRAWAL:
    ProcessWithdrawal (transaction);
    break;

case TransactionType.TRANSFER:
    ProcessTransfer (transaction);
    break;

case TransactionType.NOOP:
    // no processing required
    break;

default:
    LogTransactionError ("Unknown transaction type", transaction);
}       

Vícenásobné větvení: if-then-elseif

Kaskáda if-then-elseif-...-else

Použití stejné jako u switch

Příklad: náhrada slabého switch

Náhrada switch bez podpory řetězců

if (userCommand.equals (COMMAND_STRING_COPY)) {
    Copy ();
} else if (userCommand.equals (COMMAND_STRING_DELETE)) {
    Delete ();
} else if (userCommand.equals (COMMAND_STRING_FORMAT)) {
    Format ();
} else if (userCommand.equals (COMMAND_STRING_HELP)) {
    Help ();
} else {
    HandleUserInputError (ErrorType.InvalidCommandInput);
}       

Příklad: aritmetické porovnání

Varianta 1

int compare (int a, int b) {
  if (a < b) {
    return -1;
  } else if (a > b) {
    return 1;
  } else {
    return 0;
  }
}         

Varianta 2

int compare (int a, int b) {
  if (a < b) {
    return -1;
  } else if (a > b) {
    return 1;
  }

  return 0;
}         

Varianta 3

int compare (int a, int b) {
  if (a < b) {
    return -1;
  }
  if (a > b) {
    return 1;
  }
  return 0;
}         

Varianta 4 (Perl 6)

sub compare ($$) {
  given ($1) {
    when ($_ < $2) {
      return -1;
    }
    when ($_ > $2) {
      return 1;
    }
    default {
      return 0;
    }
  }
}         

Iterace

Základní druhy cyklů

S pevně daným počtem opakování

S flexibilním počtem opakování

Klíčová slova v tuto chvíli nejsou podstatná i když je zjevné, že uvedené základní typy cyklů se typicky realizují pomocí konstrukcí for, for-each, while a do-while.

Nicméně velké množství cyklů spadá do kategorie s flexibilním počtem opakování, protože jsou v nich používány testy uprostřed těla cyklu, které umožňují buď předčasně ukončit provádění těla cyklu skokem na začátek cyklu, nebo předčasně ukončit celý cyklus skokem za něj.

Řízení provádění cyklu

Co se dá na cyklu zkazit?

Zásady uvedené na následujících slajdech mají za cíl těmto chybám předcházet.

Jak udržet cykly pod kontrolou?

Minimalizovat počet věcí ovlivňující cyklus

Vnitřek cyklu = černá skříňka

Jak udržet cykly pod kontrolou?

Minimalizovat počet věcí ovlivňující cyklus

Vnitřek cyklu = černá skříňka

while (!inputFile.EndOfFile () && moreDataAvailable) {
                                                   
                                                   
                                                   
                                                   
}        

Úvodní část cyklu

Vstup do cyklu

Inicializační kód

Řídící struktura cyklu

Řídící část včetně inicializace na jednom místě

V hlavičce for pracovat pouze se řídící proměnnou

Testovací část bez vedlejších efektů

Nekonečné cykly – typicky event handlery

Příklad: přečtení záznamů ze souboru

While nevhodně maskovaný jako for

// read all the entries from a file
for (logFile.MoveToStart (), entryCount = 0; !logFile.EndOfFile;
  entryCount++) {

  logFile.getEntry ();
}       

Příklad: přečtení záznamů ze souboru

While nevhodně maskovaný jako for

// read all the entries from a file
for (logFile.MoveToStart (), entryCount = 0; !logFile.EndOfFile;
  entryCount++) {

  logFile.getEntry ();
}       

Vhodnější použití for, nejasný význam

entryCount = 0;
for (logFile.MoveToStart (); !logFile.EndOfFile(); logFile.getEntry()) {
  entryCount++;
}        

Příklad: přečtení záznamů ze souboru

While nevhodně maskovaný jako for

// read all the entries from a file
for (logFile.MoveToStart (), entryCount = 0; !logFile.EndOfFile;
  entryCount++) {

  logFile.getEntry ();
}       

Vhodnější použití for, nejasný význam

entryCount = 0;
for (logFile.MoveToStart (); !logFile.EndOfFile(); logFile.getEntry()) {
  entryCount++;
}        

Použití řídící struktury while

entryCount = 0;
logFile.MoveToStart ();
while (!logFile.EndOfFile ()) {
  logFile.GetEntry (&logEntry);
  putEntry (entryCount, logEntry);
  entryCount++;
}        

V prvním případě příkazy týkající se proměnné entryCount vzbuzují dojem, že tato proměnná se podílí na řízení cyklu, přestože cyklus posouvá vpřed volání logFile.getEntry(), které je naopak v těle cyklu.

Druhý případ ukazuje v principu správné a logické použití příkazu for, jelikož v hlavě cyklu jsou pouze příkazy, které tento cyklus řídí, zatímco příkazy týkající se proměnné entryCount jsou umístěny před tělem (inicializace) a v těle (aktualizace) cyklu.

S ohledem na potenciální nutnost testovat, zda se záznam povedlo přečíst, je však v tomto případě nejvhodnější použití příkaz while. Příkaz for by měl být primárně využíván pro cykly, jejichž řízení je jednoduché a po přečtění hlavy cyklu není třeba se jím příliš zabývat.

Příklad: přeskočení řádku na vstupu

Cyklus s vedlejším efektem v testu

while ((inputChar = cin.get ()) != '\n') {
  ;
}        

Test bez vedlejšího efektu

do {
  inputChar = cin.get ();
} while (inputChar != '\n');

Příklad: čtení řádku ze vstupu

Cyklus s vedlejším efektem v testu

while ((inputChar = cin.get ()) != '\n') {
  inputLine.append (inputChar);
}        

Příklad: čtení řádku ze vstupu

Cyklus s vedlejším efektem v testu

while ((inputChar = cin.get ()) != '\n') {
  inputLine.append (inputChar);
}        

Test bez vedlejšího efektu

do {
  inputChar = cin.get ();
  if (inputChar == '\n') {
     break;
  }

  inputLine.append (inputChar);
} while (true);

Příklad: čtení řádku ze vstupu

Cyklus s vedlejším efektem v testu

while ((inputChar = cin.get ()) != '\n') {
  inputLine.append (inputChar);
}        

Test bez vedlejšího efektu

do {
  inputChar = cin.get ();
  if (inputChar == '\n') {
     break;
  }

  inputLine.append (inputChar);
} while (true);

Kratší verze s malou duplicitou

inputChar = cin.get ();
while (inputChar != '\n') {
  inputLine.append (inputChar);
  inputChar = cin.get ();
}      

Výkonná část cyklu

Cyklus by měl mít pouze jednu funkci

Struktura těla cyklu

Délka těla cyklu

Ukončení cyklu

Ujistěte se, že cyklus skončí

Snažte se učinit podmínky ukončení zjevné

Nepoužívejte řídící proměnné mimo cyklus

Příklad: závislost na řídící proměnné

Nevhodné použití řídící proměnné

for (recordCount = 0; recordCount < MAX_RECORDS; recordCount++) {
  if (entry [recordCount] == testValue) {
    break;
  }
}

// lots of code
...
if (recordCount < MAX_RECORDS) {
  return true;
} else {
  return false;
}       

Příklad: závislost na řídící proměnné

Přiřazení do speciální proměnné

found = false;
for (recordCount = 0; recordCount < MAX_RECORDS; recordCount++) {
  if (entry [recordCount] == testValue) {
    found = true;
    break;
  }
}

// lots of code
...
return found;

Příklad: závislost na řídící proměnné

Přiřazení do speciální proměnné

found = false;
for (recordCount = 0; recordCount < MAX_RECORDS; recordCount++) {
  if (entry [recordCount] == testValue) {
    found = true;
    break;
  }
}

// lots of code
...
return found;

Následující krok ke zvážení: extrakce cyklu do metody

Předčasné ukončení cyklu

Příkazy break a continue

Použijte continue k přeskočení zbytku těla

Použijte break k ukončení cyklu

Použití continue mi nepřijde zdaleka intruzivní jako break. Pokud pracujeme s konceptem těla cyklu jako funkce, continue představuje předčasný návrat z funkce, což je poměrně běžný jev a uživatele nemusí tolik zajímat – funkce prostě před zpracováním dat ověří, že je na nich co zpracovávat.

Oproti tomu break je jakousi analogií výjimky a jeho použití v těle cyklu se dá přirovnat k vyvolání výjimky ve funkci, jejíž volající výjimku neošetřuje a nechává ji projít až o úroveň výš, v našem případě tedy ven z cyklu cyklu. Proto break činí řízení cyklu složitějším a jeho použití je nutné umět zdůvodnit.

Příklad: break v bloku do-switch-if

Chybné použití break

do {
  ...
  switch (...) {
    ...
    if (...) {
      ...
        break;
      ...
    }
    ...
  }
  ...
} while (...);

Příklad: break v bloku do-switch-if

Chybné použití break

do {
  ...
  switch (...) {
    ...
    if (...) {
      ...
        break;
      ...
    }
    ...
  }
  ...
} while (...);

Použití break s návěštím

do {
  ...
  switch (...) {
    ...
    CALL_CENTER_DOWN:
    if (...) {
      ...
        break CALL_CENTER_DOWN;
      ...
    }
    ...
  }
  ...
} while (...);

Řídící proměnné

Obecná doporučení (viz. práce s proměnnými)

Vyhněte se explicitním řídícím proměnným

Příklad: procházení pole v Javě

S použitím explicitní řídící proměnné

for (int i = 0; i < a.length; i++) {
  System.out.println (a [i]);
}       

S použitím implicitní řídící proměnné

for (String s: a) {
  System.out.println (s);
}       

Další řídící struktury

Explicitní návrat z funkce · GoTo

Explicitní návrat z funkce

Použijte return pokud zlepší čitelnost

Použijte ochranné podmínky pro zjednodušení obsluhy chyb

Příklad: předčasný návrat z funkce

Obsluha chyb skrývá normální kód

If file.validName () Then
  If file.Open () Then
    If encryptionKey.valid () Then
      If file.Decrypt (encryptionKey) Then
        ' lots of code
        ...
      End If
    End If
  End If
End If  

Příklad: předčasný návrat z funkce

Obsluha chyb skrývá normální kód

If file.validName () Then
  If file.Open () Then
    If encryptionKey.valid () Then
      If file.Decrypt (encryptionKey) Then
        ' lots of code
        ...
      End If
    End If
  End If
End If  

Obsluha chyb s předčasným návratem, ideální

' set up, bailing out if errors are found
If Not file.validName () Then Exit Sub
If Not file.Open () Then Exit Sub
If Not encryptionKey.valid () Then Exit Sub
If Not file.Decrypt (encryptionKey) Then Exit Sub

' lots of code
...     

Příklad: předčasný návrat z funkce

Obsluha chyb s předčasným návratem, realistická

' set up, bailing out if errors are found
If Not file.validName () Then
  errorStatus = FileError.InvalidFileName
  Exit Sub
End If

If Not file.Open () Then
  errorStatus = FileError.CannotOpenFile
  Exit Sub
End If

If Not encryptionKey.valid () Then
  errorStatus = FileError.InvalidEncryptionKey
  Exit Sub
End If

If Not file.Decrypt (encryptionKey) Then
  errorStatus = FileError.CannotDecryptFile
  Exit Sub
End If

' lots of code
...     

GoTo. GoTo? GoTo.

GoTo žije – dokonce i v jazyce Ada

Proč je goto špatné?

Proč se goto hodí?

Kdy použít GoTo?

Modelová situace

Standardní řešení pomocí if-then

Jiná řešení

Příklad: goto při obsluze chyb

process_t process_create (...) {
    ...
    proc_thread = (struct thread *) kmalloc (sizeof (struct thread));
    if (proc_thread == NULL)
        goto fail_exit;
    ...
    proc_vmm = vmm_alloc ();
    if (proc_vmm == NULL)
        goto fail_clean_thread;
    ...
    result = vmm_vmalloc (proc_vmm, & program_image, ...)
    if (result != EOK)
        goto fail_clean_vmm;
    ...
    proc_stack = vmm_alloc_stack (proc_vmm, ...);
    if (proc_stack != NULL)
        goto fail_clean_image;
    ...
    result = copy_to_vm (proc_vmm, proc_image, ...);
    if (result != EOK)
        goto fail_clean_stack;
    ...
    // now we can finally initialize the process
    ...
    return (process_t) proc_thread;

    // Here comes error handling
fail_clean_stack:
    vmm_vfree (proc_vmm, proc_stack);
fail_clean_image:
    vmm_vfree (proc_vmm, proc_image);
fail_clean_vmm:
    vmm_free (proc_vmm);
fail_clean_thread:
    kfree (thread);
fail_exit:
    return NULL;
}       

Jak používat GoTo?

Jak používat GoTo?

Nepoužívat!

Jak používat GoTo?

Nepoužívat!

Kdy a jak se tedy uchýlit?

Obecné poznámky k řízení toku

Logické výrazy · Numerická porovnání · Zkrácené vyhodnocování · Zjednodušování výrazů

Logické výrazy

Používejte vhodný typ a hodnoty True a False

Používejte implicitní porovnání

Zjednodušení složitých výrazů

Rozdělte složité testy na více částí

Přesuňte složité výrazy do funkcí

Přesuňte logiku do rozhodovacích tabulek

Příklad: zjednodušení podmínky

Špatně čitelná podmínka

if (document.atEndOfStream() && !inputError
    && lineCount >= MIN_LINES && lineCount <= MAX_LINES
    && !errorProcessing()) {
  /* ... */
}       

Zjednodušená varianta

boolean allDataRead =
  document.atEndOfStream() && !inputError;
boolean legalLineCount =
  (lineCount >= MIN_LINES) && (lineCount <= MAX_LINES);

if (allDataRead && legalLineCount && !errorProcessing()) {
  /* ... */
}       

Konstrukce logických výrazů

Výrazy formulujte pozitivně

Používejte závorky pro vyjasnění výrazů

Vyhodnocování logických výrazů

Úplné vs. zkrácené (short-circuit) vyhodnocení

Časté použití (&&)

Raději nepoužívat

Numerická porovnání

Uspořádání podle číselné osy

Jiný pohled

Porovnání s nulovým prvkem

Test na hodnotu False implicitní

Porovnání s hodnotami 0, '\0' a NULL explicitní

Smíšená porovnání spíše explicitní

Ostatní drobnosti

Konstanty v podmínce nalevo

Používejte { a } i když je v těle jen jeden příkaz