Clean Code – čistý kód v praxi

čistý kód

Clean Code (čistý kód) by měl být základní výbavou každého svědomitého a čestného vývojáře. Nejde přeci jenom o nás, ale i o ty druhé, co budou mít tu čest pracovat na našem projektu s námi (či po nás) a ty, kteří na nás spoléhají. Jak se říká, pořádek dělá přátele. Dost řečí, pojďme se podívat na smyšlený příklad z praxe, na kterém si ukážeme, kolik času se dá ušetřit, když nebudeme programovat schizofrenně. Dejme našim uměleckým dílům určitá pravidla a řád tak, aby ostatní měli chuť při code review náš kód olíznout. Ukažme ostatním vývojářům, že Mozart se dá zahrát i na klávesnici.

Pokud Vám čistý kód nic neříká, zkuste začít s lehkým úvodem, který jsem psal nedávno. Teorii jsem sám neměl nikdy rád, ale je dobré vědět alespoň základy – ono tam toho vlastně ani víc není. Minimálně budete moci vypadat chytře, až se někdo bude vyptávat, proč by ho to vlastně mělo vůbec zajímat a co mu to přinese. Teorií ale asi jen tak někoho nepřesvědčíte, zvlášť když vám chybí tolik potřebné přesvědčovací skills, jako mě. Zkusme se tedy podívat na něco konkrétního, na čem si ukážeme, kolik času se dá doopravdy ušetřit, když děláme věci pořádně.

Uvažujme následující smyšlenou neexistující funkci, kterou nenapsal můj kolega Karel:

function kontrola($u)
{
  foreach ($u['o'] as $o) {
      if($o['p'] == -1 && $o['c'] == -1)
          return 1;
  }
  return false;
}

K čemu slouží tato funkce? Chvíle ticha…wtf

Teoreticky by mohlo jít klidně o funkci pro kontrolu prostaty. Kdo ví.

Nyní si představte, že máte tu čest spravovat eshop, který je plný takových kamarádských funkcí. Jednoho krásného rána vám poklepe na ramena produkťák s tím, že by chtěl do výpisu nových objednávek uživatele přidat i neaktivní objednávky. Vy budete hodinu hledat v kódu, až konečně zjistíte, že budeme muset upravit právě tuto funkci. Dejme tomu, že už nějakým magickým zázrakem víte, že úprava této funkce nerozbije nic dalšího, což na takto napsaných projektech bývá příjemná souhra náhod. Co ale tedy v této funkci upravit, aby se to chovalo tak, jak potřebujeme? Můžete hádat, ale vlastní boty na to určitě nevsadíte. Budete tedy muset hledat a zkoušet. To vás ale bude stát čas. Hodně času. Vemte si, že tímhle budete trávit celou Vaši pracovní dobu. Jaký je reálný poměr hledání v kódu ku psání na takovém projektu? Jestli budete psát více jak 10% procent Vašeho času, bude to úspěch, většinou to totiž bývá méně. Ukažme si tedy, jak tuto funkci jednoduše upravit, abychom mohli více tvořit a méně hledat a opravovat.

Používejte názvy vysvětlující význam

Jméno proměnné, funkce nebo třídy by mělo zodpovědět všechny zásadní otázky. Mělo by vám říci, proč existuje, co dělá a jak se používá. Pokud vyžaduje jméno poznámku, pak svůj význam neodkrývá. Neděláme jednopísmenné názvy proměnných. Stejně tak proměná $time nebo $result jsou příklady špatně nazvaných proměnných, protože nám jejich názvy vlastně moc nepomůžou ve vyjasnění, co představují a k čemu slouží. Nebojíme se tedy delších názvů proměných. $elapsedTimeInDays zní už mnohem lépe.

function hasUserNewOrder($user)
{
  foreach ($user['offers'] as $offer) {
      if($offer['paid'] == -1 && $offer['canceled'] == -1)
          return 1;
  }
  return false;
}

Zpátky k naší funkci. Není to takhle lepší? Hned víme, že funkce slouží k ověření existence nové objednávky uživatele, vstupem je uživatel, a iteruje se nad jeho objednávkami. Dokonce víme, že když je objednávka nezaplacená a nezrušená, vracíme jedničku, jinak false.

Používejte objekty místo polí

Do procedurálního kódu (tj. kódu používajícího datové struktury) se obtížně přidávají nové datové struktury, protože všechny funkce se musí změnit. Změní-li se způsob práce s objektem, stačí změnit funkci. Změní-li se způsob práce s polem, musíme změnit všechny výskyty.

