Tip pro lepší kód: místo kódu s komentářem napište funkci

Od Petr Zemek, 2014-06-28

Když programátor napíše blok kódu či složitější podmínku, tak má tendenci k vytvořenému kusu kódu napsat vysvětlující komentář. Z hlediska pochopitelnosti kódu je to samozřejmě lepší, než kdyby se čtenář musel snažit pochopit význam analýzou kódu. V dnešním příspěvku si však ukážeme lepší alternativu: místo bloku kódu s komentářem napíšeme funkci.

Původní kód

Mějme následující kód napsaný v jazyce C. Převzal jsem jej z jednoho starého projektu, na kterém jsem kdysi dělal. Jedná se o funkci pro vložení symbolu na vrchol zásobníku pro precedenční analýzu v překladači. Zkuste si jej pročíst.

// prec_stack.h
void prec_stack_push(PrecStack *s, Item item);
 
// prec_stack.c
void prec_stack_push(PrecStack *s, Item item)
{
    if (s->top == NULL) {
        // The stack is empty, so we initialize it with the first item.
        s->top = prec_stack_create_item(item);
        s->top->next = s->top->prev = NULL;
    } else {
        // There is already an item on the stack, so we add another one.
        struct prec_stack_item *new_item = prec_stack_create_item(item);
        new_item->next = s->top;
        new_item->prev = NULL;
        s->top->prev = new_item;
        s->top = new_item;
    }
 
    // If we have pushed a terminal, we need to store this piece of information
    // for the precedence analyzer.
    if (item.type == T_TERM) {
        s->topmost_term = s->top;
    }
}

Zásobník je implementován obousměrně vázaným seznamem. Při vložení položky se musíme dívat, zda je zásobník prázdný či nikoliv. To se zjistí tak, že se podíváme, zda na jeho vrcholu nic není (s->top == NULL). Pokud je zásobník prázdný, tak vložíme první položku, jinak vložíme další položku do neprázdného zásobníku. Nakonec se podíváme, zda je předaná položka terminál a pokud ano, tak si tuto informaci poznačíme.

Vylepšený kód

Místo kusů kódu s komentářem vytvoříme funkce s popisným názvem:

void prec_stack_push(PrecStack *s, Item item)
{
    if (stack_is_empty(s)) {
        push_item_to_empty_stack(s, item);
    } else {
        push_item_to_nonempty_stack(s, item);
    }
 
    if (is_terminal(item)) {
        mark_topmost_item_as_topmost_terminal(s);
    }
}

Zkuste si kód přečíst teď. Cítíte ten rozdíl? Nyní se kód čte tak, jakoby se jednalo o obyčejný anglicky psaný text. Nemusíme se starat o implementační detaily. Pokud by nás zajímalo, jak je něco implementované, tak se mrkneme na kód pomocných funkcí. Ty umístíme do daného modulu a označíme je jako privátní s využitím klíčového slova static. To nám zaručí, že nedojde ke kolizi symbolů při linkování, pokud bychom v některém jiném modulu měli funkci téhož jména:

static bool stack_is_empty(PrecStack *s)
{
    return s->top == NULL;
}
 
static void push_item_to_empty_stack(PrecStack *s, Item item)
{
    s->top = prec_stack_create_item(item);
    s->top->next = s->top->prev = NULL;
}
 
static void push_item_to_nonempty_stack(PrecStack *s, Item item)
{
    struct prec_stack_item *new_item = prec_stack_create_item(item);
    new_item->next = s->top;
    new_item->prev = NULL;
    s->top->prev = new_item;
    s->top = new_item;
}
 
static bool is_terminal(Item item)
{
    return item.type == T_TERM;
}
 
static void mark_topmost_item_as_topmost_terminal(PrecStack *s)
{
    s->topmost_term = s->top;
}

Všimněte si, že u pomocných funkcí ani nejsou potřeba komentáře, protože z jejich popisného názvu je jasné, co dělají.

Výhody uvedeného postupu

