Любимият ми пример за четимост

Освен на Python, в курса ни във ФМИ се опитваме да учим студентите на „добър код“ (каквото и да значи това). Всеки семестър правим поне една лекция на тема „добри практики“ (спорно с каква успеваемост, понеже такива неща се учат с реални проекти, а не лекции). Искам да споделя любимия си пример.

Иде реч за това как може да направим кода по-ясен (revealing intent). Последователно показвам три различни парчета код, като след всяко питам какво прави. Първото е следното:

    movl    $0, 4(%rbp)
loop:
    cmpl    $12, 4(%rbp)
    jge     done
    leaq    _array(%rip), %rax
    movslq  4(%rbp), %rcx
    movq    (%rax,%rcx,8), %rdi
    callq   _puts
    movl    %eax, 8(%rbp)
    movl    4(%rbp), %eax
    addl    $1, %eax
    movl    %eax, 4(%rbp)
    jmp     loop
done:

Обикновено отговорът е:

Ъ? Ами…

Ако човек няма идея от assembly, декриптирането на този код е загубена кауза. Иначе – изглежда като цикъл и имената _array и _puts са подсказващи. Вероятно човек може да се сети, че това е компилираната версия на втория пример:

for (int i = 0; i < 12; i++) {
    puts(array[i]);
}

Тук всички дават следния отговор:

Извежда масив с 12 елемента

Съвсем точно.

Интересното е, че няма голяма „концептуална“ разлика между двете. Макар, втория вариант да е по-кратък (и „структуриран“), той казва същото – „извеждаме 12 низа“. Въобще не загатва какви или защо.

Третият пример се получава със съвсем лека промяна:

for (int i = 0; i < 12; i++) {
    puts(monthNames[i]);
}

Новият отговор е:

Извежда имената на месеците

Забележете, че само едно нещо е променено (arraymonthNames), но отговора вече е на по-високо ниво на абстракция. „Масив с 12 елемента“ звучи като имплементационен детайл, докато „имената на месеците“ е общо-разбираема, непрограмистска концепция.

Това е фин детайл, но разликата е съществена. Изводи колко искаш, но на мен любимият ми е следния: опитвайте да кръщавате нещата на domain-а, вместо на имплементацията.

4 thoughts on “Любимият ми пример за четимост

  1. Aquarius първият пример не е коректен по принцип, 95% от програмистите които познавам си нямат никакъв хабер от assembly също така 99% от програмистите на под 30 годишна възраст. На въпросът защо, нормалният отговор от тях е кому е нужно това имаме си езици от високо ниво. Преполагам и аз щях да съм на същото дередже ако едно време на Имко-1/Правец-82 изборът не беше ограничен между BASIC и Асемблер.

  2. Здравей, според мен примерът започва подвеждащо и абстрактността може да се гледа и от коренно различна позиция.

    Самото даване на асемблер на хора, които смятат, че това е нещо сложно е подвеждащо – асемблерът не е сложен.

    Това че се отнася точно за месеци го няма в първоначалния код и според мен не е „грешка“ или недостатък на кода – цялостния компилиран код или сорс код на асемблер щеше да съдържа дефиниция на масива (съответно името му и съдържанието) или ако беше от двоичен източник, щеше да включва низова символна таблица или метаданни и адреса от където започва четенето – тогава съдържанието щеше да показва, че в точно този пример това са месеци, дори и да няма идентификатор „месеци“. Същото може да се постигне и с коментар от една дума, така че пак щеше да е ясно приложението на кода:

    leaq _array(%rip), %rax ; months

    Примерът даден по този начин е като от пример за разнищване на код в обратна посока, при който са повредени символните таблици или се зареждат от някъде (и не могат да бъдат възстановени).

    Също така такъв кратък код на асемблер или каквото и да било извън контекст обикновено не съществува, освен като изрезка/макрос/функция с цел именно да бъде обобщен и да не е обвързан с конкретно приложение и конкретни стойности (и фиксиран брой итерации): ако се промени адреса на масива и броя, тогава ще извежда други неща, например 12-те апостоли или 12-те часа или 12-те души от „мръсната дузина“ или „числата от едно до дванайсет на китайски“ – но само ако кодът е обобщен, което в такъв случай щеше да е предимство, защото с по-малко промени ще може да се разширява областта на приложение.

    Също така, този код вероятно в асемблер щеше да е част от функция със съответно име, или която като параметър получава какво да печата, print(„months“) и пр. и този цикъл вътре можеше и да не е _array или monthNames, а по-скоро: „puts(a[i])“, от заглавието щеше да е ясно и дългото име можеше да е излишно.

    Т.е. ако се разшири обхвата на вниманието, така че да се вижда контекста на асемблерния код, пак щеше някъде да има някой термин от областта на приложение, без да е нужен код на Си.

    Бих поспорил за обясненията за абстрактността.

    Колкото е по-абстрактен модела, толкова този, който прилага кода трябва да реши за какво да го ползва и да допълни детайлите. А когато се закачат фиксирани имена и фиксирани стойност, не се вдига абстрактността (на модела), а обратното – сваля се, нещата се конкретизират и вкарват в по-определени по-тесни рамки и им се намалява областта на приложение.

    Това е добре в определени случаи и за начинаещи, но е зле в други и намалява модулността и възможността за поворно използване на кода.

  3. Както казва Тодор в горния коментар, твърдението, че C кода сам по себе си е с по-висока абстракция е по-скоро некоректно. Разликата между асемблерския фрагмент и съответстващия C код е най-вече в гранулярността. Асемблерския код изразява същото намерение, на същото ниво на абстракция, но с доста по-финна гранулярност, така че да е разбираемо за хардуера. Ако в C кода прекрачиш края на масива те очаква „undefined behavior“ (а не OutOfBoundsException), и именно това е едно от доказателствата, че си на същото ниво на абстракция.

    За да си на по-високо ниво на абстракция, трябва да „стъпиш“ на някакъв модел който концептуално е под твоята приложна логика за конкретната ситуация. Подходящ пример за ниско и високо ниво на абстракция са например четене от диска блок по блок vs четене на файл.

Вашият коментар

Вашият имейл адрес няма да бъде публикуван.