Стабилен софтуер на теория
Jan 21, 2021 02:40 · 1191 words · 6 minute read
Стабилен софтуер е не когато помислим за всичко, което може да се случи на практика - а когато помислим за всичко, което може да се случи и на теория
“На теория практиката е същото като теорията, но на практика не е.” - народна мъдрост
Попаднах на интересна статия за добавянето на поддръжка на Firefox за Apple Silicon (ARM64) и macOS Big Sur (11.0). Интересна ми стана частта, в която описват проблеми, породени от версията на macOS. Досега години наред, откакто Apple преминаха на Intel, macOS версиите бяха 10.x . Това е подтикнало в главите на доста програмисти да се разиграе следната ситуация:
- Унуфри: “Трябва да разбера на коя версия на macOS върви програмата.”
- Също Унуфри: “Мога да парсна числата като стандартна semver версия:
x.y.z
, да ги сравня последователно “лексикографски” и да проверя дали е по-голямо от10.14
.” - Отново Унуфри: “Обаче вече 20 години версиите не са се променяли и стоят на 10. Едва ли ще увеличат major версията. Значи мога да извадя minor числото и да сравня само него:
parseInt(/\d\.(\d)/.match(osVersion)[0], 10) > 13
. Готово, много по-лесно и работи.”
Каква грешка допусна Унуфри? Направи предположение, което на практика е вярно, но на теория не е. На практика между това допускане и реалното увеличение на версията са минали поне 3 години. Изведнъж практиката настига теорията и проверката започва да сравнява с нулата от 11.0
.
Голям проблем ли е това? В случая не - работило е много време, оправили са го лесно. Но подобни неща се натрупват - ако има много пропуски, само заради броя им вероятно често ще се появяват нови и нови проблеми.
Поради същата причина Microsoft пропуснаха версия 9. Заради много други инстанции на Унуфри, казващи си, че това е “достатъчно добро на практика”: const isWin9x = osVersion[0] === '9';
.
Поради подобни причини имаме и Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 OPR/73.0.3856.344
за User Agent идентификатор във всеки модерен браузър.
Това е и причината macOS Safari на ARM64 да докладва Intel Mac OS X 10_15_6
вместо Apple Silicon Mac OS X 11_0_0
, въпреки, че няма нищо от Intel в хардуера.
Примерите помагат за разбиране, но не са документация
Следвайки примерите, Унуфри стига до извода, че 10.x
е форматът, с който трябва да работи.
Разбирайки, че това е версия, следователно може да има стойности, различни от примерите, човек може да стигне до извода, че трябва да го прочете като версия и да се съобрази с всички компоненти.
Както в условните задачи по математика - ако нещо е дадено, значи вероятно трябва да се ползва. Или поне да се игнорира съзнателно и с достатъчно обяснение.
Не е достатъчно да видим какви стойности връща дадено API - трябва да прочетем документацията. Изключително често ми се случва да си спестявам часове дебъгване още преди да съм написал кода - като просто отделя 15 минути да прочета внимателно документацията на библиотеката, която ще използвам. Ако maintainer-ите на библиотеката са отделили цели дни в описанието, значи има смисъл от това, което го пише там.
Например, изчитайки описането на една библиотека, която дори не използвах nextjs-serverless-lambda-handler, ми отвори очите за детайл, който не съм и подозирал, че съществува. Този детайл все още нямаше да съм го разбрал, месец по-късно и щеше да се появи в най-неподходящия момент, изключително неприятен за дебъгване.
На типовата система трябва да се угажда
На всеки, който е писал на TypeScript поне веднъж му се е случвало да използва non-null-assertion оператора (somethingThatCanBeNull!.property
).
В момента, в който пишем това винаги имаме ясна причина в главата си защо в този случай е вярно. На практика. Но типовата система казва, че на теория е възможно и да не е така - и рано или късно ще се окаже права. Предположението, което правим е вярно сега, но не можем да гарантираме, че ще бъде вярно след месец и 200 commit-а по-късно.
Един пример от миналата година. В един React компонент - главен компонент за екран, имаше следното:
function OrderConfirmationScreen() {
// We're only getting to this screen when we check out an order,
// so we are guaranteed to have the order in Redux already.
const order = useSelector(getCheckoutOrder)!;
// ... code that uses order without checking if present ...
}
Изглежда вярно, докато не се появят три допълнителни промени, всяка по себе си запазваща правилото, но трите заедно го чупят:
- В някои ситуации се появява нов екран между този и предходния.
- Ъпдейтва се библиотеката за навигация - новото подразбиращо се поведение е този екран да остане mount-нат по-дълго, отколкото е бил преди, дори да не се вижда.
- Започваме да зачистваме order-а от Redux, за да оправим друг бъг.
Изведнъж теорията настига практиката и в някои ситуации мобилното приложение започва да крашва, показвайки бял екран. Причината е JavaScript еквивалента на null pointer exception в React компонент, който дори не се вижда в този момент. Ако типовата система можеше да говори, би казала надменно “Казах ти, че това ще е проблем, ама ти - не”.
Ако бяхме угодили на типовата система, поведението щеше да се съобразява с тази възможности - например чрез съобщение или предаване на стойността по не-глобален начин, който може да се верифицира автоматично.
Понякога дори if (!something) { throw "This should not happen because ..." }
е по-добро решение от non-null assertion, който после ще каже Cannot read property X of undefined
.
Грешките са част от теорията
Всеки ред код може да предизвика (различни видове) грешки.
Това е очевидно на теория, но на практика го забравяме често. Да не се интересуваме от тези ситуации е програмисткият еквивалент на заравяне на глава в пясъка. Разбира се, трябва да обработваме грешките само на места, където има какво да направим по въпроса. А когато не можем да ги обработим - поне трябва да покажем някак на бъдещите дебъгващи.
Често се случва да видим нещо такова:
function loadSomeOptionalData() {
try {
...
} catch {
return undefined;
}
}
На практика всичко е точно - грешката, че не могат да се заредят данните се игнорира и се връща празна стойност. На теория, обаче, грешката не винаги е тази, която очакваме. Всякакви грешки, дори Cannot read property X of undefined
, ще бъдат хванати, игнорирани и няма да има опционалните данни. Тъй като данните са опционални, такъв проблем може да остане незабелязан много дълго време.
За да се съобразим с теорията трябва да направим това:
function loadSomeOptionalData() {
try {
...
} catch (error) {
if (error instanceOf CouldNotLoadDataError) {
logger.warn("Could not load optional data X", error);
return undefined;
}
throw error;
}
}
Така елиминираме предположението “грешката е тази, която очакваме”. Ще разберем за проблема в момента, в който този код се счупи по неправилен начин.
Постоянно правим assumption-и, трябва да сме болезнено наясно с тях
Най-често бъговете са резултат от грешни предположения за това как работи (или как трябва да работи) нещо.
Колкото по-дисциплинирани сме в това да изброим разбиранията си за нещо и да разграничим предположенията от фактите - толкова по-лесно излизаме от ситуацията, в която си скубем косите гледайки парче код с часове. Извиквайки всеки метод да се замисляме сигурни ли сме в поведението му. И ако не сме - да проверим документацията.
Debugging is like being the detective in a crime movie where you are also the murderer.
- Filipe Fortes https://twitter.com/fortes/status/399339918213652480