function hasUserNewOrder(User $user)
{
  foreach ($user->getOffers() as $offer) {
      if($offer->isPaid() == -1 && $offer->isCanceled() == -1)
          return 1;
  }
  return false;
}

V tomto kroku jsme se zbavili polí. Vznikl objekt $user, který má u sebe objekty objednávek $offers. Nad těmi potom iterujeme a dotazujeme.

Nepoužívejte mixed datové typy

Existence mixed datového typu v našem projektu je prostor pro chybu, protože je třeba před každým jejím použitím ošetřit očekávaný datový typ. Pokud může jedna proměnná být string, null a array, je třeba před každou iterací foreach nad touto proměnou udělat kontrolu, jestli se jedná o pole. Kód je pak plný zbytečného ošetřování na datový typ. V horším případě na to nic netušící kolega Karel jednou zapomene, což může mít fatální důsledky.

function hasUserNewOrder(User $user)
{
  foreach ($user->getOffers() as $offer) {
      if($offer->isPaid() == -1 && $offer->isCanceled() == -1)
          return true;
  }
  return false;
}

Nyní jsme se zbavili nepříjemné vlastnosti naší funkce, která vracela někdy integer a někdy boolean.

Používejte Identical místo Equal (=== místo ==)

Pokud víme, čemu chceme aby se co rovnalo, tak to tak napíšeme. Použití equal je zbytečný prostor pro chybu, protože neporovnává datové typy a sem tam se rovná (či nerovná) něco, co jste nemuseli chtít. To že jste si jistí, že se tam taková hodnota nikdy nedostane, není validní argument. Nikdy nevíte, kdo a kdy po vás bude na projektu pracovat. Věděli jste například toto?

  • „“ == null // true
  • null == false // true
  • 0 == „“ // true
  • „0“ == 0 // true
  • „php“ == 0 // true
  • „1php“ == 0 // false
function hasUserNewOrder(User $user)
{
  foreach ($user->getOffers() as $offer) {
      if($offer->isPaid() === -1 && $offer->isCanceled() === -1)
          return true;
  }
  return false;
}

V tomto kroku budeme konkrétní a použijeme === místo ==.

Průběžně refaktorujte a zkrášlujte kód

Náš kód je naší vizitkou. S každou úpravou kódu se celek mění a sem tam se v kódu ukáže něco, co lze vylepšit nebo úplně odstranit. Je třeba si takových věcí všímat a kód průběžně vylepšovat. Chceme, aby vývojáři, kteří přijdou po nás, byli unešeni krásou a sofistikovaností našeho kódu a místo obecně známého a zažitého „sakra, kterej blbec tohle psal“ říkali jenom „wow“ (a nemyslím word of warcraft).

function hasUserNewOrder(User $user)
{
  foreach ($user->getOffers() as $offer) {
      if(!$offer->isPaid() && !$offer->isCanceled())
          return true;
  }
  return false;
}

V našem příkladu se zbavíme zbytečného přirovnání, protože použité funkce vrací boolean a porovnání na true/false pak není třeba.

Buďme pozitivní 🙂

Napůl plná sklenice zní lépe než na půl prázdná. Nejedná se o žádné klíčové pravidlo, spíše moje doporučení. Typicky při funkci vracející boolean máte na výběr dvě možnosti, jak funkci nazvat. Ve většině případů napíšeme jeden getter a když se potřebujeme zeptat opačně, dotaz negujeme. Obecně je lepší v takovém případě napsat pozitivní verzi, například je lepší zeptat se, zda-li je někdo živý, než se ptát zda není mrtvý.

function hasUserNewOrder(User $user)
{
  foreach ($user->getOffers() as $offer) {
      if(!$offer->isPaid() && $offer->isActive())
          return true;
  }
  return false;
}

V našem případě přepíšeme !isCanceled() na isActive(). Je pravda, že v reálném světe je diskutabilní, zda-li se jedná o opozita, uvažujme pro zjednodušení, že v tomto případě ano.

Každá fce by měla mít jeden vstupní a výstupní bod

PHP je narozdíl od javascriptu synchronní. Kromě mnoha nevýhod, které to s sebou nese, by se našlo i jedno plus. Pokud zařídíte, že každá funkce má jeden vstupní a jeden výstupní bod, je mnohem jednodušší takovou funkci debugovat a testovat. Je velmi nepříjemné, když při debugování delší funkce musíme zároveň zkoumat, který že return se vlastně použije a proč se nám to k našemu breakpointu (nebo velmi oblíbenému var_dump + exit) vůbec nedostane. Další výhodou je, že nám to umožní lépe dodržet pravidlo nepoužívání mixed typů, protože výsledek funkce je reflektován pouze v jedné proměnné.

