/ janturon.cz / Od kodéra k analytikovi / Abstrakce a Rozděl a panuj

Abstrakce a Rozděl a panuj

Tyto principy jsou aplikovány od počátků programování a jsou natolik samozřejmé, že se o nich většina literatury ani nezmiňuje. Uvádím je zde jednak pro úplnost, a také pro představu těm, kterým běžné návrhové vzory zatím přijdou příliš komplikované.

Abstrakce

Při kódování se často stává, že potřebujeme provést podobnou posloupnost akcí jako před chvílí, pouze s drobnou změnou. Mnoho začínajících programátorů to řeší metodou copy&paste, což je antivzor: Velikost kódu tak začíná narůstat doslova geometrickou řadou a každou změnu je nutné provádět na několika místech. Vzhledem k tomu, že přehlédnutí jednoho takového místa většinou vede k chybě a v rostoucím kódu je čím dál tím těžší si všechna ta místa zapamatovat, brzy se tímto postupem dostáváme do stavu, kdy už nejsme schopni program dále vyvíjet.

V této slepé uličce dokáže programu vrátit jednoduchost "návrhový vzor" abstrakce. Dokonce není nutné celý kód rovnou mazat, lze ho upravit postupem refaktoringu - změna kódu beze změny jeho funkčnosti.

Pozn.: z důvodu jednoduchosti nahrazování zástupných symbolů (tokens) v řetězcích je zde použit kód C (kompatibilní s C++) s funkcemi printf a sprintf. Ryzí C++ kód lze nejvhodněji napsat pomocí boost ("druhý std") funkce format.

Příklad: vytiskněmě dětem násobilku dvou

printf("2 x 1 = 2"); printf("2 x 2 = 4"); printf("2 x 3 = 6"); printf("2 x 4 = 8"); printf("2 x 5 = 10"); printf("2 x 6 = 12"); printf("2 x 7 = 14"); printf("2 x 8 = 16"); printf("2 x 9 = 18"); printf("2 x 10 = 20");

Deset jednoduchých řádků, není problém. Dítě se to však rychle naučí, a potřebovalo by i násobilku tří, čtyř, pěti, šesti, sedmi, osmi, devíti a desíti. 1xcopy, 8xpaste a ruční úprava, případně nějaké hromadné nahrazení. Jsme na devadesáti řádcích, ale zvládli jsme. Pokud se ale budou na aplikaci hrnout další požadavky (třeba vytisknout násobilku do přehledné tabulky) - to už je lepší si to nakreslit ručně. A to je pouhá násobilka. Je načase provést abstrakci, která spočívá v následujících krocích:

  1. Nalezení závislosti mezi opakujícími se řádky.
  2. Zápis obecného řádku pomocí parametru.
  3. Úprava kódu s využitím obecného řádku pomocí parametru.

V tomto případě se mění znak před a za rovnítkem, jinak jsou řádky stejné. Obecný řádek by měl následující tvar:

printf("2 x i = 2i");

kde i nabývá hodnot 1 až 10. Násobilku dvou je tedy možné zapsat dvěma řádky:

for(int i=1; i<=10; i++) printf("2 x %d = %d", i, 2*i);

Provedeme refaktoring tím, že nakopírované desetiřádkové odstavce budeme nahrazovat tmto dvouřádkem: kód se zmenší o 80%, z devadesáti řádků na 18.

for(int i=1; i<=10; i++) printf("2 x %d = %d", i, 2*i); for(int i=1; i<=10; i++) printf("3 x %d = %d", i, 3*i); for(int i=1; i<=10; i++) printf("4 x %d = %d", i, 4*i); for(int i=1; i<=10; i++) printf("5 x %d = %d", i, 5*i); for(int i=1; i<=10; i++) printf("6 x %d = %d", i, 6*i); for(int i=1; i<=10; i++) printf("7 x %d = %d", i, 7*i); for(int i=1; i<=10; i++) printf("8 x %d = %d", i, 8*i); for(int i=1; i<=10; i++) printf("9 x %d = %d", i, 9*i); for(int i=1; i<=10; i++) printf("10 x %d = %d", i, 10*i);

Je vidět, že můžeme provést ještě jednu úroveď abstrakce, a kód se nám z 18 řádků smrskne na pouhé 3, ve kterých se úpravy provádí snadno:

for(int j=2; j<=10; j++) for(int i=1; i<=10; i++) printf("%d x %d = %d", j, i, i*j);

Rozděl a panuj

