Spustit jako prezentaci

Dokonalý kód

Umění programování a techniky tvorby software

David Majda

MFF UK

Otázka na úvod

Co je to programování?

Otázka na úvod

Co je to programování?

Problém

O čem bude tento seminář?

"Ukážeme si praktické programátorské techniky, které vedou k psaní přehlednějšího, kvalitnějšího a lépe udržovatelného kódu. Vysvětlíme si, proč je používat, a předvedeme si spoustu dobrých i špatných příkladů."

Kód = nejdůležitější část software?

O čem bude tento seminář?



Činnosti při tvorbě softwaru - obecně

O čem bude tento seminář?



Činnosti při tvorbě softwaru - jak je budeme probírat

Osnova

  1. Design software, inherentní a zavlečená složitost.
  2. Zásady pro práci s primitivy strukturovaného programování.
  3. Design metod: pseudokód, lokálnost × duplicita kódu, data-driven programming, práce s výjimkami.
  4. Design tříd: dědičnost × kompozice, coupling a decoupling, modularizace a vrstvy abstrakce, desgin rozhraní (API).
  5. Refaktorizace. Defenzivní programování, názvové konvence a dokumentace.
  6. Testování, unit-testing, ladění.
  7. Plánování a udržovatelnost software.
Osnova je předběžná!

Pro koho je seminář určen?

Ideálně:

Méně ideálně:

Anketa, kolik lidí je z jakého ročníku

Organizace

Zápočet