function hasUserNewOrder(User $user)
{
  $hasNewOrder = false;
  foreach ($user->getOffers() as $offer) {
      if(!$offer->isPaid() && $offer->isActive())
          $hasNewOrder = true;  
  }  
  return $hasNewOrder; 
}

Naše funkce má dva výstupní body. Místo toho tedy vytvoříme proměnnou, která v sobě ponese výsledek a tu na konci vrátíme. Do funkce by šel přidat ještě early out pomocí break. To je ale spíše optimalizace (výstup funkce bude na jakýkoliv vstup pořád stejný) a pro tuto ukázku to není podstatné.

Public funkce děláme jen když to nejde jinak

Klíče od bytu bychom cizímu taky asi jen tak nedali. Každá public funkce je závazek. S jejím zveřejněním se může v projektu začít volat z různých míst. Od té doby už nemusí být úplně jednoduché takovou funkci výrazně měnit – například přejmenovat nebo změnit parametry. Proto je lepší takové funkce nedělat, pokud to není nutné. U private funkcí máte jistotu, že se používají jenom ve třídě, do které patří a je mnohem jednodušší najít její použití. Public funkce je jako dítě, když ji uděláte, budete se o ni muset starat.

private function hasUserNewOrder(User $user)
{
  $hasNewOrder = false;
  foreach ($user->getOffers() as $offer) {
      if(!$offer->isPaid() && $offer->isActive())
          $hasNewOrder = true;  
  }  
  return $hasNewOrder; 
}

Naší funkci si v tomto kroku schováme.

Vždy balíme kód do bloků

Určitě ne moc důležité pravidlo, na které jsem si sám z javascriptu odvykl, ale do čistého kódu by asi patřit mělo. Význam je prostý. Blok totiž například u ifu není nutně třeba, dokud máme jen jeden řádek kódu, který se má po splnění podmínky vykonat. Často se ale stává, že časem potřebujeme logiku v bloku ifu rozšířit. Nepozorný vývojář je zvyklý jít na konec řádku, za který chce přidat nový kód (v našem případě $hasNewOrder = true;) a skočit po enteru a připsat další řádek. To ale způsobí chybu, protože pokud není kód v bloku, tento již do podmínky nepatří. Dalším důvodem bude jistě lepší čitelnost.

private function hasUserNewOrder(User $user)
{
  $hasNewOrder = false;
  foreach ($user->getOffers() as $offer) {
      if(!$offer->isPaid() && $offer->isActive()) {
          $hasNewOrder = true;  
      }
  }  
  return $hasNewOrder; 
}

Závěr

Nyní se tedy vraťme na začátek. Pamatujete naše zadání?

„Jednoho krásného rána vám poklepe na ramena produkťák s tím, že by chtěl do výpisu nových objednávek uživatele přidat i neaktivní objednávky.“

Povedlo by se vám udělat potřebnou úpravu na této funkci ve tvaru, na kterém jsme začínali?

function kontrola($u)
{
  foreach ($u['o'] as $o) {
      if($o['p'] == -1 && $o['c'] == -1)
          return 1;
  }
  return false;
}

Nyní to zkusme na funkci po úpravách.

private function hasUserNewOrder(User $user)
{
  $hasNewOrder = false;
  foreach ($user->getOffers() as $offer) {
      if(!$offer->isPaid() && $offer->isActive()) {
          $hasNewOrder = true;  
      }
  }  
  return $hasNewOrder; 
}

Funkce je na první pohled čitelnější a úpravy se budou dělat mnohem jednodušeji. Nebude třeba dlouze hledat a zkoušet, čímž ušetříme mnoho času, zvlášť pokud máme projekt plný nečitelných funkcí. Matiku si musí promyslet každý sám, já jsem přesvědčen, že ušetřený čas bude naprosto zásadní pro budoucnost Vašeho projektu. Nebudu Vás ale napínat, zpět k našemu úkolu. Nové produktové zadání by jsme mohli na přepsané funkci vyřešit takto.

private function hasUserNewOrder(User $user)
{
  $hasNewOrder = false;
  foreach ($user->getOffers() as $offer) {
      if(!$offer->isPaid()) {
          $hasNewOrder = true;  
      }
  }  
  return $hasNewOrder; 
}

Většině z vás bylo asi od začátku jasné, že se bude jednat o úpravu podmínky, málo kdo by to ale trefil přesně. Troufám si říct, že na přepsané funkci po aplikaci pár pravidel čistého kódu se čitelnost funkce násobně zlepšila a je možno na ní dělat úpravy bez zdlouhavého hledání a zkoušení. Nemyslíte?

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *