Z jakých důvodů a kdy bychom měli vytvářet funkce? Na to se podíváme právě dnes.
Historka na úvod
Během mého doktorského studia na VUT FIT mi kolega vyprávěl zajímavou historku. Dělal tehdy cvičícího a při opravování projektů narazil na jeden, který jej zaujal jednou velmi specifickou vlastností. Tou vlastností bylo, že všech 2 000 řádků projektu bylo v jediné funkci, main()
. Když se studenta ptal, proč kód nerozdělil do funkcí, dostalo se mu následující odpovědi: "Na základech programování nás učili, že funkci máme vytvořit v případě, kdy se nám kód opakuje. Mně se ale v projektu žádný kód neopakuje, takže mi přišlo zbytečné vytvářet další funkce." Ano, zní to úsměvně, ale realita je taková, že důvody pro vytváření funkcí nejsou vždy dobře známy. Pojďme se tedy na několik důležitých důvodů podívat.
Proč tedy vytvářet funkce?
Dobrých důvodů se najde hned několik.
- Odstranění duplicitního kódu. Jedná se o pravděpodobně nejčastější důvod pro vytváření funkcí. Aby náš kód byl tzv. DRY (z anglického Don't repeat yourself), měli bychom místo bezmyšlenkovitého copy&paste původní kód extrahovat do funkce, kterou na obou místech zavoláme. Abychom si to ukázali na příkladu, mějme následující kód v Pythonu:
can_edit_project_settings = user.is_admin() or user.is_developer() if can_edit_project_settings: ... # ... can_edit_project_settings = user.is_admin() or user.is_developer() if can_edit_project_settings: ...
Znalost o tom, kdy může uživatel změnit nastavení projektu, je na dvou místech. Pokud budeme oprávnění pro editování nastavení potřebovat upravit (např. přibude další role), tak je potřeba změnu provést na všech místech, na což se snadno zapomene. To může mimo jiné vést na bezpečnostní díru. Pokud je však znalost obsažena na jednom místě, tak stačí změnit toto místo:
def can_edit_project_settings(user): return user.is_admin() or user.is_developer()
- Abstrakce (skrývání složitosti) pro lepší pochopitelnost. Z hlediska pochopitelnosti je ideální, pokud funkce obsahuje kód na jedné úrovni abstrakce (princip zvaný Single level of abstraction). Je to podobné, jako kdybyste někoho učili řídit. Vysvětlíte mu, jak nastartovat, jak se točí volantem, jak funguje brzda, plyn a spojka. Již ale nemá smysl dotyčnému začátečníkovi vysvětlovat princip spalování v motoru či chemické vlastnosti benzínu, protože tyto detaily pro řízení auta vědět nepotřebuje. Pojďme se však podívat na programovací příklad. Mějme následující kód v C++:
void add_new_arg(FunctionCall fc, Expression arg) { Function f = fc.get_function(); unsigned int arg_count = fc.get_arg_count(); if (f.get_param_count() > arg_count || f.is_var_arg()) { // Můžeme přidat další argument. // ... } else { // Nelze přidat další argument. // ... } }
Do hlavní vysokoúrovňové logiky funkce (lze přidat další argument?) se nám pletou nízkoúrovňové detaily (jak zjistit, že do volání funkce lze přidat další argument?). Vhodnější je tuto kontrolu extrahovat do samostatné funkce (metody):
void add_new_arg(FunctionCall fc, Expression arg) { if (fc.new_arg_can_be_added()) { // ... } else { // ... } }
Koho by zajímaly detaily, může se podívat na implementaci
FunctionCall::new_arg_can_be_added()
.Dalším častým nešvarem je používání nízkoúrovňových operací ve vysokoúrovňovém kódu. Mějme následující kód v Pythonu:
order_date = db.get_order_date(order_id) # Datum je v databázi uloženo v UTC. My jej ale potřebujeme v lokální časové zóně. order_date = order_date.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).replace(tzinfo=None) # ...
Lepším řešením je nízkoúrovňový kód zapouzdřit do funkce:
order_date = db.get_order_date(order_id) order_date = utc_to_local_time(order_date) # ...
Kód bude mnohem čitelnější. Související výhodou je, že vytvořením funkce odpadne i nutnost vysvětlujícího komentáře.
- Znovupoužitelnost. Jak jsme mohli vidět u předchozího bodu, vytvořením funkce umožňujeme jednoduché znovupoužití na dalších místech. Místo toho, abychom kód opakovali, můžeme použít již hotové řešení.
- Jednodušší testování a ladění. S tím souvisí i jednodušší testování a ladění. Pokud je kód v samostatné funkci, tak stačí testovat a ladit ji. Její uživatelé pak mohou předpokládat, že funkce funguje, a nemusí tak např. u funkce zpracovávajících objednávky testovat, že funguje kód na převod dat z UTC do lokální časové zóny.
- Dodržování Deméteřina zákonu. O Deméteřině zákonu týkající se strukturální duplikace jste již možná slyšeli. Mějme pošťáka, reprezentovaného třídou
Mailman
, který doručuje balíček za 200,- Kč zákazníkovi, reprezentovaného třídouCustomer
. Při doručení je potřeba tento obnos zaplatit. Kód pošťáka by pro tuto akci mohl v Javě vypadat takto:
customer.getWallet().withdraw(new Money(200));
Tento kód však neodpovídá reálnému světu. Dali byste pošťákovi svou peněženku, ať si peníze vybere sám? Dále je kód špatně rozšiřitelný o další metody placení a porušuje princip Tell, don't ask. Především však ale obsahuje tzv. strukturální duplikaci (to, že zákazník má peněženku, přístupnou přes
getWallet()
, je obsaženo jak ve tříděCustomer
, tak ve tříděMailman
kvůli kódu pošťáka výše). Co s tím? MetodugetWallet()
ve tříděCustomer
nahradíme za metodugetPayment()
:public class Customer { public Money getPayment(Money amount); // ... };
Tím, že jsme přidali tuto novou metodu se kód pošťáka zjednoduší:
customer.getPayment(new Money(200));
Navíc nyní mnohem více odpovídá realitě, je rozšiřitelný (změny v
Customer.getPayment()
se ho nedotknou), odpadne zbytečná závislost třídyMailman
naWallet
a strukturální duplikace, kód je lépe testovatelný a již neporušuje princip Tell, don't ask.
Napadá vás ještě další důvod, proč vytvářet funkce? Podělte se o něj v komentáři!