Това е в лекцията ми от varnaconf – “Какво ми се иска да знаеха програмистите (а явно не го знаят)”.
(имаше няколко различни заглавия в тоя смисъл, не успях да подбера най-грубото)
Може би най-важното е да знаят, че имам лопата и си я държа под ръка…
Иначе, малко по-сериозно, да почна от там защо изобщо реших да водя лекцията. Пряко свързана е с работата ми и реално представлява мрънкане/оплакване от някаква част от проблемите, с които съм се срещал и не съм очаквал, че ще се срещна.
Моята работа по принцип е да карам неща, писани от други хора да работят. Това включва много debug-ване (което ми е едно от любимите занимания), много четене на код и малко писане. Държа да си кажа, аз не мога да пиша кой-знае колко добре, липсва ми времето и желанието за толкова съсредоточаване (ttee ми отне няколко дни, а не би трябвало).
Четенето на код е основно да разбера защо нещо не работи (или понякога как изобщо работи), съответно с мисъл за бъдещето обяснявам на хората къде е проблема, как може да се оправи, как аз съм го оправил, така че в бъдеще да не видя същото нещо. За съжаление ако историята ни учи на нещо, то е, че хората не се учат от историята, та моите опити са доста неуспешни. Приемете това като едно просто изливане на мъката.
Спрях се на два типа проблеми – теоретични и от неинформираност, понеже бях ограничен в 30 минути, но ще се опитам да засегна нещата в по-голяма дълбочина тук.
Ще започна с първия теоретичен проблем – copy-paste.
Copy-paste е метод за разпространение на грешката. Не мога да преброя колко пъти съм видял код, копиран няколко пъти и във всичките места със същата грешка. Човек би си помисли, че това е понеже няма как да се избегне (наистина има такива случаи), но най-често изглежда или като мързел от страна на програмиста, като липса на абстрактно мислене или като непознаване на езика и средствата на разработка. Голяма част от повторения код изглежда като писан от хора, които не са наясно че има неща като for цикли, функции, макроси и т.н..
Доколкото знам, започват да се появяват инструменти, които поне могат да откриват такива неща, но не съм забелязал сериозната им употреба. Тези неща много лесно се хващат с четене на кода (deja vu, “това някъде съм го виждал) и е едно от нещата, които code review може лесно да хване.
Умното писане ми е друг проблем. Много често се случва след-начинаещ програмист да открие нова възможност на езика, нов lib, нов метод на писане и да започне да го прилага навсякъде, където види, просто защото му се вижда много готин. Така се прилага безсмислено сложно средство за решаване на прости проблеми, което прави debug-ването много трудно, защото както е казал Браян Кърниган, “дебъгването е два пъти по-сложно от програмирането, така че ако програмирате с пълния си капацитет, не сте достатъчно умен да да го дебъгвате после”.
Някои езици много улесняват подобни неща, например Perl и C++. Мнението на Al Viro за C++ и защо не се ползва за kernel-а много добре показва проблема – накратко, езикът има твърде много възможности, всеки програмист свиква с някакво под-множество от тях, и когато в един проект се съберат да пишат няколко човека, се получава страшна смесица от стилове.
(на лекцията бях казал грешно, че идва от Линус, но мисля, че говорех за това, доста отдавна съм го чел)
По принцип често ме викат, когато трябва да се debug-не нещо такова супер сложно и омазано и най-честото нещо, което казвам е, че трябва да се пренапише.
(За пример за умен код давам примерните реализации на fizzbuzz, ако някой някога ми напише нещо като последната в production код, ще трябва да има преди нея поне толкова голям коментар, който обяснява защо е написано така и как точно работи)
Проблемът с валидацията може би ще иска отделна публикация, лекция, учебник, курс или религия по темата. Факт е, че след толкова години security и всякакви подобни проблеми никой не проверява получените данни.
(след малко дискусии с Кънев и още няколко човека искам да обясня, че например трансформирането на данните до вид, който е валиден също е вид валидация, т.е. правилното escape-ване например)
Докато водих лекции на едни ученици около летния лагер на БАН преди година-две във Варна, присъствах на тяхно предаване на проект, който представляваше симулация на банкомат – в общи линии му се подаваше сума и програмата по някакъв начин казваше в какви банкноти и по колко ще я върне. Имаше всякакви красиви реализации и т.н., но повечето изгърмяваха по неприятен начин (или зацикляха, или програмата умираше) на всичко, което не беше съвсем валидно – отрицателни числа, дроби, такива, за които нямаха типове банкноти – само защото нямаха няколко прости проверки в началото и вярваха, че входът ще е верен.
(това с вярването сигурно работи в религиите, но не и в програмирането)
Елементарното нещо – да се направи проверка още на входа на данните, се пропуска от всички. Много често ако има валидация, е от типа “shotgun” (т.е. все едно сте лепнали кода на стената и от далече сте стреляли по него с пушка със сачми, и където е паднала сачма там сте сложили проверка за нещо) и повече пречи, отколкото помага.
Друга такава елементарна грешка е идеята “аз пиша клиента и сървъра за нещо, значи данните ще са ми верни и няма защо да ги проверявам”, което е така до момента, в който някой друг се добере до този интерфейс и започне да праща там. Резултатите от това варират между викове “кой го е писал това” и вопли “как са ни хакнали и са ни потрили всичко?”…
Вариация на предното е “само аз пиша в базата, знам какво е и няма нужда да го проверявам” – имаше няколко случая на cross-site scripting през такива проблеми.
И нещо, за което малко хора се сещат изобщо, а на малкото останали хора, които държат на performance (като например game developer-ите) им е противопоказно е defensive programming-а – да се прави допълнителна проверка на още няколко места, понеже в крайна сметка всички допускаме грешки, нормално явление е да ги допускаме и е добре да ги хващаме. Пряко следствие от това е, че assert-ите не се пускат само в debug код, а и в production.
Като допълнение към валидацията, което не можах да разпъна добре на лекцията е, че няма замисляне колко сложни да се правят протоколите, така че да са лесни за валидация. Писах за лекциите от 28c3 и по-точно за Packet-in-packet атаката и за the science of insecurity, така че няма да повтарям (много), но е важно да правим всичко така, че да ни е нужен максимално прост апарат за обработката и валидацията му – колкото повече от нещата ни могат да се разберат и обработят с краен автомат например, толкова по-добре. В противен случай можем да се озовем в ситуацията на антивирусния софтуер, който спрямо математическата теория не може да бъде направен да работи напълно правилно.
(Проблемът там е като следствие от теоремата на Гьодел, която казва, че вътре в една система не можем да проверим дали всяко твърдение в нея е вярно или невярно, т.е. няма как със средствата на машина на Тюринг да валидираме друга машина на Тюринг)
И за пример за невъзможна за валидиране и използва сериозно система ще дам комбинацията от HTML5 и CSS3 (без JavaScript), която се оказва, че е Turing-complete.
След чистата теория нека да хванем няколко по-преки проблема.
“I don’t believe in miracles, I rely on them” изглежда да е стандартна програмистка философия. Трудно се схваща, че почти всичко може да върне грешка и съответно трябва да се направи нещо, ако това се случи. Като за начало, имам две парчета код за пример. Първото е “наивната версия”:
#define BUFSZ 8192 int fd0, fd1; size_t len; char buff[BUFSZ]; fd0 = open(file1, O_RDONLY); fd1 = open(file2, O_RDWR); while ( (len = read(fd0, buff, BUFSZ) !=0 ) ) { write(fd1, buff, len); } close(fd0); close(fd1);
Второто е ремонтираната (DIE трябва да печата име на файл, име на функция, ред, подадения string и strerror(errno)):
#define BUFSZ 8192 #define DIE()... int fd0, fd1; size_t len; char buff[BUFSZ]; if ( ( fd0 = open(file1, O_RDONLY) ) <0 ) DIE("open fd0"); if ( ( fd1 = open(file1, O_RDONLY) ) <0 ) DIE("open fd1"); while ( (len = read(fd0, buff, BUFSZ) > 0 ) ) { if ( write(fd1, buff, len) < len) DIE("writing"); } if (len<0) DIE("reading"); close(fd0); close(fd1);
(Оставил съм втората версия както ми е в презентацията, с грешките от един copy-paste. И аз съм идиот, това го видях на самата лекция, когато ми го посочиха)
Разликата е ясна, и примери от първия тип може да се срещат къде ли не. Най-често съм виждал такива неща при php програмистите, което може би идва от това, че понеже всяка маймуна може да пише на php, твърде много маймуни са започнали да го правят.
Втората версия не прави някаква магия и да решава проблема – тя просто ни дава едно важно свойство, наречено “failfast”, т.е. програмата да прекрати изпълнението си колкото се може по-скоро след като стане ясно, че не може да го завърши правилно. С код с подобно поведение и няколко други прекрасни неща, като идемпотентност на изпълнението (много дразнеща дума, значи в общо линии че ако кодът се изпълни 1 или N пъти, резултатът в крайното състояние ще е все същия, т.е. можем да повтаряме колкото си искаме) и ACID свойства можем да правим истински работещи системи, а не такива, които се чупят, като някой ги погледне накриво.
Fallacies of distributed computing са друго нещо, което много ми се искаше да е известно на повечето програмисти. Много често хората пропускат колко гнусно нещо като поведение е мрежата. Ще се спра накратко на няколко от проблемите:
Мрежата не е с нулева латентност – усеща се когато например се изисква два компонента да работят синхронно и да си разменят постоянно съобщения, за да си синхронизират състоянието. Тогава дори бързата локална мрежа ще вкара сериозни забавяния, а в момента, в който минем на нещо на по-дълги разстояния, повечето такива неща просто спират да работят.
Това е и едно от нещата, които не можем да очакваме по-добрия хардуер и физиката да поправят, понеже поне до момента няма нещо, което да можем да използваме за предаване на данни и да е по-бързо от скоростта на светлината.
Пропускателната способност не е безкрайна – понякога хората не се усещат, че предаването на указател към огромни данни през мрежата не става точно така.
Мрежата не е reliable – нормално е за мрежата да губи пакети, да изчезва, да има jitter, а ако е internet е нормално това да е още по-зле. Съответно не може да се пише какъвто и да е софтуер, който я използва без да се имат в предвид тези failure modes и да се взимат мерки за тях.
Мрежата не е сигурна – може да се подслушва, някой да се представя за някой друг и т.н. и т.н. и т.н., неща, които ги чуваме даже по новините и все пак никой не се сеща да ползва криптография.
Накратко, мрежата ви мрази.
Накратко два доста конкретни проблема от ползването на version control-а – още не се преподава сериозно, хората започват да го учат в движение в първата си работа и това води до всякакви проблеми, например до ползване на CVS или до това ако двама човека работят заедно, да седят един до друг и да си разменят файлове по skype.
Първият проблем от неяснотата как се използват VCS (version control systems) е че когато кажеш на някой “branch-ни това, за да работим спокойно по trunk-а” изобщо не те разбират, а като обясниш “направи копие еди-къде-си”, хората просто взимат файловете и ги add-ват наново там, което води до загуба на history-то. След като им се посочи грешката, те продължават да не схващат защо това е такъв проблем, понеже за тях VCS-а е само backup и commit messages са някакъв бълвоч, който никой не гледа, а когато потрябва да се гледат, вече е късно.
(Много е забавно да се анализират всички commit messages на някоя фирма и да се види кои са най-често срещаните думи. Моята любима (беше на 10то място в една фирма) е “blahovina”)
Вторият проблем е как като се прави някакъв сайт, редовно се слага config файла в repository-то. Понеже този файл е различен за production-а, staging-а и за машините на developer-ите, редовно се случва някой да го commit-не от грешната машина и да го update-не в production-а, чупейки всичко. Двете решения, за които аз знам са или да се прави template на този файл и само той да стои в repo-то, а другото е да се detect-ва средата и да се load-ват подходящите настройки (както прави rails, доколкото схванах).
И един bonus за четящите тук – нещо, което не можах да спомена в лекцията, но обсъждахме доста подробно след това, външните dependencies на един проект, кой носи отговорност за тях и как се update-ват. Ще говоря основно за неща, които се deploy-ват по сървъри, но има доста паралели и с end-user-ския софтуер.
По принцип имаме два варианта. Единият е да използваме външните неща от дистрибуцията/операционната система, оставяйки на тях отговорността (и подбирайки ги правилно, например да ползваме debian stable или centos или rhel), другия е ние да си ги вкараме в проекта и да си носим отговорността за тях.
Много проекти се спират на втория вариант, след което обаче забравят какво са вкарали, или пък дават зависимости през външна пакетна система (например ruby gem-ове) и пак забравят да ги update-ват. Или не забравят да ги update-ват, но не смеят, понеже ще се счупи нещо. Или ще update-нат цялото нещо към най-новата версия заради някакъв супер готин нов feature и ще потрошат цялата система.
За това хората, които не разбират как се прави това, не трябва да го правят – ако човек не участва в оперативната работа (я като админ, я като част от devops екип), по-добре да остави на другите да му кажат версии и външни пакети, за да може да се работи стабилно и без downtime. Като малка реклама на debian stable (и донякъде ubuntu server lts), те правят дистрибуция много подходяща за сървъри, понеже въпреки, че са със стари пакети, те реално за живота на дистрибуцията винаги backport-ват важните patch-ове (основно за security проблеми), така че системата е едновременно и сигурна и не се променя поведението ѝ.
Специално за външните dependencies също мога да направя цяла отделна лекция, в която да си излея мъката.
За всички, преживели изчитането на това – снимка на обстановката, в която спах във Варна :)
(най-много ме зарадваха силно невярващите погледи на някои хора от залата, т.е. има надежда, че има програмисти, които знаят тия неща)