V dnešním díle seriálu o chybách v návrhu se podíváme na nešvar, který znepříjemňuje práci uživatelům knihoven: nekonzistentní rozhraní.
O co půjde?
Každý z nás již někdy používal nějakou knihovnu. Ať se už se jednalo o standardní knihovnu daného jazyka či o externí knihovnu specializovanou pro daný účel, tak použitelnost této knihovny byla do značné míry dána snadnosti jejího používání. S tím souvisí i to, jaké má knihovna aplikační rozhraní (API), tj. jak je knihovna strukturována, jak jsou pojmenovány obsažené entity (třídy, metody, typy), co tyto entity dělají apod. Pokud byla knihovna, a především její rozhraní, navržena kvalitně, tak se bude snadno používat a její uživatelé budou spokojení. Pokud ale bylo rozhraní šito horkou jehlou, budou na knihovnu uživatelé nadávat. V tomto příspěvku bych se chtěl zaměřit na jeden z nešvarů, který se u knihoven velmi často objevuje: nekonzistentní rozhraní.
Jak vypadá nekonzistentní rozhraní?
Nebudeme běhat kolem horké kaše a hned si ukážeme, jak se může projevovat nekonzistentní rozhraní.
-
Nekonzistentní pozice parametrů. Určitě jste se s tímto problémem setkali. Chcete zavolat funkci či metodu a přemýšlíte nad pořadím, v jakém ji předat argumenty. Tvůrce knihovny by se měl snažit o to, aby se funkce a metody daly používat jednotným způsobem. Vezměme si např. funkce
fputs()
afprintf()
ze standardní Céčkové knihovny, jež mají následující prototypy:V prvním případě se výstupní stream předává jako poslední parametr, ve druhém jako první parametr. Zde je to do značné míry dáno tím, že
fprintf()
má proměnný počet parametrů, takže to nejde udělat naopak. Co ale ufputs()
dát parametrstream
jako první?Jako další příklad bych chtěl uvést funkce
in_array()
astrstr()
ze standardní knihovny jazyka PHP:bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) string strstr ( string $haystack , mixed $needle [, bool $before_needle = false ] )
V prvním případě se nejdříve předá hledaný objekt a poté pole, v druhém případě se nejdříve předá řetězec a až poté hledaný objekt.
-
Nekonzistentní pojmenování. Při pojmenování byste se měli držet jednotného stylu a použití zkratek. Jinak uživatelé vašich knihoven budou tápat, zda napsat
Function
neboFunc
, či dokonce jenFun
. První příklad nekonzistence tohoto typu máme ve standardní knihovně C++:#include <sstream> std::stringstream s;
Třída se nazývá
stringstream
, ale je v hlavičkovém souborusstream
...Další ukázka je z jazyka PHP, kde je velmi často mícháno, zda je k oddělení slov v názvech funkcí použito podtržítko či nikoliv. Ukázky:
string stripslashes ( string $str ) string strip_tags ( string $str [, string $allowable_tags ] ) int strcmp ( string $str1 , string $str2 ) mixed str_replace ( mixed $search , mixed $replace , mixed $subject [, int &$count ] ) int msql_numfields ( resource $result ) int mysql_num_fields ( resource $result )
Jako poslední příklad si uvedeme názvy metod pro zjištění velikosti kolekce v různých jazycích:
size()
,length()
acount()
. Schválně zkuste zavzpomínat, která z těchto pojmenování se používají v knihovnách, které používáte. -
Nekonzistentní typy. Pro daný účel byste se měli rozhodnout a používat typy konzistentně. Vezměme si např. C++. Pro celočíselné typy je možno použít jak vestavěné typy, tak
typedefy
:signed {char, short, int, long, long long} unsigned {char, short, int, long, long long} size_t container::size_type
a spoustu typů z cstdint. Mnohdy se pak stává, že jedna funkce z třídy má typ parametru
unsigned int
a podruhésize_t
, což nemusí být vždy to stejné a může dojít k problému.Další nekonzistence může být v tom, zda se argument má předat hodnotou, přes ukazatel či přes referenci. Pokud se u jedné funkce musí psát před objekt
&
a u jiné nikoliv, může to způsobit uživateli frustrace. Např. v LLVM se běžně pracuje s funkcemi, reprezentovanými třídouFunction
, přes ukazatel:static Function * Function::Create (FunctionType *Ty, LinkageTypes Linkage, const Twine &N="", Module *M=0) static NodeType * CFG::getEntryNode(Function *F) Function * CallGraph::removeFunctionFromModule(Function *F)
Když si ale vytvoříte průchod nad funkcemi zděděním třídy
FunctionPass
, tak u něj je virtuální metoda, při které funkci dostanete přes referenci:virtual bool FunctionPass::runOnFunction (Function &F)=0
Jednou je pak třeba psát
F->method()
, jindyF.method()
. -
Duplicity. Pro jednu věc by zde měla být jedna funkce/metoda. Předejdete tak nedorozuměním, kdy si uživatel není jistý, kterou z funkcí použít, když u obou to vypadá, že dělají to stejné. Příkladem může být třída
std::string
v C++, mající tyto dvě metody, které dělají to stejné:size_type size() const; size_type length() const;
U PHP to jsou např. následující funkce, z nichž všechny provádějí rozdělení vstupu, ale s mírnými odlišnostmi:
array split ( string $pattern , string $string [, int $limit = -1 ] ) array spliti ( string $pattern , string $string [, int $limit = -1 ] ) array str_split ( string $string [, int $split_length = 1 ] ) array preg_split ( string $pattern , string $subject [, int $limit = -1 [, int $flags = 0 ]] ) array explode ( string $delimiter , string $string [, int $limit ] ) string strtok ( string $str , string $token )
Samozřejmě, dalo by se nalézt mnoho dalších typů nekonzistencí, které znesnadňují uživatelům knihoven život. V příspěvku jsem zmínil jen některé. Snažte se vždy o to, aby vaše rozhraní byla konzistentní. Vaši uživatelé vám za to poděkují.