Slajdy jsou vytvořené pomocí frameworku S5 (http://meyerweb.com/eric/tools/s5/)

Jazyk

Jazyk používaný na semináři a v úkolech = Java

Další jazyky

Občas odbočíme i k jiným jazykům:

Ruby

Literatura

Steve McConnell: Code Complete 2

Joshua Bloch: Effective Java Programming Language Guide

Internetové zdroje

Artima
www.artima.com
Joel on Software
www.joelonsoftware.com
Paul Graham
www.paulgraham.com
Stevey's (Drunken) Blog Rants
steve-yegge.blogspot.com
steve.yegge.googlepages.com/blog-rants

+ odkazy roztroušené po slajdech

Související přednášky

Softwarové inženýrství (SWI026)

Návrhové vzory (PRG024)

Související přednášky

Testování software (TIN070)

Dotazy?

"Routine from hell"

void HandleStuff( CORP_DATA & inputRec, int crntQtr,
   EMP_DATA empRec, double & estimRevenue, double ytdRevenue,
   int screenX, int screenY, COLOR_TYPE & newColor,
   COLOR_TYPE & prevColor, StatusType & status, int expenseType )
{

int i;
for ( i = 0; i < 100; i++ ) {
   inputRec.revenue[i] = 0;
   inputRec.expense[i] = corpExpense[ crntQtr ][ i ];
   }
UpdateCorpDatabase( empRec );
estimRevenue = ytdRevenue * 4.0 / (double) crntQtr;
newColor = prevColor;
status = SUCCESS;

if ( expenseType == 1 ) {
     for ( i = 0; i < 12; i++ )
           profit[i] = revenue[i] - expense.type1[i];
     }
else if ( expenseType == 2 ) {
          profit[i] = revenue[i] - expense.type2[i];
          }

else if ( expenseType == 3 )
          profit[i] = revenue[i] - expense.type3[i];
          }

Co je na tomto kódu špatně? Podle Steve McConnella:

  • The routine has a bad name. HandleStuff() tells you nothing about what the routine does.
  • The routine isn't documented.
  • The routine has a bad layout. The physical organization of the code on the page gives few hints about its logical organization. Layout strategies are used haphazardly, with different styles in different parts of the routine. Compare the styles where expenseType == 2 and expenseType == 3.
  • The routine's input variable, inputRec, is changed. If it's an input variable, its value should not be modified (and in C++ it should be declared const). If the value of the variable is supposed to be modified, the variable should not be called inputRec.
  • The routine reads and writes global variables it reads from corpExpense and writes to profit. It should communicate with other routines more directly than by reading and writing global variables.
  • The routine doesn't have a single purpose. It initializes some variables, writes to a database, does some calculations – none of which seem to be related to each other in any way. A routine should have a single, clearly defined purpose.
  • The routine doesn't defend itself against bad data. If crntQtr equals 0, the expression ytdRevenue * 4.0 / (double) crntQtr causes a divide-by-zero error.
  • The routine uses several magic numbers: 100, 4.0, 12, 2, and 3.
  • Some of the routine's parameters are unused: screenX and screenY are not referenced within the routine.
  • One of the routine's parameters is passed incorrectly: prevColor is labeled as a reference parameter (&) even though it isn't assigned a value within the routine.
  • The routine has too many parameters. The upper limit for an understandable number of parameters is about 7; this routine has 11. The parameters are laid out in such an unreadable way that most people wouldn't try to examine them closely or even count them.
  • The routine's parameters are poorly ordered and are not documented.

A já dodávám:

  • Názvy parametrů a lokálních proměnných používají zkratky.
  • Názvy typů v parametrech jsou nekonzistentní (_TYPE × _DATA) kryptické.
  • Deklarace řídící proměnné cyklu (int i) by měla být v cyklu samotném.
  • Testy hodnoty parametru expenseType by měly být realizovány příkazem switch.

Proč vlastně psát kvalitní kód?

Technology debt

Motivace individuálního programátora

Proč je psaní dobrého software těžké?

Inherentní × zavlečená složitost

Inherentní složitost

Zavlečená složitost

Odlišení druhů složitosti

"Barevný kód"

Cíl designu software

  1. Minimalizovat množství inherentní složitosti, kterou se musíme zabývat v jednom okamžiku
  2. Globálně minimalizovat složitost zavlečenou
    • Použitím expresivnějšího jazyka
      • Příklad: Vytiskni setříděný seznam řetězců (Java × Ruby)
    • Použitím pokročilejší technologie
      • Příklad: "chytré ukazatele" v C++
    • Odsunutím do knihoven
      • Příklad: Generování unikátního ID

Příklad: Vytiskni setříděný seznam

Java

List<String> myList = new ArrayList<String>();
myList.add("one");
myList.add("two");
myList.add("three");

Collections.sort(myList, new Comparator<String>() {
  public int compare(String a, String b) {
    return a.length() - b.length();
  }
});

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

Příklad: Vytiskni setříděný seznam

Java

List<String> myList = new ArrayList<String>();
myList.add("one");
myList.add("two");
myList.add("three");

Collections.sort(myList, new Comparator<String>() {
  public int compare(String a, String b) {
    return a.length() - b.length();
  }
});

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

Příklad: Vytiskni setříděný seznam

Java

List<String> myList = new ArrayList<String>();
myList.add("one");
myList.add("two");
myList.add("three");

Collections.sort(myList, new Comparator<String>() {
  public int compare(String a, String b) {
    return a.length() - b.length();
  }
});

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

Ruby

puts ["one", "two", "three"].sort_by { |item| item.length }

Příklad: Vytiskni setříděný seznam

Java

List<String> myList = new ArrayList<String>();
myList.add("one");
myList.add("two");
myList.add("three");

Collections.sort(myList, new Comparator<String>() {
  public int compare(String a, String b) {
    return a.length() - b.length();
  }
});

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

Ruby

puts ["one", "two", "three"].sort_by { |item| item.length }

Vlastnosti dobrého designu software

Některé vlastnosti jsou přímo proti sobě... takový je zkrátka život.

Five worlds

Ne všechny zde probírané věci platí pro všechny typy software

Typy software

  1. Krabicový
  2. Interní
  3. Embedded
  4. Hry
  5. Na jedno použití
  6. "Servware"

Krabicový software

Interní software

Embedded software

Opravdu je update tak obtížný? http://jwz.livejournal.com/730821.html

Hry

Software na jedno použití

"Servware"

"Servware"

"Servware"

Zásady pro práci s primitivy strukturovaného programování

Konstanty · Proměnné · Podmínky · Cykly

Organizačně technické záležitosti

Konstanty

Co je konstanta?

Proč používat konstanty?

Proč používat konstanty?

Zažil jsem programátora, kterého při programování systému pro správu elektronického obchodu nenapadlo definovat konstantu pro DPH. Několik měsíců nato se sazba DPH změnila z 22 % na 19 % – grepování kódu na 1.22 a převrácenou hodnotu (onoho programátora nenapadlo psát všude 1 / 1.22) nebylo nic příjemného.

Musí být každý literál konstanta?

Existují výjimky

0 Inicializace součtů, akumulátorů, indexů při for-cyklu
1 Inicializace součinů, indexů při for-cyklu, posuny o jedničku, odečítání od konce
  • endChar = str.charAt(str.length() - 1)
2 Půlení intervalů, průměry

+ Pokud by použití konstanty výrazně snížilo čitelnost kódu

Konstanty porušují princip lokality

Princip lokality

Věci, které spolu souvisí, mají být v kódu u sebe.

Každé porušení principu lokality znamená, že programátor bude muset pobíhat po zdrojovém kódu sem a tam, aby vyhledal všechny potřebné informace ke kusu kódu, na kterém právě pracuje. To narušuje koncentraci a zdržuje, a tedy snižuje produktivitu.

Dnes situaci vylepšují "chytrá" IDE, kde se po najetí nad nějaký objekt zobrazí jeho definice nebo se na ni dá rychle odskočit a vrátit se zpět.

Názvy konstant

Název konstanty by měl přesně a úplně popisovat, co konstanta představuje

Výčtové typy

Pokud jazyk má (C, C++, Java, C# ano), používat

Příklad (Java)

public enum LogLevel { INFO, WARNING, ERROR }

Výčtové typy

Pokud jazyk nemá, emulovat konstantami

Příklad (C)

#define LL_INFO    0
#define LL_WARNING 1
#define LL_ERROR   2
#define LOG_LEVEL_FIRST   0
#define LOG_LEVEL_INFO    0
#define LOG_LEVEL_WARNING 1
#define LOG_LEVEL_ERROR   2
#define LOG_LEVEL_LAST    2

Proměnné

Deklarace a inicializace proměnných

Povinná deklarace?

Deklarace a inicializace proměnných

Libovolné umístění deklarace?

Inicializace zároveň s deklarací?

Mám na mysli "klasický" Visual Basic – jak je to u verze .NET, netuším.

Oblast platnosti proměnné (scope)

Minimalizace variable span/lifetime

Deklarujte/inicializujte proměnné co nejblíže jejich použití

Seskupte příkazy pracující se stejnými proměnnými

U proměnných obecně postupuje od nejmenšího možného rozsahu směrem k většímu

High level pohled

Zásada minimalizace stavového prostoru

Čím menší je stavový prostor programu, tím lépe.

Pravidlo optimalizace pro čtenáře

Šetřte čas čtenáře/upravujícího na úkor pisatele.

Aplikace na rozsahy platnosti

Drobnosti

Nepoužívejte jednu proměnnou pro více účelů

Vyhněte se skrytým významům

Časté je využívání dolních bitů ukazatelů, protože ty dnes bývají zarovnané na 4 bajty.

Hodí se to například u interpretů jazyků, kde jsou potřeba rychlé operace s celými čísly. Podle dolních bitů se rozhodne, zda se v horní části daných 4 bajtů nachází číslo, nebo ukazatel na nějaký složitější objekt. V případě čísla se tak ušetří jedna dereference (za cenu, že takové číslo nemůže zabrat celých 32 bitů).

Pokud jste se někdy divili, proč má JavaScript nebo Ruby o jeden bit menší celá čísla, než byste čekali, teď už víte proč.

Drobnosti

Je-li to možné, použijte final resp. const

Je-li to možné, deklarujte proměnné jako interfacy

Viz Effective Java: Programming Language Guide, Item 34.

Názvy proměnných

"You can't give a variable a name the way you give a dog a name – because it's cute or it has a good sound." – Steve McConnell

Název proměnné by měl přesně a úplně popisovat, co proměnná představuje

Názvy proměnných – příklady

Příliš dlouhé

Příliš krátké

Tak akorát

Booleovksé proměnné

Název by měl implikovat booleovskost

Pozor na předponu "is"

Název by měl být pozitivní

Zkratky

Pokud možno nepoužívejte zkratky

Častá opozita

Příklad: Symetrie

C/C++

if (item->prev) { item->prev->next = item->next; }
if (item->next) { item->next->prev = item->prev; }

Čemu se v názvech vyhnout

Vyhněte se nevhodným názvům dočasných proměnných

Vyhněte se názvům s podobným významem

Vyhněte se očíslovaným proměnným

Vyhněte se názvům lišícím se jen velikostí písmen

Podmínky

Druhy podmínek

Příkaz if

Příkaz switch


Ternární operátor

Používám céčkové názvy podmínkových příkazů, příkaz switch bývá v některých jazycích někdy nazýván jinak.

Příkaz if

Dilemata spojená s příkazem if

U mnoha z těchto dilemat se můj a McConnelův názor liší. Možná jsem se tady zatím málo spálil, možná vycházím z trochu jiných principů (aby se kód četl přirozeně × zabránění "hloupým chybám"). Názor si zde musíte utvořit sami.

Kdy použít variantu s else?

Příklad (Java)

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

vs.

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

Kdy použít variantu s else?

Obecně: Obvyklý, "pozitivní" případ by měl mít nejkratší cestu v kódu

Příklad: Pojídač komentářů

C/C++

if (*curr_pos == '/') {
  curr_pos++;
  if (*curr_pos == '/') {
    curr_pos++;
    while (*curr_pos && !is_newline(*curr_pos)) {
      curr_pos++;
    }
  } else {
    return NULL;
  }
} else {
  return NULL;
}
if (*curr_pos != '/') { return NULL; }
curr_pos++;

if (*curr_pos != '/') { return NULL; }
curr_pos++;

while (*curr_pos && !is_newline(*curr_pos)) { curr_pos++; }
Příklad je z mého zápočtového programu z C z druháku. Samozřejmě jsem tehdy použil první variantu :-)

Který případ do které větve?

Do if-větve dát případ...

Který případ do které větve?

Do if-větve dát případ...

Jak na numerická porovnání?

Příklad (JavaScript)

if (slideIndex >= 0 && slideIndex < slides.length) { ... }

Spoléhat na short-circuit evaluation?

Co to je?

Typické použití (&&)

Příklad (Java)

if (s != null && s.length() > 0) { ... }

Spoléhat na short-circuit evaluation?

Argumenty pro

Argumenty proti

Názory

Být explicitní u true/false-like?

Argumenty pro

Argumenty proti

Názory

Složité podmínky

Vyhněte se složitým podmínkám

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

Java

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


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

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

Drobnosti

Konstanty v podmínce nalevo

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

Závorky navíc neškodí

if/then/elseif/.../else

Řazení variant

Vždy mějte finální else

Příkaz switch

Příkaz switch

Příklad (Ruby)

case inputLine
  when "debug"
    dumpDebugInfo
    dumpSymbols

  when /p\s+(\w+)/
    dumpVariable($1)

  when "quit", "exit"
    exit

  else
    print "Illegal command: #{inputLine}"
end

break v příkazu switch

Příklad (C/C++)

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

break v příkazu switch

Propadávání se vyhněte

Příklad (Java)

switch (errorDocumentationLevel) {
  case DocumentationLevel.FULL:
    displayErrorDetails(errorNumber);
    // Fall through
  case DocumentationLevel.SUMMARY:
    displayErrorSummary(errorNumber);
    // Fall through
  case DocumentationLevel.NUMBER_ONLY:
    displayErrorNumber(errorNumber);
    break;
  default:
    throw new AssertionError("Invalid level.");
}

Případy

Řazení případů

Akce v případech jednoduché

Případ default

Nepoužívejte "falešné" defaulty

Vždy mějte default

Ternární operátor

Ternární operátor

Cykly

Rozdělení cyklů

Co se dá na cyklu zkazit?

Zásady uvedené na následujících slajdech mají za cíl se těchto chyb vyvarovat.

Obecné zásady

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

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

Explicitní řídící proměnná

Pokud to jazyk umožňuje, vyhněte se explicitní řídící proměnné

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

Java

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

Příklad: Výpis 5 řetězců

Java

for (int i = 0; i < 5; i++) {
  System.out.println("Hello!");
}

Ruby

5.times { puts "Hello!" }

Řídící proměnné

Používejte snadradní názvy

Řídící proměnné

Nesahejte na řídící proměnnou uvnitř for cyklu

Nesahejte na řídící proměnnou po skončení for ckylu

Strukturální záležitosti

Ne víc jak 3 úrovně zanoření

Céčkový for používat pro jakýkoliv cyklus se strukturou "inicializace, podmínka, krok"

V hlavičce for cyklu pracovat jen s řídící proměnnou

Vyhnout se přiřazení v hlavičce cyklu

Tělo cyklu

Každý cyklus by měl mít jen jednu funkci

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

Tělo cyklu krátké

"Housekeeping" na konci těla

Příkazy break a continue

Nebojte se break a continue

Okolí ckylu

Inicializační kód umístěte těsně před cyklus

Prázdné cykly

Příklad (C)

/* Eat characters until end of the line. */
while (fgetc(f) != '\n') ;

/* Eat characters until end of the line. */
int ch;
do {
  ch = fgetc(f);
} while (ch != '\n');

Nekonečné cykly

Design procedur, funkcí a metod

DRY. Parametry. Práce s výjimkami. Pseudokód.

Terminologie

Terminologie

V tomto semináři budu dále mluvit jen o funkcích, ale ve většině případů bude obsah platný i pro procedury (funkce bez návratové hodnoty) a metody (funkce a procedury svázané s objekty).

Proč používat funkce?

Proč používat funkce?

Strukturování programu

Příklad (Java)

"Každá instance objektu IdentifiedObject při vytvoření obdrží unikátní ID."

public IdentifiedObject() {
  static int nextId = 0;
  this.id = nextId++;
  /* ... */
}
private int getUniqueId() {
  static int nextId = 0;
  return nextId++;
}

public IdentifiedObject() {
  this.id = getUniqueId();
  /* ... */
}

Často abstrahované věci

Abstrakce pro práci s ukazateli je asi vhodnější implementovat spíš jako makra, než jako funkce. Princip ale zůstává.

Don't Repeat Yourself

Don't Repeat Yourself

Každá informace v systému by v něm měla mít jednu autoritativní a jednoznačnou reprezentaci.

Znovupoužití kódu není pro vás nic nového – opakovaně používané věci se zabstrahují do funkcí a tříd.

Generátory kódu jsou o mnoho zajímavější – umožňují z autoritativní reprezentace znalosti vygenerovat konkrétní kód. Příkladem můžou být generátory parserů, kde je kód vygenerován z gramatiky, a existuje i mnohem triviálnější příklad: céčkový preprocesor, expandující makra.

Dynamické jazyky se často vyznačují tím, že umí generovat kód za běhu. To umožňuje odbourat jinak nutný krok generování a zrychlit tak vývojový cyklus. Příklad se nachází o několik slajdů níže.

O principu DRY si můžete přečíst více v článku Orthogonality and the DRY Principle.

Don't Repeat Yourself

Každá informace v systému by v něm měla mít jednu autoritativní a jednoznačnou reprezentaci.

S databázemi je kromě uvedení dat do 3. normální formy často problém synchronizace představy o schématu mezi databází a kódem aplikace. Jakoukoliv změnu je obvykle nutné provést na obou místech. Tento problém zajímavě řeší framework Ruby on Rails, který díky možnostem jazyka Ruby dynamicky generuje objekty reprezentující záznamy v databázových tabulkách, které jsou tak vždy v souladu s databází, která je v tomto případě autoritativním zdrojem informací o schématu.

Dokumentace v mnohém duplikuje kód. Částečně to lze řešit tím, že je přímo z kódu generována, např. pomocí systému JavaDoc. Není to ale řešení dokonalé – duplicitu neodstraňuje, pouze umisťuje duplicitní věci co nejvíc k sobě. Navíc zavádí do systému určitou byrokracii. O tom všem bude řeč na některém z dalších seminářů.

Příklad: Vliv síly jazyka (1)

Java

int[] a = { 1, 2, 3 };

for (int i = 0; i < a.length; i++) {
  a[i] = a[i] * 2;
}

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

Příklad: Vliv síly jazyka (1)

Java

int[] a = { 1, 2, 3 };

for (int i = 0; i < a.length; i++) {
  a[i] = a[i] * 2;
}

for (int i = 0; i < a.length; i++) {
  System.out.println(a[i]);
}
Dva for cykly s úplně stejnou hlavičkou... zjevná duplicita kódu, která si říká o zabstrahování. Bohužel, Java tuto možnost nenabízí, resp. nabízí, ale stálo by nás to tolik úsilí a kódu navíc, že se to nedělá.

Příklad: Vliv síly jazyka (1)

Ruby

a = [1, 2, 3]

for i in 0...a.length do
  a[i] = a[i] * 2
end

for i in 0...a.length do
  puts a[i]
end

Příklad: Vliv síly jazyka (1)

Ruby

a = [1, 2, 3]

a = a.map { |item| item * 2 }
a.map { |item| puts item }

V jazycích s funkcionálními prvky, jako je třeba Ruby, existuje obvykle abstrakce průchodu kolekcí – funkce map. Tato funkce si bere za parametr jinou funkci, která dostane za parametr položku kolekce a vrátí, co má být do kolekce umístěno místo ní. Postupným spuštěním této funkce na všechny prvky kolekce je vytvořena nová kolekce a ta je pak funkcí map vrácena. V našem případě je funkci map předána funkce anonymní. (Ostřílení Rubysté prominou zamlčení rozdílu mezi funkcí a blokem a fakt, že správně by kód výše měl používat map! a each.)

Příklad je převzat a upraven z http://www.joelonsoftware.com/items/2006/08/01.html.

Pokud vás téma funkcionálního programování zaujalo, doporučuji článek Functional Programming For The Rest of Us.

Příklad: Vliv síly jazyka (2)

Java

class Expression { public abstract String getStringForm(); }

class BinaryOperator extends Expression {
  private Expression left;
  private Expression right;

  /* ... */
}

class PlusOperator extends BinaryOperator {
  public String getStringForm() {
    return left.getStringForm() + " + " + right.getStringForm();
  }
}

class MinusOperator extends BinaryOperator {
  public String getStringForm() {
    return left.getStringForm() + " - " + right.getStringForm();
  }
}

Příklad: Vliv síly jazyka (2)

Java

class Expression { public abstract String getStringForm(); }

class BinaryOperator extends Expression {
  private Expression left;
  private Expression right;

  /* ... */
}

class PlusOperator extends BinaryOperator {
  public String getStringForm() {
    return left.getStringForm() + " + " + right.getStringForm();
  }
}

class MinusOperator extends BinaryOperator {
  public String getStringForm() {
    return left.getStringForm() + " - " + right.getStringForm();
  }
}

Další příklad duplicity kódu, tentokrát jde o dvě třídy, které se krom svého názvu liší jedním jediným znakem.

Tento příklad je zjednodušenou verzí problému, který jsem řešil ve své diplomové práci.

Jako bonus můžete zkusit přijít na to, proč by u složitějších výrazů metoda getStringForm moc dobře nefungovala.

Příklad: Vliv síly jazyka (2)

Java

class Expression { public abstract String getStringForm(); }

class BinaryOperator extends Expression {
  private Expression left;
  private Expression right;
  private String operator;

  public String getStringForm() {
    return left.getStringForm() + " " + operator + " "
      + right.getStringForm();
  }

  /* ... */
}

class PlusOperator extends BinaryOperator {
  public PlusOperator()  { operator = "+"; }
}

class MinusOperator extends BinaryOperator {
  public MinusOperator() { operator = "-"; }
}
Duplicitu se nám díky přidání atributu operator do třídy BinaryOperator a přesunu metody getStringForm tamtéž podařilo omezit. Stále ale potřebujeme tři řádky kódu na každou třídu, která se liší jen jedním znakem.

Příklad: Vliv síly jazyka (2)

Ruby

class Expression
end

class BinaryOperator < Expression {
  attr_reader :left, :right, :operator

  def get_string_form()
    [ @left.get_string_form,
      @operator,
      @right.get_string_form ].join(" ")
  end

  # ...
end

class PlusOperator < BinaryOperator
  def initialize(); @operator = "+"; end
end

class MinusOperator < BinaryOperator
  def initialize(); @operator = "-"; end
end
Kód jsme jen mechanicky přepsali do Ruby.

Příklad: Vliv síly jazyka (2)

Ruby

class Expression
end

class BinaryOperator < Expression {
  attr_reader :left, :right, :operator

  def get_string_form()
    [ @left.get_string_form,
      @operator,
      @right.get_string_form ].join(" ")
  end

  # ...
end

[ [:PlusOperator,  "+"], [:MinusOperator, "-"] ].each do |def|
  module_eval %Q{
    class #{def[0].to_s} < BinaryOperator
      def initialize(); @operator = "#{def[1]}"; end
    end
  }
end

Nyní jsme kód upravili tak, že všechny věci, kterými se třídy PlusOperator a MinusOperator odlišovaly, (tedy název a textová podoba operátoru) jsme zaznamenali do pole, které procházíme. V každém kroku hodnoty z příslušné položky dosadíme do "šablony" kódu, který následně vyhodnotíme. Kód definující třídu je tak přítomen jen jednou.

Na počtu řádků jsme v tomto příkladu zjevně neušetřili, ale v předobraze příkladu v mé diplomové práci bylo tříd celkem 41. To už znát je, nemluvě o eliminaci strukturální duplicity kódu.

Abstrakce má svou cenu

Číselné systémy


Název Příklad Délka Nejsložitější operace

Abstrakce má svou cenu

Číselné systémy


Název Příklad Délka Nejsložitější operace
Čárky IIIII IIIII IIII O(n) Sčítání

Abstrakce má svou cenu

Číselné systémy


Název Příklad Délka Nejsložitější operace
Čárky IIIII IIIII IIII O(n) Sčítání
Římské číslice XIV O(n) Sčítání

Abstrakce má svou cenu

Číselné systémy


Název Příklad Délka Nejsložitější operace
Čárky IIIII IIIII IIII O(n) Sčítání
Římské číslice XIV O(n) Sčítání
Arabské číslice 14 O(logn) Násobení

Abstrakce má svou cenu

Číselné systémy


Název Příklad Délka Nejsložitější operace
Čárky IIIII IIIII IIII O(n) Sčítání
Římské číslice XIV O(n) Sčítání
Arabské číslice 14 O(logn) Násobení
Vědecká notace 0.14×102 O(log logn) Umocňování

Zápis je stále kratší a kratší, ale potřebujeme rozumět stále abstraktnějším operacím. Podobně to může být i u programů – za zkrácení a eliminování duplicity často platíme koncepčním zesložitěním. Třeba v příkladu s for cykly jsme museli porozumět konceptu předávání funkce jako parametru, který jsme předtím nepotřebovali, v dalším příkladu jsme podobně potřebovali pochopit dynamické generování kódu.

Při programování zejména v silnějších jazycích je často třeba zvážit, jak daleko v abstrakci jít a zda nakonec abstrakce nebude spíš na škodu. Obecně se vyplatí spíše přiklánět k DRY, ale výjimky se najdou.

Pro přesnost: O(log logn) platí samozřejmě jen v případě, že se vzdáme 100% přesnosti v mantise čísla.

Abstrakce má svou cenu

Předchozí slajdy s ukázkou číselných systémů se odkrývaly inkrementálně. Technicky jsem to vyřešil sadou několika slajdů, které vznikly starým dobrým copy & paste. Automatizované řešení by samozřejmě bylo možné, ale rozhodl jsem se, že v tomto případě se mi nevyplatí vrtat se několik hodin v JavaScriptu a k tomu významně zesložitit kód skriptů.

Jak má vypadat správná funkce?

Funkce má být co možná nejsamostatnější kus kódu, který dělá jednu jasně definovanou věc, odvoditelnou z jejího názvu.

Příklad: Explicitní závislost

Bez zjevné závislosti (Java)

computeMarketingExpense(marketingData)
computeSalesExpense(salesData)
computeTravelExpense(travelData)
computePersonnelExpense(personnelData)
displayExpenseSummary(marketingData, salesData, travelData,
  personnelData)

Se zjevnou závislostí (Java)

expenseData = initializeExpenseData(expenseData)
expenseData = computeMarketingExpense(expenseData)
expenseData = computeSalesExpense(expenseData)
expenseData = computeTravelExpense(expenseData)
expenseData = computePersonnelExpense(expenseData)
displayExpenseSummary(expenseData)

Příklad: Okomentování závislosti

Java

/* Following calls must be in correct order. Inside the call of
 * the taskReachedState method, the Task Manager may decide to
 * close context that contains this task and call
 * HostRuntimeInterface.closeContext method. But if the calls
 * are not ordered properly, the Host Runtime will not yet be
 * notified about the task's end (the notification happens in
 * notifyTaskFinished call) and refuse to close the context
 * (throwing IllegalArgumentException). This would be a race
 * condition.
 */
hostRuntime.notifyTaskFinished(TaskImplementation.this);
hostRuntime.getHostRuntimesPort().taskReachedState(
  taskDescriptor.getTaskTid(),
  taskDescriptor.getContextId(),
  processKilledFromOutside
    ? TaskState.ABORTED
    : TaskState.FINISHED
);
Příklad je převzat z našeho softwarového projektu. Není na něm podstatné, co přesně dělají metody notifyTaskFinished a taskReachedState, ale to, jak podrobně je chování v komentáři popsáno.

Délka funkce

Názvy funkcí

Název funkce by měl přesně a úplně popisovat, co funkce dělá

Názvy funkcí

Častá opozita

  • add/remove
  • begin/end
  • create/destroy
  • first/last
  • get/put
  • get/set
  • increment/decrement
  • insert/delete
  • lock/unlock
  • min/max
  • next/previous (prev – symetrické)
  • old/new
  • open/close
  • show/hide
  • source/target
  • start/stop
  • up/down

Parametry

Druhy parametrů

Problém

Modifikovatelné a výstupní parametry lze těžko odlišit od vstupních v místě volání (a někdy i v deklaraci funkce).

Řešení

Pokud možno nepoužívejte modifikovatelné a výstupní parametry

Zásady pro práci s parametry

Neměňte vstupní parametry v těle metody

Podobné parametry uveďte ve stejném pořadí u všech funkcí

fprintf(stream, format,...)
fputs(str, stream)
strncpy(dst, src, len)
memcpy(dst, src, len)

Vyhněte se nepoužívaným parametrům

Zásady pro práci s parametry

Ne víc jak 7 parametrů

Vyhněte se booleovským parametrům

Preferujte interfacy a abstraktní typy

Klíčová slova const resp. final často znepřehledňují kód, a tak je málokdo používá. Při návrhu nových jazyků by možná stálo za úvahu dát parametrům sémantiku konstant automaticky, bez potřeby klíčového slova.

Ke krácení seznamu parametrů: Při zkracování seznamu parametrů funkce náhradou několika atributů jednoho objektu celým objektem je dobré se zamyslet, zda je fakt, že funkci chcete posílat celý objekt náhoda nebo systematická záležitost. Náhoda se pozná tak, že při volání funkce nemáte vždy dotyčný objekt "v ruce" – v tom případě je lepší nechat seznam parametrů být, protože budete muset u některých volání funkce objekt uměle vytvářet. Naopak, pokud zjistíte, že to náhoda není, stojí za to se chvilku zastavit nad strukturou kódu – můžete např. přijít na to, že daná funkce by měla být metoda objektu, který jí posíláte.

K booleovským parametrům: Náhrada výčtovým typem/konstantou má i výhodu flexibility v případě, že časem přibudou další možné hodnoty parametru. To se u boolovských parametrů docela často stává, více viz výstižně nazvaný článek Booleans suck.

K interfacům a abstraktním typům: Viz Effective Java: Programming Language Guide, Item 34.

Vracení hodnot

Opatrně s více returny

Používejte jednotný název pro proměnnou s výsledkem

Vracejte prázdné pole, ne null

K prázdnými polím vs. null: Viz Effective Java: Programming Language Guide, Item 34.

Kontrakt metody

Specifikujte kontrakt metody

Viz Effective Java: Programming Language Guide, Item 23.

Kontrakt metody

public void log(String context, String taskID, Date timestamp,
    LogLevel level, String message)
    throws IllegalArgumentException, NullPointerException {

  if (context == null) {
    throw new NullPointerException("Context name is null");
  }
  if (context.equals("")) {
    throw new IllegalArgumentException("Context name is empty");
  }
  if (taskID == null) {
    throw new NullPointerException("Task ID is null");
  }
  if (taskID.equals("")) {
    throw new IllegalArgumentException("Task ID is empty");
  }
  if (timestamp == null) {
    throw new NullPointerException("Timestamp is null");
  }
  if (level == null) {
    throw new NullPointerException("Log level is null");
  }
  if (message == null) {
    throw new NullPointerException("Log message is null");
  }

  File contextDir = new File(basedir, context);
  if (!contextDir.isDirectory()) {
    throw new IllegalArgumentException(
      "Context not registered: " + context
    );
  }

  File taskDir = new File(contextDir, taskID);
  if (!taskDir.isDirectory()) {
    throw new IllegalArgumentException(
      "Task not registered: " + taskID
    );
  }

  /* ... */
}

Příklad je mírně upravený kus kódu z našeho softwarového projektu. Autor kódu (nikoliv já) si krátce před jeho napsáním přečetl Effective Java a byl odhodlaný psát kód opravdu poctivě a neprůstřelně.

Kód na kontrolu parametrů byl nakonec delší než samotné tělo metody (to je v příkladu vynecháno). Je na místě otázka, zda, případně kdy, se opravdu vyplatí parametry poctivě kontrolovat.

Princip bariéry

Určete si hranici, na které se budete bránit vůči závadným datům, a tam "vztyčte bariéru"

Práce s výjimkami

Výjimky × chybové kódy

Výjimky

Chybové kódy

Problematika výjimky × chybové kódy je složitější – my jsme se jí tu jen zlehka dotkli. Zájemcům o hlubší vhled doporučuji k přečtení tyto články (v uvedeném pořadí):

Kontrolované × nekontrolované výjimky

Kontrolované × nekontrolované výjimky

Kontrolovanou výjimku vyhazujte jen tehdy, může-li nastat i při správném použití funkce a dá se čekávat, že volající ji může ošetřit

K náhradě kontrolovaných výjimek za nekontrolované: Dobrý příklad je metoda parsující text v nějakém jazyce. Text může v principu obsahovat chyby, a je žádoucí, aby metoda v tom případě vyhazovala výjimku. Pokud by tato výjimka byla kontrolovaná, musel by programátor výjimku odchytávat i v případě, že by si byl jist, že metodě předává syntakticky korektní text (např. automaticky generovaný). Lepší řešení je použít nekontrolovanou výjimku a přidat metodu, která bezchybnost textu otestuje a vrátí true nebo false.

Uvedený postup ale nelze použít vždy. Např. pokud bychom chtěli před smazáním souboru zkontrolovat, zda tento soubor existuje, může nám ho mezi testem a smazáním někdo "smazat pod rukama". Je tedy potřeba mít zaručen exkluzivní přístup k datům.

Ke kontrolovaným a nekontrolovaným výjimkám obecně: Viz Effective Java: Programming Language Guide, Item 40 + 41.

Zásady pro práci s výjimkami

Výjimky používejte jen ve výjimečných situacích

Používejte standardní výjimky jazyka

K výjimečným situacím: Viz Effective Java: Programming Language Guide, Item 39.

K standardním výjimkám: Viz Effective Java: Programming Language Guide, Item 42.

Zásady pro práci s výjimkami

Používejte výjimky na odpovídající úrovni abstrakce

Ve výjimce detailně popište problém

Vyhněte se prázdným catch

K úrovni abstrakce: Viz Effective Java: Programming Language Guide, Item 43.

K detailnímu popisu problému: Viz Effective Java: Programming Language Guide, Item 45.

K prázdným catchům: Viz Effective Java: Programming Language Guide, Item 47.

Pseudokód

Pseudokód

Příklad na pseudokód

Zadání

Úloha je lehkým zjednodušením úlohy řešené v rámci kódu mé diplomové práce. Konkrétně jsem potřeboval implementovat bitové operace na "velkých číslech".

Pokud se vám zdá ukládání bitů po jednom jako neefektivní, máte pravdu. Ale je to nejjednodušší řešení a optimalizovat se ho vyplatí až v případě, že nás opravdu někde začne "tlačit bota". Což v případě diplomové práce taky nemusí nastat vůbec :-)

Pokud přemýšlíte, proč není číslo uloženo jako dvojkový doplněk rovnou, odpověď je inženýrská: PHP obsahuje knihovnu bcmath, která používá řetězcový zápis čísel a implementuje základní aritmetické operace (+, -, *, /, %,...). Je méně pracné implementovat převod mezi formáty a (triviálně) bitové operace, než (relativně obtížně) implementovat všechny operace.

Pokud si z prvního ročníku nepamatujete, co je to dvojkový doplněk, pomůže wikipedie.

Algoritmus řešení

  1. Remember if the value is negative.
  2. If the value is negative,
        trim the sign (i.e. compute the absolute value).
  3. Compute the bits of the absolute value (using standard algorithm).
  4. If the first bit is non-zero,
        prefix the bits with zero bit.
  5. If the number was negative,
        invert the bits and add one.
  6. Return the bits.
V tomto příkladu bude vše v angličtině, protože to tak mám i v diplomové práci.

Krok 1

Hlavička funkce, algoritmus zapíšeme jako komentáře

function bignum_to_bits($bignum) {
  /* Remember if the value is negative. */

  /* If the value is negative, trim the sign (i.e. compute the
     absolute value). */

  /* Compute the bits of the absolute value (using standard
     algorithm). */

  /* If the first bit is non-zero, prefix the number with
     zero bit. */

  /* If the number was negative, invert the bits and add one. */

  /* Return the bits. */
}

Krok 2

Vyplňování kódu

function bignum_to_bits($bignum) {
  /* Remember if the value is negative. */
  $negative = ($bignum[0] == "-");

  /* If the value is negative, trim the sign (i.e. compute the
     absolute value). */

  /* Compute the bits of the absolute value (using standard
     algorithm). */

  /* If the first bit is non-zero, prefix the number with
     zero bit. */

  /* If the number was negative, invert the bits and add one. */

  /* Return the bits. */
}

Krok 3

Vyplňování kódu

function bignum_to_bits($bignum) {
  /* Remember if the value is negative. */
  $negative = ($bignum[0] == "-");

  /* If the value is negative, trim the sign (i.e. compute the
     absolute value). */
  if ($negative) {
    $bignum = substr($bignum, 1);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */

  /* If the first bit is non-zero, prefix the number with
     zero bit. */

  /* If the number was negative, invert the bits and add one. */

  /* Return the bits. */
}

Krok 4

Sloučení prvních dvou kroků

function bignum_to_bits($bignum) {
  /* Remember if the value is negative, trim the sign if so. */
  if ($negative = ($bignum[0] == "-")) {
    $bignum = substr($bignum, 1);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */

  /* If the first bit is non-zero, prefix the number with
     zero bit. */

  /* If the number was negative, invert the bits and add one. */

  /* Return the bits. */
}

Krok 5

Vyplňování kódu

function bignum_to_bits($bignum) {
  /* Remember if the value is negative, trim the sign if so. */
  if ($negative = ($bignum[0] == "-")) {
    $bignum = substr($bignum, 1);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */
  $bits = array();
  do {
    array_unshift($bits, (int)bcmod($bignum, 2));
    $bignum = bcdiv($bignum, 2);
  } while ($bignum != "0");

  /* If the first bit is non-zero, prefix the number with
     zero bit. */

  /* If the number was negative, invert the bits and add one. */

  /* Return the bits. */
}

Krok 6

Vyplňování kódu

function bignum_to_bits($bignum) {
  /* Remember if the value is negative, trim the sign if so. */
  if ($negative = ($bignum[0] == "-")) {
    $bignum = substr($bignum, 1);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */
  $bits = array();
  do {
    array_unshift($bits, (int)bcmod($bignum, 2));
    $bignum = bcdiv($bignum, 2);
  } while ($bignum != "0");

  /* If the first bit is non-zero, prefix the number with
     zero bit. */
  if ($bits[0] != 0) {
    array_unshift($bits, 0);
  }

  /* If the number was negative, invert the bits and add one. */

  /* Return the bits. */
}

Krok 7

Vyplňování kódu

function bignum_to_bits($bignum) {
  /* Remember if the value is negative, trim the sign if so. */
  if ($negative = ($bignum[0] == "-")) {
    $bignum = substr($bignum, 1);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */
  $bits = array();
  do {
    array_unshift($bits, (int)bcmod($bignum, 2));
    $bignum = bcdiv($bignum, 2);
  } while ($bignum != "0");

  /* If the first bit is non-zero, prefix the number with
     zero bit. */
  if ($bits[0] != 0) {
    array_unshift($bits, 0);
  }

  /* If the number was negative, invert the bits and add one. */
  if ($negative) {
    for ($i = 0; $i < count($bits); $i++) {
      $bits[$i] = 1 - $bits[$i];
    }

    for ($i = count($bits) - 1; $i >= 0; $i--) {
      if ($bits[$i] == 0) {
        $bits[$i] = 1;
        break;
      } else {
        $bits[$i] = 0;
      }
    }
  }

  /* Return the bits. */
}

Krok 8

Vyplňování kódu

function bignum_to_bits($bignum) {
  /* Remember if the value is negative, trim the sign if so. */
  if ($negative = ($bignum[0] == "-")) {
    $bignum = substr($bignum, 1);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */
  $bits = array();
  do {
    array_unshift($bits, (int)bcmod($bignum, 2));
    $bignum = bcdiv($bignum, 2);
  } while ($bignum != "0");

  /* If the first bit is non-zero, prefix the number with
     zero bit. */
  if ($bits[0] != 0) {
    array_unshift($bits, 0);
  }

  /* If the number was negative, invert the bits and add one. */
  if ($negative) {
    for ($i = 0; $i < count($bits); $i++) {
      $bits[$i] = 1 - $bits[$i];
    }

    for ($i = count($bits) - 1; $i >= 0; $i--) {
      if ($bits[$i] == 0) {
        $bits[$i] = 1;
        break;
      } else {
        $bits[$i] = 0;
      }
    }
  }

  /* Return the bits. */
  return $bits;
}
V tuto chvíli vypadá funkce jako hotová. Ale zdaleka není...

Krok 9

Refaktorizace

function bignum_to_bits($bignum) {
  /* Remember if the value is negative, trim the sign if so. */
  if ($negative = ($bignum[0] == "-")) {
    $bignum = substr($bignum, 1);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */
  $bits = array();
  do {
    array_unshift($bits, (int)bcmod($bignum, 2));
    $bignum = bcdiv($bignum, 2);
  } while ($bignum != "0");

  /* If the first bit is non-zero, prefix the number with
     zero bit. */
  if ($bits[0] != 0) {
    array_unshift($bits, 0);
  }

  /* If the number was negative, invert the bits and add one. */
  if ($negative) {
    $bits = bits_add_one(bits_invert($bits));
  }

  /* Return the bits. */
  return $bits;
}

Krok 10

Refaktorizace

function bignum_to_bits($bignum) {
  /* Remember if the value is negative, trim the sign if so. */
  if ($negative = ($bignum[0] == "-")) {
    $bignum = substr($bignum, 1);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */
  $bits = positive_bignum_to_bits($bignum);

  /* If the first bit is non-zero, prefix the number with
     zero bit. */
  if ($bits[0] != 0) {
    array_unshift($bits, 0);
  }

  /* If the number was negative, invert the bits and add one. */
  if ($negative) {
    $bits = bits_add_one(bits_invert($bits));
  }

  /* Return the bits. */
  return $bits;
}

Krok 11

Refaktorizace

function bignum_to_bits($bignum) {
  /* Remember if the value is negative, trim the sign if so. */
  if ($negative = ($bignum[0] == "-")) {
    $bignum = substr($bignum, 1);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */
  $bits = positive_bignum_to_bits($bignum);

  /* If the first bit is non-zero, prefix the number with
     zero bit. */
  $bits = bits_prefix_with_zero_if_needed($bits);

  /* If the number was negative, invert the bits and add one. */
  if ($negative) {
    $bits = bits_add_one(bits_invert($bits));
  }

  /* Return the bits. */
  return $bits;
}

Krok 12

Refaktorizace

function bignum_to_bits($bignum) {
  /* Remember if the value is negative, trim the sign if so. */
  if ($negative = bignum_is_negative($bignum)) {
    $bignum = bignum_abs($bignum);
  }

  /* Compute the bits of the absolute value (using standard
     algorithm). */
  $bits = positive_bignum_to_bits($bignum);

  /* If the first bit is non-zero, prefix the number with
     zero bit. */
  $bits = bits_prefix_with_zero_if_needed($bits);

  /* If the number was negative, invert the bits and add one. */
  if ($negative) {
    $bits = bits_add_one(bits_invert($bits));
  }

  /* Return the bits. */
  return $bits;
}

Krok 13

Odstranění víceméně zbytečných komentářů

function bignum_to_bits($bignum) {
  if ($negative = bignum_is_negative($bignum)) {
    $bignum = bignum_abs($bignum);
  }

  $bits = positive_bignum_to_bits($bignum);
  $bits = bits_prefix_with_zero_if_needed($bits);

  if ($negative) {
    $bits = bits_add_one(bits_invert($bits));
  }

  return $bits;
}

Design tříd

Zapouzdření. Immutability. Dědičnost × kompozice. Polymorfismus.

Dva pohledy na objekty

"Céčko"

SmallTalk

"Céčko" je na slajdu v uvozovkách, protože céčková rodina není první jazyk, který s tímto pohledem přišel, ale asi vám bude nejznámější.

Céčkový pohled je technický, "od zdola". Dívá se na objekty především jako na způsob strukturování kódu. Z tohoto pohledu je objekt jen vylepšený struct, který má kromě datových položek i funkce, které s ním pracují. Do syntaxe jazyka je pak přidána možnost tyto funkce deklarovat přímo v objektu, možnost je na objektu volat pomocí operátorů . a -> a také implicitní předávání parametru this.

Pohled SmallTalku je úplně odlišný. SmallTalk považuje objekty za krabičky, které umí s okolím komunikovat pomocí zpráv. O vnitřním uspořádání krabiček není okolí nic známo. Každá zpráva má svůj název, parametry (objekty) a případně návratovou hodnotu (objekt). Může se stát, že objekt zprávu nebude znát – to se dá obecně zjišťovat jak v compile-time, tak v run-time. V případě zjišťování v run-time může objekt volání neznámé zprávy ošetřovat, což dává programátorovi poměrně velkou flexibilitu. Skvěle se tak implementují různé proxy, delegování, apod.

K SmallTalkovskému pojetí objektů má z dnešního pohledu velmi blízko jazyk Ruby, některé vlastnosti lze najít i v Javě a C#. Ostatní běžné jazyky se drží spíše céčkového modelu.

Proč objekty?

  1. Modelování reálného světa
    • Realita: věci + akce
    • Program: objekty + metody
    • Jazyk řešení přibližujeme jazyku problému
  2. Organizace dat a kódu
    • Obojí na jednom místě
    • Instance principu lokality
Body jsou očíslovány záměrně – organizace kódu je až sekundární.

Objekty jako abstraktní datové typy

Slovo "reálné" je v uvozovkách, protože problém, který řešíme, se reality mimo počítač vůbec nemusí týkat. Třeba třída HttpRequest nepředstavuje nic opravdu hmatatelného, je to (obvykle) jen sekvence bajtů zaslaná po síti.

Zodpovědnost objektu

Každý objekt by měl být zodpovědný jen za jednu věc

Eroze v tomto případě znamená tendenci přidávat do objektů různé pomocné metody, které se hodí uživatelům objektu v různých jiných částech kódu. Zejména u větších projektů s více programátory se často stává, že přidané metody nesouvisí s abstrakcí představovanou objektem a jeho zodpovědností. Vzniká tak zamotaný, nestrukturovaný kód.

Asi jediný způsob, jak se erozi vyhnout, je disciplína programátorů a případně jednoznačné vlastnictví kódu (kdy vlastník nepovolí nesystematický zásah do svého kódu).

Kontrakt objektu

Specifikujte kontrakt objektu

Klasické pojmy OOP

  1. Zapouzdření
  2. Dědičnost
  3. Polymorfismus

Zapouzdření

Zapouzdření

Jak dosáhnout zapouzdření?

Minimalizujte viditelnost atributů, metod a dalších prvků objektů

K atributům objektů přistupujte jen přes pomocné metody (getters/setters, accessors)

Skryjte implementační detaily

C++: Pokud možno se vyhněte spřáteleným třídám

Vyhněte se tranzitivním public metodám

K porušování skrývání implementačních detailů: Existuje mnoho toolkitů obalujících část Win32 API pro tvorbu GUI. Typicky jde o hierarchii tříd, na jejímž konci se nacházejí třídy pro jednotlivé ovládací prvky. Málokdy je možné a vhodné implementovat do obalující třídy všechny vlastnosti, které dotyčný ovládací prvek ve Win32 API má – třída by až příliš narostla. Pragmatické řešení je porušit zapouzdření a zveřejnit handle ovládacího prvku z Win32 API. Kdo bude potřebovat, může vlastnosti ovládacího prvku, které zapouzdřující třída neobsahuje, používat přímo pomocí handle a Win32 API funkcí. Takto je problém například vyřešen v Borland Delphi.

Jedná se o případ "prosakující" abstrakce – více viz článek The Law of Leaky Abstractions.

K tranzitivním public metodám: Jde o to, že pokud nějaká metoda třídy používá jen samé public metody, není to samo o sobě důvod, aby byla také public. Analogicky s protected.

Sémantické porušení zapouzdření

Vyhněte se sémantickému porušení zapouzdření

K porušování sémantického zapouzdření: Pokud jste nuceni se podívat do zdrojového kódu používané třídy, znamená to, že třída má buď špatnou úroveň abstrakce nebo nedostatečnou dokumentaci. Správnou akcí je v tuto chvíli donutit autora třídy k nápravě, je-li to možné.

Gettery/settery

Typické užití v Javě

class MyClass {
  private int attribute;
        
  public int getAttribute() {
    return attribute;
  }

  public void setAttribute(int attribute) {
    this.attribute = attribute;
  }
}

Problémy getterů/setterů

  1. Strukturální duplicita kódu
  2. Nepěkná syntaxe
    • myObject.attribute
          názornější než
      myObject.getAttribute()
    • myObject.attribute = value
          názornější než
      myObject.setAttribute(value)

Důsledky

U malých projektů může být opravdu více práce psát gettery/settery, než napravovat porušení zapouzdření. Jakmile je ale projekt větší a pracuje na něm více programátorů, výhody jejich psaní převáží. Psaní getterů/setterů je vlastně speciální případ optimalizace na čtenáře.

Možné řešení: Generování

Možné řešení: Vlastnosti

Proč "skoro dobře" a ne úplně dobře? Protože stále stojí víc práce použít vlastnost, než obyčejný atribut. Ale té práce je oproti jiným jazykům relativně málo.

Možné řešení: Vlastnosti

Příklad (C#)

class MyClass {
  private int attribute;
  
  public int Attribute  {
    get { return attribute; }
    set { attribute = value; }
  }
}

Příklad (Object Pascal)

type
  TMyClass = class
    private
      FAttribute: Integer;
    published
      property Attribute: Integer
        read FAttribute
        write FAttribute;
  end;

Možné řešení: Dynamické generování

Ruby

V ukázce: Vytvořit třídu s konstruktorem, ve kterém se nastaví dva atributy. Ukázat, že zvenku nejsou vidět. Pak na jeden použít attr_reader a ukázat, že už jde načíst. Totéž s attr_writer a následně použít attr_accessor na druhý atribut.

Proč správné řešení? Protože programátor nemůže atributy vůbec zveřejnit a v typickém případě není nutné psát vůbec žádný kód getterů/setterů.

Immutability

Viz Effective Java: Programming Language Guide, Item 13.

Definice

Třída je immutable právě tehdy, pokud to vytvoření instance nejdou data instance žádným způsobem změnit.

Výraz "immutable" zde nepřekládám, protože neznám žádný překlad, který by nezněl divně. Pokud vás nějaký napadne, rád o něm uslyším.

Jak vyrobit immutable třídu?

Pro C# stačí nahradit final za sealed. V C++ jde podobného efektu dosáhnout pomocí const.

Výhody immutability

K snadnému sdílení vnitřností: Třída BigInteger ze standardní knihovny Javy implementuje "velká čísla". Technicky jsou implementována pomocí pole. Metoda negate vrátí nové velké číslo, které bude negací toho, na kterém byla zavolána. Protože instance třídy BigInteger jsou immutable, původní i znegované číslo můžou sdílet pole s číslicemi bez obav, že bude přepsáno. Každá instance musí mít jen svoji informaci o znaménku. Ušetří se tak paměť.

Ke klíčům hashtabulek: Jedním z požadavků na klíč hashtabulky v Javě je, aby se po dobu, co je klíčem, nezměnila hodnota, kterou vrací jeho metoda hashCode. Tato hodnota je v drtivé většině případů počítána z atributů objektu. V případě immutable objektů se atributy nikdy nezmění a tím pádem se nezmění ani hodnota vracená metodou hashCode objektu. Podmínka kladená na klíč hashtabulky je tedy automaticky splněna.

Výhody immutability

Příklad: Dobré "stavební bloky"

Java

/* Immutable DateInterval postavený z mutable tříd Date.
 * Je nutné používat defenzivní kopie. 
 */
class DateInterval {
  private Date begin;
  private Date end;
  
  public Date getBegin() { return begin.clone(); }
  public Date getEnd()   { return end.clone();   }
  
  public DateInterval(Date begin, Date end) {
    this.begin = begin.clone();
    this.begin = end.clone();
  }
}

Příklad: Dobré "stavební bloky"

Java

/* Immutable DateInterval postavený z immutable tříd Date.
 * Není nutné používat defenzivní kopie.
 */
class DateInterval {
  private Date begin;
  private Date end;
  
  public Date getBegin() { return begin; }
  public Date getEnd()   { return end;   }
  
  public DateInterval(Date begin, Date end) {
    this.begin = begin;
    this.begin = end;
  }
}

Nevýhody immutability

Kdy má být třída immutable?

Třída by měla být immutable, pokud nemáte opravdu dobrý důvod, aby byla mutable.

/**
 * Class which stores information about timing of the experiments
 * in the regression analysis.
 * 
 * The class is immutable and especially Misho should never ever
 * try to make it mutable :-)
 * 
 * @author David Majda
 */
public class SchedulerInfo implements Serializable {
  /* ... */
}
Příklad je převzat z našeho softwarového projektu.

Častí kandidáti na immutabilitu

Dědičnost × kompozice

Kompozice

Příklad (Java)

"Car has an engine and four wheels."

class Car {
  private Engine engine;
  private Wheel[] wheels;
}

Dědičnost

Generalizace × specializace

Dědění rozhraní × dědění implementace

Liskovův substituční princip

Na každém místě, kde lze použít nějaká třída, musí jít použít i její podtřída, aniž by uživatel poznal rozdíl

Výhody dědičnosti

Zásady pro práci s dědičností

Třídu navrhněte s ohledem na dědičnost, nebo dědění z ní zakažte

Viz Effective Java: Programming Language Guide, Item 15.

Problémy při návrhu pro dědičnost

K virtuálním metodám: Každá virtuální metoda je extension point, do kterého může svůj kód umístit odvozená třída. Ta může "vyvádět psí kusy", porušit některé invarianty, způsobit reetrantnost, na kterou nejste připraveni apod. V praxi naštěstí zdá se k těmto problémům příliš nedochází – ještě jsem například neviděl někoho si z tohoto důvodu stěžovat, že v Javě jsou všechny metody virtuální.

Naopak jsem ale viděl stěžování z důvodu výkonu. Zavolání virtuální metody trvá o něco déle než zavolání obyčejné, takže ve výkonnostně kritických aplikacích (např. hry) nebo úsecích kódu se někdy vyplatí nad virtuálními metodami uvažovat i z tohoto hlediska.

Ke snižování flexibility: Tím, že zabráníme uživateli dědit od nějaké třídy, zabraňujeme mu vytvořit si její upravené verze, ve kterých si např. dopíše různé pomocné metody. To je velký problém u tříd v knihovnách Javy, které jsou často final a přitom jsou navrženy poměrně minimalisticky. Důsledek je, že skoro v každém větším projektu v Javě se najde spousta pomocných tříd (např. StringUtils), které potřebné metody implementují jako statické.

Jednou ze zajímavých vlastností C# 3.0 je, že obsahauje mechanismus, který zajišťuje, že se tyto pomocné statické metody dají volat, jako by to byly metody původní třídy. Popravdě, nevím zda je tato možnost ošklivý hack nebo elegantní řešení obtížného problému :-)

Šabl. metody/abstraktní bázové třídy

Příklad (Java)

public abstract class HttpServlet {
  protected void doGet(
    HttpServletRequest req,
    HttpServletResponse resp
  );
  
  protected void doPost(
    HttpServletRequest req,
    HttpServletResponse resp
  );
  
  /* ... */
}

Zásady pro práci s dědičností

Vyhněte se příliš složitým hierarchiím

Vyhněte se mnohonásobné dědičnosti implementace

The one indisputable fact about multiple inheritance in C++ is that it opens up a Pandora's box of complexities that simply do not exist under single inheritance. – Scott Meyers

K mnohonásobné dědičnosti: Dědičnost rozhraní je v C++ realizována ryze abstraktními třídami, v Javě a C# pak pomocí interfaců.

Společné věci přesuňte v hierarchii co nejvýš

Zásady pro práci s dědičností

Pozor na třídy s jen jednou podtřídou

Pozor na prázdnou předefinovanou metodu

K třídám s jen jednou podtřídou: Podobně platí i pro rozhraní s jen jednou implementací.

K prázdné předefinované metodě: Příkladem budiž abstraktní třída Stream, představující nějaký výstupní datový proud. Mezi jejími metodami je i metoda flush, která zapíše na disk data držená v bufferu. V memory-based streamu MemoryStream taková metoda pochopitelně bude prázdná. To je špatně. Správné by bylo zjemnit hierarchii tříd, rozdělit je na bufferované/nebufferované nebo disk-based/memory-based a metodu flush přidat jen do té větve hierarchie, kde ji třídy budou opravdu implementovat.

Kompozice × dědičnost

Příklad na zamyšlení

Chceme definovat třídy Real a Complex, představující reálná a komplexní čísla – jaký má být mezi nimi vztah?

Kompozice × dědičnost

Příklad na zamyšlení

Chceme definovat třídy Real a Complex, představující reálná a komplexní čísla – jaký má být mezi nimi vztah?

  1. Complex dědí od Real
    • Myšlenka: Komplexní čísla rozšiřují reálná čísla
    • Ale: Je-li vyžadováno reálné číslo, můžeme dosadit i komplexní
    • Ale: Je-li vyžadováno komplexní číslo, nemůžeme dosadit reálné

Kompozice × dědičnost

Příklad na zamyšlení

Chceme definovat třídy Real a Complex, představující reálná a komplexní čísla – jaký má být mezi nimi vztah?

  1. Complex dědí od Real
    • Myšlenka: Komplexní čísla rozšiřují reálná čísla
    • Ale: Je-li vyžadováno reálné číslo, můžeme dosadit i komplexní
    • Ale: Je-li vyžadováno komplexní číslo, nemůžeme dosadit reálné
  2. Real dědí od Complex
    • Myšlenka: reálné číslo "is a" komplexní číslo
    • Real bude jednoduše mít nulovou komplexní složku
    • Ale: Nulová komplexní složka zabírá paměť

Kompozice × dědičnost

Příklad na zamyšlení

Chceme definovat třídy Real a Complex, představující reálná a komplexní čísla – jaký má být mezi nimi vztah?

  1. Complex dědí od Real
    • Myšlenka: Komplexní čísla rozšiřují reálná čísla
    • Ale: Je-li vyžadováno reálné číslo, můžeme dosadit i komplexní
    • Ale: Je-li vyžadováno komplexní číslo, nemůžeme dosadit reálné
  2. Real dědí od Complex
    • Myšlenka: reálné číslo "is a" komplexní číslo
    • Real bude jednoduše mít nulovou komplexní složku
    • Ale: Nulová reálná složka zabírá paměť

Co je správně?

Kompozice × dědičnost

Příklad na zamyšlení

Správně není ani jedno!

Kompozice × dědičnost

Viz Effective Java: Programming Language Guide, Item 14.

Polymorfismus

Opakování: Co je to polymorfismus?

Polymorfismus a příkaz switch

Zadání

Implementujte AST pro aritmetické výrazy s binárními operátory "+" a "-" a jeho vyhodnocení.

Polymorfismus a příkaz switch

Řešení céčkařovo (Java)

enum ExpressionType { PLUS, MINUS, VALUE }

class Expression {
  private ExpressionType type;
  private Expression left, right;
  private int value;

  /* ... */
}

class Evaluator {
  public int eval(Expression expr) {
    switch (expr.getType()) {
      case ExpressionType.PLUS:
        return eval(expr.getLeft()) + eval(expr.getRight());
      case ExpressionType.MINUS:
        return eval(expr.getLeft()) - eval(expr.getRight());
      case ExpressionType.VALUE:
        return expr.getValue();
    }
  }
}

Polymorfismus a příkaz switch

Řešení polymorfické (Java)

abstract class Expression {
  public abstract int eval();
};

abstract class BinaryOperator extends Expression {
  protected Expression left, right;

  /* ... */
}

class PlusOperator  extends BinaryOperator {
  public int eval() { return left.eval() + right.eval(); }
}

class MinusOperator extends BinaryOperator {
  public int eval() { return left.eval() - right.eval(); }
}

Polymorfismus a příkaz switch

Řešení polymorfické (Java)

class Value extends Expression {
  private int value;
  
  /* ... */
  
  public int eval() { return value; }
}

Polymorfismus a příkaz switch

Každý switch je promarněná příležitost k polymorfismu

Design API

Skvělou přednášku o návrhu API přednesl Joshua Bloch (dříve Sun, dnes Google) na JavaPolis 2005: How to Design a Good API & Why it Matters (slajdy).

Co je to API?

API = Application Programming Interface

Proč je API důležité?

Proč je API důležité?

K tématu rozšiřitelnosti má blízko článek Steva Yeggeho The Pinocchio Problem.

Obtížnost návrhu API

Moje malá úvaha o tom, co je v programátorské branži těžké: Design is hard. Část o Ruby prosím ignorujte, tehdy jsem ho ještě skoro neznal a nevěděl jsem, o čem mluvím.

Základní pravidla designu

Myslete na uživatele.

Pohled implementátora je druhořadý

Základní pravidla designu

Dělejte jen jednu věc, a dělejte ji dobře.

"Keep It Simple, Stupid." – anonym

"When in doubt, leave it out." – Joshua Bloch

"Everything should be made as simple as possible, but no simpler." – Albert Einstein

"Konstrukční dokonalosti není dosaženo tehdy, když už není co přidat, ale tehdy, když už nemůžete nic odebrat." – Antoine de Saint-Exupéry

Zásady designu API

Zásady adaptovány z článku API Design.

Myslete na své publikum

V blogovacím systému může být vhodné udělat zvlášť API pro autory obsahu a zvlášť pro čtenáře. Tyto dvě skupiny mají totiž úplně odlišné požadavky: Autor chce obsah číst i vytvářet, a pravděpodobně ho zajímá jen jeho blog, zatímco čtenář může obsah jen číst, ale zase ho pravděpodobně bude zajímat obsah více blogů, bude požadovat agregaci, apod.

Viz také API Design: The Principle of Audience

Nechystejte žádná překvapení

"A user interface is well-designed when the program behaves exactly how the user thought it would." – Joel Spolsky

Nechystejte žádná překvapení

Využívejte idiomů cílové platformy

Nechystejte žádná překvapení

Využívejte idiomů cílové platformy

Buďte konzistentní

Nechystejte žádná překvapení

Volte dobré názvy prvků API

Citát je převzat z Joelovy série článků o návrhu uživatelského rozhraní: 1, 2, 3, 4, 5, 6, 7, 8, 9.

Viz také API Design: The Principle of Least Surprise.

K Thread.interrupted(): Tato metoda řekne, zda bylo vlákno přerušeno, ale zároveň příznak přerušení odnastaví. Další volání této metody budou tedy vždy vracet false. Přitom z názvu metody toto chování vůbec není zřejmé.

Nabídněte cestu nejmenšího odporu

Uživatelé jsou jako voda v řece: Ta, má-li v korytě překážku, rozlije se tam, kde ji mít nechcete.

Buďte odolní/robustní

Selžete rychle

Hádanka (Java)

Co vypíše následující kus kódu?

public static void main(String[] args) {        
  Set<String> col = new HashSet<String>();
  col.add("Dagi");
  col.add("NkD");
  for (String key : col) {
    col.remove(key);
    System.out.println(key);            
  }
}

Správná odpověď je "NkD" a pak je bude vyhozena výjimka ConcurrentModificationException. Iterátory kolekcí v Javě jsou totiž fail-fast, tj. hlídají si, aby jim nikdo pod rukama nepřepsal kolekci, nad kterou zrovna iterují. Pokud se tak stane, upozorní na to výjimkou, což je lepší chování, než nedeterministický pád někdy v budoucnu.

Hádanka je převzata od Romana "Dagiho" Pichlíka.

Zvolte správnou úroveň abstrakce

Maximalizujte power-to-weight ratio

Příklad: Serializace XML dokumentu

Java

import org.w3c.dom.*;
import java.io.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;

// DOM code to write an XML document to a specified output stream.
private static final void writeDoc(Document doc, OutputStream out)
    throws IOException {
  try {
    Transformer t = TransformerFactory.newInstance()
      .newTransformer();
    t.setOutputProperty(
      OutputKeys.DOCTYPE_SYSTEM,
      doc.getDoctype().getSystemId()
    );
    t.transform(new DOMSource(doc), new StreamResult(out));
  } catch (TransformerException e) {
    throw new AssertionError(e); // Can’t happen!
  }
}

Příklad ukazuje na naprosto odbyté sbírání požadavků na API, protože serializace XML dokumentu je jedna z nejčastějších operací s XML/DOM API vůbec a v Javě je zcela zbytečně komplikovaná. Vede to ke kopírování stále stejných kusů kódu po celém programu a tedy zanášení duplicit a chyb.

Příklad je převzat z Přednášky Joshuy Blocha.

Příklad: Vyhledávání v části seznamu

Vector (Java 1.0)

public class Vector {
  public int indexOf(Object elem, int index);
  public int lastIndexOf(Object elem, int index);
  
  /* ... */
}

List (Java 1.2)

public interface List {
  List subList(int fromIndex, int toIndex);

  /* ... */
}

Původní vyhledávací metody třídy Vector jsou jednoúčelové a obtížně použitelné bez dokumentace (co znamená parametr index?). Oproti tomu metoda subList v novějším interfacu List umožňuje vykonávat na části seznamu libovolnou operaci, nejen vyhledávání. Je tedy silnější, má větší concept-to-weight ratio.

Příklad je převzat z Přednášky Joshuy Blocha.

Odbočka: Vyznačování podposloupnosti

Co by měl podle vás vypsat následující kód?

"abcd".substring(1, 3)
  1. "abc"
  2. "ab"
  3. "bcd"
  4. "bc"
  5. Něco jiného?

Odbočka: Vyznačování podposloupnosti

Co by měl podle vás vypsat následující kód?

"abcd".substring(1, 3)
  1. "abc"
  2. "ab"
  3. "bcd"
  4. "bc"
  5. Něco jiného?

Odbočka: Vyznačování podposloupnosti

Co by měl podle vás vypsat následující kód?

"abcd".substring(1, 3)
  1. "abc"
  2. "ab"
  3. "bcd"
  4. "bc"
  5. Něco jiného?
Java indexuje podposloupnosti přesně podle našeho 4. případu. Stejně je na tom Python. C# ve svých knihovnách většinou bere počáteční index a délku podposloupnosti, čímž se problému elegantně vyhýbá. Ruby problém přehazuje na uživatele a nabízí mu pro vyznačování intervalů objekt Range, který může reprezentovat intervaly shora otevřené i uzavřené.

Dokumentujte

Pokud nutíte uživatele hádat, je to špatně. Pokud je nutíte koukat se do zdrojového kódu, je to ještě hůř, protože je tím porušeno zapouzdření. Uživatelé nebudou programovat proti rozhraní, ale proti jeho jedné konkrétní implementaci. V tu chvíli ztrácíte flexibilitu ji někdy v budoucnu změnit.

Z důvodu větší flexibility implementace se vyplatí v dokumentaci příliš nepopisovat vnitřnosti – detailnost by měla být jen na takové úrovni, aby uživatel mohl s rozhraním pracovat.

Proces návrhu API

  1. Zjistěte požadavky na API
    • Skeptický přístup – uživatelé neví, co chtějí
  2. Use-cases
  3. Zpětná vazba
  4. Testovací kód proti API
    • "Tested on humans"
    • Později: příklady, unit-testy
    • "Pluginové" API: alespoň 3 různé implementace
  5. Iterace a stabilizace
  6. Implementace
    • Může být i dříve – záleží na ceně a možnosti změnit

Proces návrhu API

Jeden vedoucí designer

Připravte se na evoluci

Tip na přednášku

CZJUG

Jak psát API, které přežije nástrahy času

Data-driven programming & Domain-specific languages

Data-driven programming

Příklad: Progresivní zdanění

Java

int amountWithTax(int amount) {
  if (n <= 1000) {
    return amount;
  } else if (n <= 20000) {
    return Math.round(amount * 1.10);
  } else if (n <= 500000) {
    return Math.round(amount * 1.15);
  } else {
    return Math.round(amount * 1.25);
  }
}

Příklad: Detekce operačního systému

Java

String osName = System.getProperty("os.name");

if (osName.equals("SunOS") || osName.equals("Linux")) {
  System.out.println("This is a UNIX box and therefore good.");
} else if (osName.equals("Windows NT")
    || osName.equals("Windows 95")) {
  System.out.println(
    "This is a Windows box and therefore bad."
  );
} else {
  System.out.println("This is not a box.") ;
}

Příklad je převzat a upraven z článku Understanding Object Oriented Programming, kde ovšem slouží k vysvětlování něčeho úplně jiného, než na něm ukazuji já.

Z příkladu si prosím nevyvozujte, co si myslím o různých operačních systémech :-)

Příklad: Zobrazení uplynulého času

JavaScript

function getRelativeTimeString(delta) {
  if (delta < 60) {
    return "před méně než minutou";
  } else if (delta < 120) {
    return "cca před minutou";
  } else if (delta < (45 * 60)) {
    return "před " + (parseInt(delta / 60)).toString()
      + " minutami";
  } else if (delta < (90 * 60)) {
    return "cca před hodinou";
  } else if (delta < (24 * 60 * 60)) {
    return "před " + (parseInt(delta / 3600)).toString()
      + " hodinami";
  } else if (delta < (48 * 60 * 60)) {
    return "včera";
  } else {
    return "před " (parseInt(delta / 86400)).toString()
      + " dny";
  }
}

Příklad je převzat a upraven ze skriptu na stránce Martina Hassmana.

Společná vlastnost příkladů

Co mají příklady společného?

Společná vlastnost příkladů

Co mají příklady společného?

Příklad: Progresivní zdanění

Java

class TaxStep {
  int amount;
  float coef;
  
  /* ... gettery, konstruktor ... */
}

TaxStep[] taxSteps = {
  new TaxStep(1000,  1.00),
  new TaxStep(20000, 1.10),
  /* ... */
}
float defaultTaxCoef = 1.25;

int amountWithTax(int amount) {
  for (TaxStep step: taxSteps) {
    if (amount <= step.getAmount()) {
      return Math.round(amount * step.getCoef());
    }
  }
  return Math.round(amount * defaultTaxCoef);
}

Všimněte si, jak Java tím, že nemá heterogenní pole, komplikuje vložení dat do programu – museli jsme si na to vytvořit vlastní třídu TaxStep. Alternativou by bylo data vložit do dvou samostatných polí.

V kódu metody amountWithTax je malá duplicita kódu (přičítání daně) – to je záměr, po odfaktorování do samostatné metody by se mi kód už nevešel na slajd.

Je zřejmé, že položky pole taxSteps musí být seřazeny podle atributu amount. V reálném programu je na dobré na podobné věci explicitně upozornit komentářem na začátku deklarace.

Příklad: Detekce operačního systému

Java

Map<String, String> osMessages = new HashMap<String, String>;
osMessages.put(
  "SunOS",
  "This is a UNIX box and therefore good."
);
osMessages.put(
  "Linux",
  "This is a UNIX box and therefore good."
);
osMessages.put(
  "Windows NT",
  "This is a Windows box and therefore bad."
);
osMessages.put(
  "Windows 95",
  "This is a Windows box and therefore bad."
);
String defaultMessage = "This is not a box.";

String message = osMessages.get(System.getProperty("os.name"));
System.out.println(message != null ? message : defaultMessage);
Všimněte si, jak je datová část ošklivá kvůli tomu, že Java nemá literál pro asociativní pole. Její implementace této datové struktury navíc neumí nastavit, co vrátit za hodnotu v případě nenalezení prvku, důsledkem čehož je ošklivý ternární operátor ve výpisu.

Příklad: Detekce operačního systému

Ruby

os_messages = {
  "SunOS"      => "This is a UNIX box and therefore good.",
  "Linux"      => "This is a UNIX box and therefore good.",
  "Windows NT" => "This is a Windows box and therefore bad.",
  "Windows 95" => "This is a Windows box and therefore bad.",
}
os_messages.default = "This is not a box."

puts os_messages[PLATFORM];
Tato verze v Ruby by ve skutečnosti nefungovala, protože konstanta PLATFORM v Ruby nabývá jiných hodnot, než property os.name v Javě, ale chtěl jsem pro názornost zachovat strukturu kódu.

Příklad: Zobrazení uplynulého času

JavaScript

var timedMessages = [
  [60,  function(delta) { return "před méně než minutou"; }],
  [120, function(delta) { return "cca před minutou"; }],
  [45 * 60, function(delta) {
    return "před " + parseInt(delta / 60)).toString()
      + "minutami";
  }],
  /* ... */
];
var defaultMessageFunction = function(delta) { /* ... */ };

function getRelativeTimeString() {
  for (key in timedMessages) {
    if (delta < timedMessages[key][0]) {
      return timedMessages[key][1](delta);
    }
  }
  return defaultMessageFunction(delta);
}
Pro případné hnidopichy a dobré znalce JavaScriptu: Ano, vím, procházení pole v JavaScriptu pomocí for-in není zrovna košer. Nechtělo se mi příklad ještě víc motat indexy.

Další krok

Domain-specific languages

Domain-specific languages

Většinu problémů lze nějak zapsat pomocí konvenčních datových struktur (ostatně i kód je jen strom syntaktických elementů), ale takový popis většinou není dostatečně deskriptivní.

Více o DSL (a i jiných souvisejících věcech) se dočtete v článku Martina Fowlera Language Workbenches: The Killer-App for Domain Specific Languages?. Pěkné příklady použití uvádí Dave Thomas.

Dělení DSL

Externí DSL

Interní DSL

Příklady externích DSL

Regulární výrazy

Makefiles

SQL

K regulárním výrazům: Ne vždy je zápis pomocí regulárního výrazu nejpřirozenější zápis pro vyhledávací operaci v řetězce – test, zda je přípona souboru .exe vyjádříme asi lépe pomocí volání filename.endsWith(".exe"), než použitím regulárního výrazu /\.exe$/.

Pro pořádek: Ano, vím že regulární výrazy používané v běžných programovacích jazycích už dnes nejsou ekvivalentní klasickému stavovému automatu, ale jsou silnější.

Externí DSL jsou někdy "embeddovány" do jazyků obecných – např. v případě regulárních výrazů nebo SQL.

Příklad externího DSL

Řešení přesměrování na CZille

404-redirect.txt

From: http://www.czilla.cz/press_mozcz.html
To:   http://www.czilla.cz/czilla/tiskove-zpravy/stav-mozilla-cz.html

From: /http:\/\/www\.czilla\.cz\/clanky\/tz-(.*)\.html/
To:   http://www.czilla.cz/czilla/tiskove-zpravy/$1.html

404-ignore.txt

# z bloguje.cz odkazuji na nas stary javascript
Ignore: http://www.czilla.cz/externi.js

# spamroboti
Ignore: /\/cgi-bin\/mailform.pl$/
Ignore: /\/cgi-bin\/formmail.pl$/
Ignore: /\/cgi-bin\/formmail.cgi$/
Ignore: /\/cgi-bin\/contact.cgi$/
Ignore: /\/forum\/.*$/
V době, kdy jsem minijazyk na tomto slajdu navrhoval, jsem netušil, že je to DSL :-)

Příklady interních DSL

Mapování URL (Ruby on Rails)

# Return articles for a year, year/month, or year/month/day
map.connect "blog/:year/:month/:day",
            :controller   => "blog",
            :action       => "show_date",
            :requirements => { :year  => /(19|20)\d\d/,
                               :month => /[01]?\d/,
                               :day   => /[0-3]?\d/ },
            :day          => nil,   # optional
            :month        => nil    # optional

Příklady interních DSL

Validátory (Ruby on Rails)

class User < ActiveRecord::Base
  validates_inclusion_of :gender,
                         :in      => ["male", "female"],
                         :message => "should be 'male'" +
                                     " or 'female'"

  validates_format_of :length, :with => /^\d+(in|cm)/

  validates_length_of :name,     :maximum => 50
  validates_length_of :password, :in      => 6..20
  validates_length_of :address,  :minimum => 10,
                      :message => "seems too short"
end
Příklady jsou převzaty a lehce upraveny z knížky Agile Web Development with Rails od Dava Thomase a Davida Heinemeiera.

Důležitost jazyka

Lisp je vhodný především proto, že v podstatě nemá syntaxi, vše je jen volání funkcí, takže DSL v něm vypadá docela přirozeně jako součást jazyka. Důležitý je i silný systém maker.

Ruby má naopak syntaxi poměrně bohatou (dá se pomocí ní přirozeně vyjádřit mnoho konceptů) a na rozdíl od jazyků založených na C tato syntaxe "nepřekáží". Důležité syntaktické prvky jsou:

  • Absence středníků na konci příkazů
  • Nepovinné závorky u volání funkcí
  • Možnost specifikovat v definici funkce variabilní počat parametrů a pohodlná práce s nimi uvnitř funkce
  • Snadná emulace "keyword parameters"
  • Možnost předávat volaným funkcím bloky kódu, které mohou zevnitř vyvolávat, popř. si je uložit a vyvolat později (tzv. uzávěry)
  • Literály pro často používané datové typy (pole, asociativní pole, intervaly, regulární výrazy,...)
  • Dynamické typování a s tím související snadná tvorba heterogenních datových struktur (toto není vlastnost syntaxe, ale jazyka jako takového)

Porovnání interních a externích DSL

  Externí Interní
Oddělení od kódu ano ne
Syntaxe vlastní daná
Zpracování nutno napsat v rámci jazyka
Podpora IDE neexistuje dle jazyka
Editace neprogramátory ano ne

K syntaxi: Vlastní syntaxe znamená, že si ji můžete vytvořit dle libosti – tak, aby se vám dobře četla, upravovala, parsovala, automaticky zpracovávala, atd. Nevýhodou je, že tuto syntaxi musíte jako vývojář znát a používat. Pokud je v rámci jedné aplikace používáno více DSL s výrazně odlišnou syntaxí, je zaděláno na problém.

Dokumentace

Zdrojový kód. Komentáře. Popis rozhraní. High-level dokumentace.

Rozdělení dokumentace (hrubé)

Uživatelskou dokumentací se na semináři nebudeme zabývat. Jednak to do tohoto předmětu asi nepatří a jednak její tvorbě nerozumím :-)

Uvedené rozdělení je dost neostré – co třeba když jsou vaši uživatelé programátoři?

Zdrojový kód

Zdrojový kód

Komentáře

Proč psát komentáře?

Hlavní cíle komentářů

Příklad (Java)

/* Compute the square root of Num using the Newton-Raphson
   approximation. */

r = num / 2;
while (abs(r - (num / r)) > TOLERANCE) { 
   r = 0.5 * (r + (num / r)); 
} 

System.out.println("r = " + r);
Komentář v příkladu plní oba uvedené cíle.

Druhy komentářů

Opakování kódu

Příklad (Java)

i++; // increment i

Vysvětlení kódu

Popis záměru kódu

Druhy komentářů

Shrnutí sekce kódu

Dokumentace datových struktur

Druhy komentářů

Věci, co nelze v kódu vyjádřit

Značky

Zásady dobrého komentátora

Vyhněte se byrokracii/formalitám

Vyhněte se špatně udržovatelnému formátování

Příklad (Java)

/****************************************
 * Tohle je sice hezky výrazný          *
 * komentář, ale udržovat ho bude malá  *
 * noční můra.                          *
 ****************************************/

Nenahrazuje version-control systém

očůrávání kódu = potřeba u každé změny do komentáře napsat, kdo (někdy i kdy) ji provedl

Zásady dobrého komentátora

Nepoužívejte pomocné komentáře na konci bloku

Příklad (Java)

if (something) {
  /*
   * velmi
   *
   * dlouhý
   *
   * blok
   */
} // if

Přečtěte si McConnella

Popis rozhraní

Popis rozhraní

Ukázka: Java

Příklad (Java/JavaDoc)

public interface List<E> extends Collection<E> {
  /* ... */
  
  /**
   * Returns the element at the specified position in this list.
   *
   * @param index index of element to return
   * @return the element at the specified position in this list.
   * @throws IndexOutOfBoundsException if the index is out of range
   *         (<tt>index &lt; 0 || index &gt;= size()</tt>).
   */
  E get(int index);

  /* ... */
}

V Javě se na dokumentaci rozhraní používá nástroj JavaDoc. Programátor zapíše dokumentaci do speciálně označených a formátovaných komentářů, které JavaDoc projde a sestaví z nich dokumentaci v HTML.

Výhodou tohoto přístupu je, že dokumentace je pohromadě s kódem a hrozí tedy menší riziko jejího zastarání (kód bude aktualizován, ale dokumentace ne). Na druhou stranu, dokumentace zabírá v kódu dost místa a ztěžuje tak navigaci. Tento problém dnes naštěstí vcelku uspokojivě řeší různá vývojová prostředí.

Dokumentace v JavaDocu trpí značnou mírou byrokracie – např. kromě popisu metody je potřeba popsat zvlášť i její parametry, návratovou hodnotu a vyhazované výjimky, což udělá ze stručného neformálního komentáře na tři řádky poměrně dlouhý kus textu s redundancemi (zde např. popis metody a její návratové hodnoty). Pokud bychom popis některých prvků vynechali, objeví se ve vygenerované dokumentaci prázdné místo, což není moc pěkné. Příliš vhodné není ani použití HTML pro text komentáře, což vede k nutnosti escapování.

Byrokracie při psaní dokumentace vede k tomu, že psaní dokumentace je otravná činnost a programátoři ji tedy často nevykonávají, nejsou-li k tomu nuceni. Neformální komentáře by přitom možná napsali. Opět zde částečně pomáhají vývojová prostředí, která umí připravit šablonu dokumentačních komentářů.

Ukázka: C#

Příklad (C#/XML)

public interface List<E>: Collection<E> {
  /* ... */
  
  /// <summary>
  /// Returns the element at the specified position in this list.
  /// </summary>
  ///
  /// <param name="index">index of element to return</param>
  /// <returns>
  /// the element at the specified position in this list.
  /// </returns>
  /// <exceptions name="ArgumentOutOfRangeException">
  /// if the index is out of range
  /// (<c>index &lt; 0 || index &gt;= size()</c>)
  /// </exceptions>
  E get(int index);

  /* ... */
}
Byť je dokumentace v JavaDocu relativně byrokratická, je alespoň syntakticky stručná. Oproti tomu v C# je dokumentace psána pomocí XML, což je ukázkový příklad špatně zvoleného použití tohoto jazyka. Popravdě řečeno těžko chápu, jak v tom může vůbec někdo dokumentaci psát.

Ukázka: Ruby

Příklad (Ruby/rdoc)

class List
  # ...
  
  # Returns the element at the specified position in this list.
  # The position must be nonnegative and less than size of this
  # list, otherwise +IndexError+ is thrown.
  def get(index)
    # ...
  end

  # ...
end

Ruby pro dokumentaci používá nástroj rdoc, v principu podobný JavaDocu, ale místo HTML a @-tagů používá jednoduchou wiki-like syntaxi. Dokumentaci umí vygenerovat v HTML a několika dalších formátech.

Ukázka: Python

Příklad (Python/dokumentační řetězce)

class List:
  # ...

  def get(index):
    """Returns the element at the specified position in this list.
       The position must be nonnegative and less than size of this
       list, otherwise IndexError is thrown."""
    # ...

  # ...

V Pythonu je dokumentace integrována přímo do jayzka – je-li první výraz uvnitř třídy či metody řetězec, je považován za dokumentační komentář. Dá se k němu dokonce přistupovat v runtime pomocí atributu __doc__ (viz ukázka).

Je hezky vidět, že stejně jako v kódu, i v dokumentaci mají statické jazyky sklon k byrokracii a dynamické naopak k jisté neformálnosti.

Co na rozhraní dokumentovat?

Viz Effective Java: Programming Language Guide, Item 28.

Viz Effective Java: Programming Language Guide, Item 44.

High-level dokumentace

High-level dokumentace

Těžko říct, pro jak velké projekty už wiki přestane stačit. Osobně jsem se s takovým ještě nesetkal, ale mé zkušenosti jsou omezené.

Použitelné wiki

DokuWiki

MediaWiki

Hledání vhodného wiki jsem kdysi obětoval docela dost času, výsledkem bylo nalezení DokuWiki. Více o tom jsem napsal v článku.

Refaktorizace

Refaktorizace = re + faktorizace

Refaktorizovat či nerefaktorizovat?

Máme v programu ošklivý kus kódu, který ale funguje. Zrefaktorizujeme ho?

Otázka Pro Proti
Máme testy? ano ne
Bude kód často upravován? ano ne
Velká cena chyb? ne ano

Refaktorizací musíme získat víc, než vynaložíme.

Některé metodiky vývoje software, jako extrémní programování a programování řízené testy, považují refaktorizaci za samozřejmou součást života kódu. Doporučují refaktorizovat kdykoliv je k tomu příležitost s tím, že veškerý kód je pokryt testy, takže se není třeba příliš obávat zanesení chyb.

Osobně je mi tento přístup velice sympatický a snažím se ho ve svých projektech používat, ale ne vždy je to možné a ekonomicky výhodné.

Refaktorizace má i psychologický efekt: Programátory většinou potěší, že se jim podařilo z programu dostat pryč kus entropie. A s výsledným čistším kódem se jim lépe pracuje. To má i "manažerské" dopady jako nižší burn-out rate a tedy menší fluktuaci zaměstnanců, což zvyšuje produktivitu. Může být tedy vhodné nechat programátory refaktorizovat i ve chvíli, kdy refaktorizace sama o sobě nic podstatného nepřinese.

Kdy refaktorizovat?

Typické příležitosti

"Code smells"

Přehled "code smells"

Příliš dlouhý nebo zanořený cyklus: Řešením je vyextrahovat vnitřní části do samostatných funkcí.

Špatná koheze třídy: Jinými slovy porušení zásady "Každý objekt by měl být zodpovědný jen za jednu věc".

Nekonzistentní úroveň abstrakce rozhraní třídy: Často vzniká postupnou evolucí kódu.

Nutnost dělat paralelní změny na více místech: U složitějších programů není zdaleka vždy možné se této nutnosti zbavit.

Paralelní příkazy switch: Často je lepší využít polymorfismus.

Přehled "code smells"

Metoda využívá jinou třídu více než tu, ve které byla deklarována: Bývá signálem k přesunu metody do jiné třídy.

Přílišné používání primitivních typů: Správně bychom si měli definovat vlastní datové typy odpovídající sémantice kódu. Bohužel, zdaleka ne ve všech jazycích je to jednoduše možné. Např. v Javě je jedinou možností, jak definovat vlastní datový typ, vytvořit novou třídu. S tou se pak ale nedá pohodlně pracovat (mj. kvůli absenci přetěžování operátorů). Obecně platí, že prakticky všechny typové systémy v běžné používaných jazycích jsou poměrně slabé a umožňují toho vyjádřit málo.

Řetězec funkcí posílající si stejná data: Může signalizovat špatně navrženou abstrakci.

Podtřída využívající jen málo metod svého předka: Možná je na místě kompozice místo dědičnosti.

Přehled "code smells"

Komentáře používané na vysvětlení obtížně pochopitelného kódu: Je-li to možné, je dobré se držet zásady "Don't document bad code – rewrite it" (Kernighan a Plauger, 1978).

Setup/teardown kód okolo volání funkce: Příkladem může být inicializace prvků nějaké datové struktury před voláním funkce a "rozbalování" té samé nebo jiné datové struktury po volání. Kdo někdy pracoval s Win32 API, dobře ví, o čem je řeč.

Všechny uvedené "code smells" jsou opsány z McConnella. Na jiných místech můžete najít mnohé další, někdy i s návody na to, kterou refaktorizaci použít k nápravě:

Druhy refaktorizací

Rozdělení i všechny seznamy refaktorizací jsou opsány z McConnella. Stejně jako u "code smells" platí, že výčet není úplný a existují mnohé další.

Refaktorizace dat

Použití lokální proměnné místo parametru uvnitř metody: Myslí se tím dodržování zásady "Neměňte vstupní parametry v těle funkce".

Refaktorizace dat

Refaktorizace příkazů

Sloučení duplicitních fragmentů kódu v různých částech podmínky: Máte-li například stejný kus kódu na konci if-větve a else-větve podmínky, měli byste ho přesunout až za podmínku. Samozřejmě můžou nastat i podstatně složitější případy, kdy bude nutná extrakce do metody.

Refaktorizace funkcí

Zapouzdření přetypování směrem dolů (downcastingu): Pokud například píšete třídu EmployeeList zapouzdřující seznam objektů typu Employee, měly by jeho metody vracet tento typ, i když třeba nějaká podkladová třída pracuje s obecným Object. Přetypování by mělo být třídou zapouzdřeno.

Refaktorizace implementace tříd

Náhrada virtuálních metod inicializací dat: Pokud se podtřídy neliší kódem, ale jen hodnotou některých svých atributů, stačí v nich tyto atributy nainicializovat a společný kód ponechat v bázové třídě.

Refaktorizace rozhraní tříd

Skrytí delegáta: Představte si, že třída A volá třídu B a pak třídu C. Nebylo by lepší, kdyby volala jen třídu B a ta delegovala na třídu C? Někdy ano, někdy ne.

Přidání cizí funkce: Někdy byste potřebovali přidat k nějaké třídě novou metodu, ale nemůžete to z nějakého důvodu udělat. V tom případě musíte metodu přidat do jiné třídy, kde bude "cizí".

Přidání rozšiřující třídy: V předchozím případě (cizí funkce) se můžete rozhodnout vytvořit pro nově přidané metody novou třídu – rozšiřující třídu. Lze použít jak dědičnost (rozšiřující třída bude potomkem rozšiřované třídy), tak kompozici (rozšiřující třída bude obsahovat instanci rozšiřované třídy).

Refaktorizace rozhraní tříd

Zapouzdření nepoužívaných metod: Když zjistíte, že používáte jen omezenou část veřejného rozhraní třídy, může být vhodné vytvořit novou třídu, která má veřejnou jen tuto část, a používat ji místo třídy původní. Je důležité, aby i tato nová třída měla jasně definovanou abstrakci, tj. nebyla to náhodná sada metod.

Globální refaktorizace

Vytvoření jednoznačného zdroje pro data, která nemáte pod kontrolou: Některá data mohou být například skrytá v třídách GUI. Spodní vrstvy programu by neměly o existenci GUI nic vědět, je tedy lepší vytvořit nějakého prostředníka, který jim data bude zprostředkovávat.

Pomoc nástrojů

Tvorba refaktorizačních nástrojů pro statické jazyky je jednodušší, jelikož je v compile-time známo mnohem víc informací o struktuře kódu, než u dynamických jazyků. V dynamických jazycích si místo refaktorizačních nástrojů typicky musíme vystačit s tradičními Unixovými nástroji a skriptováním.

Ale i pokud pracujeme se statickým jazykem, zdaleka ještě nemáme vyhráno – například u C++ a C# se plete do cesty podmíněná kompilace, která tvorbu nástrojů také komplikuje. Je to pravděpodobně jeden z důvodů, proč jsou dnes asi nejdál refaktorizační nástroje pro Javu, která podobné vlastnosti nemá.

Testování

Unit testing. Test-driven development.

Cíle testování

Hlavní cíl

Vedlejší cíle

Testovat ručně nebo automaticky?

Všechny testy by měly být automatické

Jednoznačný výsledek je buď "test prošel", nebo "test neprošel".

Vyplatí se psát testy?

Záleží na ceně chyb a jejich nápravy

V případě obyčejné webové aplikace se formalizace testování pravděpodobně nevyplatí – data v aplikaci obyčejně nebývají důležitá, chyby nebývají fatální, uživatelé je poměrně rychle odhalí a jejich oprava je snadná. Samozřejmě, situaci může ovlivnit např. fakt, že aplikace je placená (uživatelé ze sebe nenechají dělat pokusné králíky a utečou ke konkurenci) a nebo se v ní manipuluje s důležitými údaji (často peníze).

U jakékoliv komponenty, která je používána na více místech už je cena chyb větší, protože je automaticky budou obsahovat všechny programy, které komponentu používají, a po opravě nalezených chyb se všechny tyto programy budou muset opravit také. Důkladné testování se zde už pravděpodobně vyplatí.

V případě software kardiostimulátoru, nebo obecně jakéhokoliv jiného software, kde jsou v sázce zdraví či životy lidí a nebo velké finanční částky, mnohdy investice do testování přesahují investice do vývoje samotného programu. Často se používají metody z kategorie formálních specifikací a model checkingu.

Rozdělení testů

Unit testy testují malé jednotky programu, typicky jednotlivé třídy a metody v nich. Testy komponent testují větší funkční celky, integrační testy pak jejich vzájemnou interakci. Systémové testy testují celý vyvíjený produkt jako celek v jeho finální konfiguraci a cílovém prostředí.

Regresní testy často vznikají implicitně, každý test spouštěný automaticky se totiž stává testem regresním.

Rozdělení testů

Black box testing

White box testing

Problémy testování

Opačný cíl než psaní programu je hlavně problém psychologický. Protože programátor chce, aby program fungoval, může mít (zejména je-li pod tlakem) tendenci testy vynechávat nebo odbývat, případně se v nich záměrně vyhnout některým situacím, o nichž ví, že v programu způsobí problémy.

Unit testing

Pokud si budete chtít unit testy opravdu vyzkoušet v praxi, a budete při tom využívat nástroj JUnit, doporučuji projít si nejdříve následující odkazy (nejlépe v uvedeném pořadí). Shromáždil jsem je při psaní našeho softwarového projektu.

Unit testing

Výhody a nevýhody unit testingu

Výhody

Nevýhody

K podpoře změn: Nejčastější důvod ke snaze neměnit, co funguje, je riziko zanesení chyb do programu. Pokud používáme unit testy, vytvářejí pod programem jakousi záchrannou síť a nově zanesené chyby většinou odhalí. "Většinou" samozřejmě není "vždy", ale pokud jsou testy dobře napsané, je pravděpodobnost zanesení obvykle dostatečně malá, aby změnám nebránila. Celkovým výsledkem používání unit testů jsou obvykle častější refaktorizace a tedy i čistější kód.

Jak vypadá takový unit test?

Co patří do unit testů

Navíc převeďte na unit test...

Co nepatří do unit testů

Test není unit test, pokud...

Testy, které toto dělají jsou také potřeba, a můžou být napsány s pomocí unit-testing frameworku, ale nejsou to unit testy.

Jak se nepatřičným věcem vyhnout?

Stubs/Mock objects

Pokrytí

Definicie pokrytí

100% pokrytí?

Obtížně testovatelné věci

GUI

Webové rozhraní

"Dobře" u JavaScriptu neznamená "technicky správně", ale "stejně jako prohlížeče".

Zásady pro psaní testů

1 třída kódu ~ 1 třída testů

Používejte správné asserty

K třídám kódu a testů: Toto pravidlo nemusí platit vždy, můžeme mít víc tříd testů na jednu třídu kódu. Pozor ale, je to často signál, že třída má víc různých zodpovědností. Podobné pravidlo o metodách neplatí – metody mají často víc různých aspektů, které je vhodné testovat zvlášť.

K používání správných assertů: Nebojte se si sadu assertů rozšířit, pokud zjistíte, že používáte nějaký assert spolu s pomocným kódem okolo na více místech.

Zásady pro psaní testů

Neodchytávejte výjimky

Snažte se kód co nejvíc "provětrat"

Kód testů je taky kód

Test-driven development

Několik zajímavých odkazů k tématu:

Cyklus

  1. Napsat test
  2. Spustit test, ověřit nesplnění
  3. Napsat kód
    • Minimum pro splnění testu
  4. Spustit test, ověřit splnění
  5. Refaktorizovat kód

Výhody a nevýhody

Výhody

Nevýhody

Experimenty zde myslím vyzkoušení naprogramování více variant řešení nějakého problému, jejich zhodnocení podle nějakého kritéria a následné použití jedné z nich a zahození ostatních. TDD může omezit experimentování s kódem v tom smyslu, že pokud máme k různým variantám kódu rovnou psát i testy, zvětší se náklady na každou variantu, což může vést až k tomu, že se experimentování nevyplatí. Někdy může být tedy výhodnější metodiku porušit, ozkoušet víc variant a teprve dodatečně k nim dopsat testy.

Pro inteligentního vývojáře může být také nepřirozené přemýšlet v tak malých úsecích návrhu/kódu, jak vyžaduje TDD; lépe by se mu pracovalo s většími celky a možná i na abstraktnější úrovni, než je kód. Pro takového vývojáře může být TDD ztráta času a tedy i produktivity.

Oba předchozí odstavce lze shrnout pod obecnou poučku, že žádná metodika není dokonalá a vhodná pro všechny situace.

Dodatky

Co jsme nestihli

Další náměty na témata

Budoucnost předmětu

Konec

Děkuji Jakubovi Vránovi za vychytání mnoha chybiček a překlepů.