Vytiskněme předchozí příklad trošku přehledněji: tak, že dáme před každou násobilku nadpis:

for(int j=2; j<=10; j++) { printf("\nNásobilka %d.", j); for(int i=1; i<=10; i++) printf("%d x %d = %d", j, i, i*j); }

Kód je hezky hutný (změnu stačí zapsat jednou na jedno místo). Buďme ale důslední a vytiskněme ho do tabulky 3x3:

for (int k = 1; k <= 3; k++) { for (int j = 1; j <= 3; j++) printf("Násobilka %d ", k*j+1); puts(""); for (int j = 1; j <= 3; j++) printf(" ----------- "); puts(""); for (int i = 1; i <= 10; i++) { for (int j = 1; j <= 3; j++) printf("|%d x %d = %d| ", k * j + 1, i, i * (k * j + 1)); puts(""); } puts(""); puts(""); }

Nakonec jsme to víceméně zvládli. Kód je ale špagetový, tabulka je lehce rozhozená a opakuje se cyklus j. I zde by šla provést abstrakce, ale už bychom se nad tím museli zamyslet (a ten, kdo by to četl, by nad tím musel přemýšlet také), což je zbytečně drahé, a proto nepřípustné. Je načase na rozděl a panuj - rozdělení tohoto složitého problému na několik jednoduchých podproblémů, které snadno a přehledně vyřešíme zvlášť. Potom problém opanujeme tak, že jeho řešení poskládáme z právě vyřešených podproblémů.

Předně si lze všimnout, že to rozhození je způsobeno různou délkou číslic. Vytvořme si proto funkci, která dá před číslo dvě mezery, je-li menší než 10 a jednu mezeru, není-li 100. V tomto všimnutí, kde je problém, spočívá základní gryf programátorů.

char* NumIndent(char* result, int n) { if (n < 10) sprintf(result," %d",n); else if (n < 100) sprintf(result," %d",n); else sprintf(result,"%d",n); return result; }

Podíváme-li se na onu tabulku 3x3, zjistíme, že třikrát tiskneme totéž: záhlaví, podtržení a řádek násobilky. To jsou ony podproblémy, které vyřešíme zvlášť. Budeme přitom používat i abstrakci (to jsou ty parametry u podproblémů).

// podpodproblém rozhozené tabulky char* NumIndent(char* result, int n) { if (n < 10) sprintf(result," %d",n); else if (n < 100) sprintf(result," %d",n); else sprintf(result,"%d",n); return result; } // viz * char buffer1[4], buffer2[4], buffer3[4]; // podproblém záhlaví void printHeader(int n) { for (int i = 0; i < 3; i++) printf("Násobilka %s ", NumIndent(buffer1, n + i)); puts(""); } // podproblém podtržení void printUnderLine() { for (int i = 0; i < 3; i++) printf("----------------- "); puts(""); } // podproblém řádku void printLine(int set, int row) { for (int i = 0; i < 3; i++) printf("|%s x %s = %s| ", NumIndent(buffer1, set+i), NumIndent(buffer2, row), NumIndent(buffer3, row*(set+i)) ); puts(""); } // hlavní problém for (int k = 0; k < 3; k++) { int n = 3 * k + 2; printHeader(n); printUnderLine(); for (int j = 1; j <= 10; j++) printLine(n, j); puts(""); }

Každý úsek kódu (vč. hlavního problému) je nyní krátký a srozumitelný. Hlavní problém jsme opanovali pomocí podproblémů řádku, podtržení a záhlaví, podproblém řádku a záhlaví jsme dále vyřešili díky dílčímu problému rozhozené tabulky.

* Síla jazyka C se zde projevila ve formě nekonstantních řetězců (mutable strings): jiné jazyky (např. Javascript) používají pouze konstantní řetězce (a každá instance tak spotřebuje novou paměť) a spotřebují tak na volání NumIndent v printLine celkem 120B paměti, zatímco C přepisuje nepotřebnou paměť a vystačí si s 12B paměti. To nejsou závratná čísla, ale přeci jen je to desetkrát méně. Pro malé, často volané funkce je jazyk C velmi vhodný.

Výhodou Javascriptu je, že alokace vyrovnávacích pamětí (buffer1-3) přenechá interpretu (prohlížeči), nenarušuje tak obchodní logiku aplikace, která je čitelnější a méně pravděpodobně se do ní zavleče chyba. (Prohlížeč správu paměti pravděpodobně neprovede tak efektivně jako programátor v konkrétním případě, ale zato zaručeně bezchybně).