Napsání funkce místo bloku kódu s komentářem má obvykle následující výhody:

  • Čitelnost. Kód je čitelnější. Namísto toho, abychom se museli prokousávat implementačními detaily, které nás nemusí vůbec zajímat (např. způsob implementace zásobníku pomocí seznamu versus implementace pomocí pole), se můžeme soustředit na podstatné záležitosti (co daná funkce dělá). Dále, kratší kód bývá obecně jednodušší na pochopení, protože nemusíme v hlavě udržovat informace o spoustě vytvořených lokálních proměnných či zanořených podmíněných příkazech a cyklech.
  • Jedna úroveň abstrakce. Pomocí tohoto principu lze vytvářet funkce, jejichž kód je na jedné úrovni abstrakce. Opět to zvyšuje čitelnost.
  • Udržovatelnost. Tím, že máme kód izolovaný do menších funkcí, jej činíme udržovatelnějším. Kdybychom v původním kódu chtěli změnit způsob implementace vložení položky do zásobníku, museli bychom projít celý kód funkce prec_stack_push() a ujistit se, zda změnou neovlivníme kód, který je uveden později. Pokud máme kód izolovaný do samostatné funkce, stačí se podívat na parametry a návratovou hodnotu, protože to jsou jediné vstupy a výstupy.
  • Komentáře mají tendenci zastarávat. Komentáře mají oproti kódu tendenci zastarávat v situacích, kdy programátor změní kód, ale již změny nereflektuje do komentáře nad ním. Tím, že místo komentáře píšeme kód, toto riziko snižujeme.

Poznámky na závěr

  • I když je příklad výše v Céčku, koncept nahrazení kódu s komentářem za funkci je na jazyce nezávislý. U objektově orientovaných jazyků se pak jedná o vytvoření metody místo funkce.
  • Tento princip úzce souvisí s refaktorováním "vyjmutí metody" (angl. extract method).
  • Funkce is_terminal() by měla přijít do modulu definujícího Item, nikoliv do modulu pracujícího se zásobníkem pro precedenční analýzu. Jedná se o zápach v kódu zvaný Feature Envy.
  • V refaktorování by šlo jít ještě dál a vyjmout kód pro vložení prvku do samostatné funkce. Na úrovni prec_stack_push() by nás pak nemuselo zajímat, že je odlišný postup pro vložení prvního prvku a dalších prvků.
  • Někteří programátoři jsou velmi zdráhaví či neochotní k vytváření mnoha menších funkcí/tříd. I já jsem býval jedním z nich, než jsem si plně uvědomil výhody, které malé a soustředěné funkce/metody přinášejí. Tip zmíněný v tomto příspěvku považuji za jeden z nejdůležitějších, které jsem se kdy naučil.
Obsah tohoto pole je soukromý a nebude veřejně zobrazen.

Filtrované HTML (využíváno)

  • Povolené HTML značky: <a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <table>
  • Zvýraznění syntaxe kódu lze povolit přes následující značky: <code>, <blockcode>, <bash>, <c>, <cpp>, <haskell>, <html>, <java>, <javascript>, <latex>, <perl>, <php>, <python>, <ruby>, <rust>, <sql>, <text>, <vim>, <xml>, <yaml>.
  • Řádky a odstavce se zalomí automaticky.
  • Webové a e-mailové adresy jsou automaticky převedeny na odkazy.
CAPTCHA
5 + 12 =
Vyřešte tento jednoduchý matematický příklad a vložte výsledek. Např. pro 1+3 vložte 4.
Nějak se mi tady rozmohl spam, takže poprosím o ověření.

polacek@redhat.com (neověřeno)

10 years 1 month zpět

Docela dobré je taky u funkcí, co vracejí bool, přidat do jména suffix _p, aby bylo vidět, že se jedná o predikát. Tudíž třeba is_terminal_p, etc.

Petr Zemek

10 years 1 month zpět

In reply to by polacek@redhat.com (neověřeno)

Ono is_terminal_p() bych četl jako "je předaný symbol terminál typu p?" Např. is_terminal_id() ("je terminál typu id"?), is_terminal_int() ("je terminál typu int?") apod. Používám raději jen prefixy has_, is_ apod., ze kterých je většinou patrné, o co jde.

Když to vezmu trochu zeširoka, tak osobně jsem odpůrcem manglingu, maďarské notace, WinAPI konvencí apod. Přijde mi, že to naopak zbytečně zhoršuje čitelnost a správu kódu. Mám o tom již delší dobu rozepsaný příspěvek, ale ještě jsem se k jeho dokončení nedostal.