Jakože 500 řádek je moc? Kolik je moc pro tebe? Jen tolik, kolik se vejde na monitor? Já zas neholduju tunám napůl generovaných souborů.
EDIT: Navíc špageta je, když je to dlouhý kód, který něco dělá. V jeho případě jde jen o větvení highlevel kódu podle parametrů příkazového řádku (je jich prostě moc).
19. 2. 2024, 12:48 editováno autorem komentáře
Právě proto bych každý command řešil v separátní funkci. Větvení je základní operace; tím že do funkce nacpeš "if", že se ta funkce nestane snesitelně dlouhou. Pro mě je moc, když jedna funkce dělá moc věcí, které by se daly krásně rozdělit na podfunkce. Nejsem dogmatik, ale jedno vodítko je třeba v Clean Code:
The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. Functions should not be 100 lines long. Functions should hardly ever be 20 lines long.
19. 2. 2024, 12:56 editováno autorem komentáře
Ne. Ale také, když sám nepomůžu, nenadávám. Ani na díru v asfaltu, ani na cizí kód. (Dokud jej nemám sám opravovat. A I pak se raději vyjadřují opatrně, protože autor může mít nějaký dobrý důvod pro své počínání. Navzdory příručkám a všem "dobrým mravům" teoretiků.)
19. 2. 2024, 21:49 editováno autorem komentáře
> OK. Dal jsi někam na Github pull req. nebo fork s touto opravou? Jen se ptám, jestli jen kritizuješ, nebo i něco děláš.
Sorry, ale co je tohle za otázku? Jednak jsem se o projektu dověděl dnes, jednak logicky nemohu přispívat všude a jednak přece nechci kecat autorům do toho, že to píšou takhle - předpokládám, že se jim to takhle líbí.
Prostě to na mě působí jako spousta jiných projektů v C, které jsem viděl, takže jsem chtěl hlavně vyvolat nějakou diskusi o tom, jestli to v C jinak nejde, jestli to někdo další považuje za dobré a tak.
> Právě proto bych každý command řešil v separátní funkci.
A odkud by jsi ty separátní funkce volal?
Ve funkci `main` je sice spousta `if ...`, ale každá odpovídá více méně nějaké volbě příkazové řádky, takže `if` by tam zůstaly, jen by v tělě volaly další funkci a teprve v ní by bylo tělo kódu, který to zpracuje.
Nějak mi uniká, co se tím vyřeší. Takhle je to nakonec přehlednější, protože i když to na první pohled nevypadá, oni se tam volají další funkce, tady třeba funkce `following_add`
```c
if (strcmp(cmd, "follow") == 0) { /** **/
xs *msg = msg_follow(&snac, url);
if (msg != NULL) {
char *actor = xs_dict_get(msg, "object");
following_add(&snac, actor, msg);
enqueue_output_by_actor(&snac, msg, actor, 0);
if (dbglevel) {
xs_json_dump(msg, 4, stdout);
}
}
return 0;
}
```
Takže jako jo, můžeš to ještě obalit jednou funkcí, ale proč?
Druhá věc je to, a s tím souhlasím, že jazyk `C` je trochu neohrabaný (například místo několikanásobné `if` by byl hezčí nějaký `switch` a předávání parametru odkazem je taky zdrojem problémů.
Jestli chápu dobře, tak navrhujete :
- Enum pro commandy
- Fukci string na enum
- Switch podle enumu
- řadu funkcí pro jednotlivé položky
Takže tu máme 3-4 věci, které jsou tak těsně svázané, že jedna bez druhé nedávají smysl. Najednou musíte hlídat 3-4 různá místa, aby byly v souladu a překladač moc nepomůže.
A za tenhle boilerplate navíc získáte to, že budete mít řadu bloků příkazů jako předtím. Akorát se změní že nad každým blokem nebude název toho příkazu jako string v ifu, ale jako jméno funkce.
Změní se toho podstatně víc. Každá funkce má nějaký omezený dosah, stejně jako její proměnné. Nebude docházet k zaplevelení jmenného prostoru a případným překvapením, když někdo něco přehlédne. Ale hlavně mám jistotu, že main() nebude nadále nekontrolovatelně bobtnat, až tam zase něco někdo bude chtít přidat. Jenom kousek tady, kousek tam, už je to stejně celkem dlouhé, tak co.
Že musím hlídat 3-4 různá místa samozřejmě může nebo nemusí být pravda - záleží na tom, co jazyk a použitá knihovna dovolí a dokáže.
> Každá funkce má nějaký omezený dosah, stejně jako její proměnné.
Dosah proměnných je omezený závorkama pod každým ifem. Cokoliv je venku z ifů je sdílené mezi více příkazy a muselo by to být venku i ve vašem řešení se switchem.
A alternativa k bobtnání mainu je co? Bobtnání enum, StringToEnum a pak té řady funkcí? Že to ve výsledku nabobtnalo ještě víc je jedno, vždyť na každém místě přibyl jen kousek.
> Že musím hlídat 3-4 různá místa samozřejmě může nebo nemusí být pravda - záleží na tom, co jazyk a použitá knihovna dovolí a dokáže.
Já myslel, že se bavíme konkrétně o C. A 3-4 místa jsou to právě proto, že C. Jo, kdyby byl v C obecný StringToEnum, jedno místo by možná odpadlo. Pokud se ten switch napíše tak, že překladač může chytnout nepokrytý enum, tak snad taky jedno místo pro chybu odpadne.
Jo, C má v tomhle rozhodně své limity. Ale zároveň dost omezení není v jiných jazycích o moc lepších.
Ty příkazy mají jen pár ruzných druhů parametrů. Ale ty parametry mají navázané společné kusy kódu (třeba user_open). Tady ty příkazy stačilo podle těch společných parametrů seřadit.
Ve vámi navrhovaném switchi by to takhle nešlo. Tam by ty společné části bylo nutno buď zduplikovat do každé funkce, nebo provést podmíněně před tím switchem. To podle jazyka a použitého parseru commandliny znamená buď další navázané "předswitche" před tím hlavním switchem, nebo použití nějakého mocného parseru commandliny s konfigurací odpovídající složitosti. A obě ty možnosti jsou těsně svázané s tím hlavním switchem.
Jo, nebyla by to funkce na 500 či kolik řádků. Ale jestli by to ve výsledku bylo jednodušší si nejsem vůbec jistý.
> Napríklad by sa to dalo otestovať...
Možná byste se na ten kód měl podívat. Funkcionalita, pro kterou mi dává smysl psát unittesty je spíš v jiném souboru. Tenhle main to akorát volá podle parametrů commandliny a přidává nějaké kontroly a výpisy. Na téhle úrovni má automatizované testování smysl spíš v opakovaném spouštění celého programu a kontrolování výsledků.
A když už budete psát testy, tak tenhle test byste měl mít tak jak tak. Protože i kdybyste ty příkazy vytáhl do samostatných funkcí, tak potřebujete otestovat i tu přidanou vrstvu, co ty jednotlivé funkce volá.
> Právě proto bych každý command řešil v separátní funkci.
No ale když se podívám na ten main, tak to tak dost často je. Obvykle jedna funkce, co dělá to hlavní a kolem pár kontrol a výpisů.
Vytrhávat z tohohle funkce akorát tyhle bloky přemístí v rámci souboru. Opravdu nějak čitelnosti pomůže, když název toho, co ten blok dělá najednou nebude v úvozovkách? A přibude tam boilerplate na předávání potřebného kontextu?
Nejste dogmatik, ale citoval jste ukázkový příklad cleancoderského dogmatismu. Počet řádků je zrovna metrika na pendrek. Funkce má být tak velká aby dělala jednu věc a dávala smysl sama o sobě. Na kolik řádků to bude závisí na vlastní složitosti toho, co ta funkce dělá, na použitém programovacím jazyce atd.
Cleancoderský "dogmatismus" přece není opravdový dogmatismus, je to "rule of thumb". Předpokládám, že autor sám by nevolal k popravě někoho, kdo udělá delší funkci než X řádků, pokud to nelze rozumně jinak. Že ty funkce budou v rámci jednoho souboru, je jenom jedna možnost. Uznávám, že v jazyce, který nemá moduly, je vnímáno jako opruz, když se mají funkce vkládat jinam.
Proč nepoužít nějaký rozumný parser příkazové řádky, který automaticky řeší spoustu věcí na jednom místě? Proč nepoužít namísto těch ifů (kde na první pohled nikdo neví, jestli jsou si rovny / jsou exkluzivní nebo ne), nějaký switch?
> Proč nepoužít nějaký rozumný parser příkazové řádky, který automaticky řeší spoustu věcí na jednom místě?
Protože ten formát příkazové řádky je jednoduchoučký. Nejsou tam žádné volitelné inty a floaty, žádné pojmenované parametry, jen pár stringů. Jeden z nich je občas url. U něčeho takového by kontrola výsledků toho parseru vyšla +- složitá jako tohle.
> Proč nepoužít namísto těch ifů (kde na první pohled nikdo neví, jestli jsou si rovny / jsou exkluzivní nebo ne), nějaký switch?
Protože C neumí switch podle stringu. Takže ty ify tam budou muset být vždycky, navrch k tomu switchi.
> pokud to nelze rozumně jinak.
Jakoukoliv funkci jde vždycky seknout na půl. A pod to nenápadné slůvko "rozumně" se schová celá tahle naše debata.
> Uznávám, že v jazyce, který nemá moduly, je vnímáno jako opruz, když se mají funkce vkládat jinam.
Vytáhnout funkci do jiného souboru je i v C brnkačka. Tohle není těžké. Jen nedává smysl rozhazovat těsně svázané funkce do víc souborů. Protože ke skákání mezi funkcema přibude ještě overhead s přepínáním mezi soubory.
Ano ano.
A tohle (tahle diskuse) je přesně důvod, proč programy v céčku jsou většinou celkem pohodově čitelné, kdežto ve "vyšších jazycích" se v tom bez nějaké InteliSense ani prase nevyzná.
BTW On i ten switch jde docela jednoduše když se chce, ale obvykle to dává smysl v případech, které bych rozhodně vrcholem čitelnosti nenazval (tím myslím tu čuňárnu, kdy se některé case neukončí breakem, ale nechaj se proběhnout další větví.)
Tak zrovna 500 řádková funkce s jednou podmínkou plnou kódu za druhou je bez chytřejšího editoru docela nečitelná, protože nejde vidět celý kontext najednou. Neříkám že bych to nutně přesouvala do jiného souboru, ale rozhodně by mi přišlo čitelnější mít pod sebou podmínky které volají separátní funkce (nehledě k tomu že už vhodně zvolené jméno funkce v tu chvíli přispívá k dokumentaci kódu)
Btw, ono je potřeba dát bacha i na druhý extrém. Byla jsem v situaci kdy jeden programátor v teamu odmítal používat jakýkoliv pokročilejší editor a celý vývoj se podřídil tomu co je čitelné pro něj. Což ve výsledku vedlo ke zhoršené čitelnosti pro všechny ostatní. Kolik lidí dneska bude vyvíjet a číst kód bez něčeho co má alespoň základní porozumění kódu? LSP už integrují i jednoduché textové editory.
ale tady ten kód není nepřehledný, ta délka je daná počtem argumentů, jednotlivé blocky if jsou oddělený vizuálně, všechny mají podobnou syntax a lze rychle rozponat, co daný block zpracovává. Ten kód se neliší příliš od jiných C kódu a je to částečně dané i omezením jazyka. Nebo co komu tam připadá nečitelné? Pokud mám 20 příkazů tak se mi těžko jejich zpracování vleze na jednu stránku.
Základ jakéhokoliv přispění do existujícího kódu (ať už v rámci týmu, tak v rámci open source) je prostě dodržení shodného code style. Často při přispěvování do open source ztrávím více času zjišťováním jak ten kód naformátovat, pojmenovat, rozložit než samotným kódem.
Programuji i v Javě bez LSP. Nemám rád, když na mě editor křičí, když ještě kód dopisuji a postupně iteruji, ale to je moje cesta, ať si každý volí jak mu to vyhovuje.
> čitelnější mít pod sebou podmínky které volají separátní funkce
Už jsem to tu jednou naznačoval. V jednom případě máme pod sebou řadu bloků pro jednotlivé příkazy a ty bloky jsou těla ifů. V druhém případě máme pod sebou řadu bloků pro jednotlivé příkazy a ty bloky jsou těla funkcí. Liší se to jen tím, jestli je ten název příkazu, který ten blok vykonává v úvozovkách nebo bez nich.
A v druhém případě nám přibude ještě kus boilerplate kódu, který ty jednotlivé funkce volá. Ale čitelnost tohohle kusu je celkem irelevantni. Není tam nic zajímavého ke čtení a vlastně se to nedá ani napsat více či méně čitelně. Údržba tohohle kusu není o čtení kódu ale o mechanickém odškrtávání pokrytých možností. V ideálním případě nechci, aby takový kód byl čitelný, ale aby vůbec nemusel existovat.
zanořování do funkcí, abstrakce, fasády se v C tolik nenosí, ten kód bývá zpravidla dlouhá nudle nějakých blocků kódu. Z velké části za to může zvyk lidí, kteří to dělají. Má to i technické důvody, ten boilerplate nic funkčního nepřináší, musí se čas od času refactorovat a to výrazně snižuje dlouhodobou udržovatelnost a změny pomoci patchů.
Podívejte se třeba na takové zpracování argument nástroje ls z coreutils, https://github.com/coreutils/coreutils/blob/master/src/ls.c#L1912-L2484, sice se používá switch, ale 500 řádků jak nic.
Na první pohled to vypadá jako pěkný nepořádek, za mě je ale naprosto zásadní, že ten kód má stejnou strukturu jako 30 let stará verze stejné funkce https://github.com/coreutils/coreutils/blob/2ba9e70e52ca41cd46e5177bdc9ee5cae51a0cb5/src/ls.c#L621-L900, pořád jsem schopný jí porovnat, pořád jsem schopný aplikovat některé původní patche. Velice rychle jsem schopný očima porovnat zpracování těch argumentů.
Kdyby to celé bylo abstrahované do jednotlivých funkci, byly použity generátory na zpracování argumentů, už bych zpětně to nemusel dát vůbec dohromady, porovnat či migrovat, bylo by to roztrhané po mnoha místech.
Tohle je třeba věc, kterou Clean code vůbec neřeší.
V tomhle byl už před několika dekádami napřed Smalltalk. Verzování nedělá na úrovni textových souborů, ale na úrovni sémantických jednotek - "přidána třída", "přejmenována metoda", "upraveno tělo metody" atp. I když se to tak člověku uvyklému práci se zdrojáky v souborech (což jsem už dlouho i já) nemusí zdát, verzování na úrovni stavebních jednotek jazyka bylo mnohem šikovnější, a přechod k "současným" jazykům byl v tomto ohledu trochu trapný.
Source code in files? How quaint (Kent Beck)
smalltalk, itegrované IDE, testovací prostředí, verzování, object inspection, ukápla slzička. Ten, kdo tvrdí, že java je object oriented jazyk nikdy neviděl smalltalk :). Dneska tu mezeru plní asi Pharo.
Pharo mám v TODO listu už dlouho, rád bych v tom mohl zkusit něco udělat. Poté co se smalltalk postupně přestal masivně používat, přešel jsem na mainstreamovější jazyky, jako třeba erlang.
Holt postupně vyhráli jazyky, které zápisem připomínají více náž jazyk a náš způsob myšlení a ty obskurní hlavolamy nikdo nechce.