Кодът създава интерфейси. Но самият код също е интерфейс.

За нещо толкова важно четимостта на кода е шокиращо зле дефинирана. Често го представяме като набор от правила: Използвайте изразителни имена на променливи. Когато една функция стане голяма, разделете я на по-малки функции. Използвайте стандартни шаблони за проектиране.

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

Можем да се опитаме да разрешим този проблем, като натрупаме повече правила: Ако имената на променливите стават изключително дълги, преработете основната логика. Когато един клас натрупа тонове помощни методи, може би трябваше да са два. Не използвайте дизайнерски модели в контексти, в които не пасват.

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

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

За какво е четивността?

На практика четливостта обикновено се свежда до „обичам да го чета“. Това не е страхотна евристика. Освен че е субективно, то се заплита в миналия ни опит с четенето.

Нечетливият код се чете като роман, който се опитва да се представи за код. Покрити с дълги разказни коментари. Файлове, пълни с текст, за четене в последователен ред. Умност заради самия ум. Липса на повторна употреба на думи. Кодът се опитва да бъде четим, но е насочен към грешния тип читатели.

Има разлика между четимост и четимост на кода.

Кодът създава интерфейси. Но самият код също е интерфейс.

Четим ли е кодът, когато изглежда добре? Да изглеждаш красиво е хубав страничен ефект от четливостта, но не е толкова полезен. Може би в маржовете, това помага за задържането на служителите. Но също така прави и добрият план за стоматология. Освен това всеки има различно мнение за това какво „изглежда красиво“. Съвсем скоро тази дефиниция за четливост се превръща във водовъртеж от табулатори, интервали, скоби, камилска обвивка и други свещени войни за форматиране. Никой няма да припадне, като види неправилно подравнени аргументи, въпреки вниманието, което привличат по време на преглед на кода.

Четим ли е кодът, когато генерира по-малко грешки? Да имаш „по-малко грешки“ е добре, но какъв е механизмът тук? Топлите размити чувства, които някой изпитва, когато види четим код? Четенето не може да извика грешки, без значение колко мощно читателят се мръщи на кода.

Четим ли е кодът, когато е по-лесен за редактиране? Лесното редактиране звучи като най-добрата причина. Изискванията се променят, добавят се функции, появяват се грешки и в крайна сметка някой ще трябва да редактира нашия код. За да го редактират, без да създават проблеми, те трябва да разберат какво редактират и как техните редакции ще променят поведението. Това ни дава нова евристика: Четимият код трябва да бъде лесен за безопасно редактиране.

Какво прави кода по-лесен за редактиране?

В този момент може да почувстваме принуда отново да нарушим правилата. „Кодът се редактира по-лесно, когато имената на променливите са изразителни.“ Добър опит, но всичко, което направихме, е да преименуваме „четимост“ на „лекота на редактиране“. Тук търсим нови прозрения, а не същото запаметяване правило по правило във фалшив мустак и перука.

Нека започнем, като оставим настрана факта, че говорим за код. Програмирането съществува от няколко десетилетия, точка от времевата линия на човешката история. Ако се ограничим до тази точка, ние черпим идеите си от плитък кладенец.

Вместо това, нека разгледаме четливостта през призмата на дизайна на интерфейса. Животът ни е изпълнен с интерфейси, цифрови и други. Играчката има функции, които я карат да се търкаля или скърца. Вратата има интерфейс, който й позволява да се отваря, затваря и заключва. Книгата подрежда данните в страници, което позволява по-бърз произволен достъп от превъртане. Официалното обучение по дизайн ни казва още повече за тези интерфейси; попитайте вашия дизайнерски екип за повече информация. В противен случай всички сме използвали добри интерфейси, дори и да не знаем винаги какво ги прави добри.

Кодът създава интерфейси. Но самият код, заедно с неговата IDE, също е интерфейс. Този потребителски интерфейс е насочен към много малка група потребители: нашите съотборници. До края на тази публикация ще ги наричаме „потребители“, за да останем в пространството на UI дизайна.

Имайки това предвид, разгледайте някои примерни потребителски потоци:

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

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

Можем да предположим, че нашите потребители няма да могат да стигнат до правилния код. Това важи и за хоби проекти; достатъчно лесно е да забравите местоположението на функция, дори ако аз съм този, който първоначално я е написал. Така че нашият код трябва да може да се търси.

Ако поддържаме търсене, ще имаме нужда от малко SEO. Тук влизат експресивни имена на променливи. Ако потребителят не може да намери функция, като се придвижи нагоре в стека за извикване от известна точка, той може да започне да въвежда ключови думи в търсенето. Сега не всяко име трябва да има всяка ключова дума. Когато нашите потребители търсят код, те трябва да намерят само една входна точка и могат да работят навън оттам. Трябва да ги приближим до мястото, където искат да бъдат. Включете твърде много ключови думи и те ще бъдат разочаровани от шумни резултати от търсенето.

Ако потребителят успее веднага да се убеди, че „това ниво на логика е правилно“, той може да забрави всички предишни слоеве на абстракция, освобождавайки умствено пространство за следващите слоеве.

Потребителите могат също да търсят чрез автоматично довършване. Те имат обща представа каква функция трябва да извикат или какъв случай на enum искат да използват, така че ще започнат да въвеждат и ще изберат автоматичното довършване, което има смисъл. Ако дадена функция е предназначена да се използва само в конкретни случаи или има предупреждения, които изискват внимателно четене, можем да сигнализираме това с по-дълго име. Когато потребителят чете списъка за автоматично довършване, той обикновено избягва сложната опция, освен ако не знае какво прави.

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

И те ще искат да намерят тази информация с кратка скорост. В повечето случаи компилирането отнема време и изпълнението може да изисква ръчно посещение на много, много различни крайни случаи. Когато е възможно, нашите потребители биха предпочели да прочетат поведението на кода, вместо да добавят точки на прекъсване и да изпълняват кода.

За да пропуснат бягането, те трябва да отговарят на две условия:

  1. Те разбират какво се опитва да направи кодът.
  2. Те са уверени, че прави това, което твърди.

Абстракцията помага да се удовлетвори първото условие. Потребителите трябва да премахват слоевете абстракция, докато достигнат желаното ниво на детайлност. Мислете по линиите на йерархичен потребителски интерфейс. Позволяваме на потребителите първо да правят големи навигации, а след това по-прецизни, когато се доближат до логиката, която искат да прочетат в детайли.

Последователното четене на файл или метод отнема линейно време. Веднага след като потребителят може да щракне нагоре или надолу през стекове за повиквания, той преминава към търсене в дърво. Като се има предвид добре балансирана йерархия, това отнема само логаритмично време. Списъците имат своето място в потребителските интерфейси, но обмислете внимателно дали един контекст трябва да съдържа повече от няколко извиквания на метод.

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

Когато потребителят е в този режим, отделните токени започват да имат по-голямо значение. element.visible = true/false bool флаг е лесен за анализиране изолирано, но изисква мислено комбиниране на два различни токена. Ако вместо това флагът е element.visibility = .visible/.hidden, контекстите, включващи флага, могат да бъдат прегледани, без да се налага да четете името на променливата, за да разберете, че става дума за видимост.¹ Виждали сме същия дизайн подобрения в потребителските интерфейси. През последните няколко десетилетия бутоните за потвърждение се развиха от OK/Отказ до по-описателни опции като Запазване/Изхвърляне или Изпращане/Продължаване на редактирането. Потребителят може да разбере какво се случва, като разгледа самите опции, вместо да се налага да чете целия контекст.

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

На теория безопасността идва от разбирането. След като нашият потребител разбере текущото поведение на кода, той трябва да може да го редактира безопасно. На практика инженерите са хора. Нашите мозъци използват същите преки пътища като мозъка на всеки друг. Колкото по-малко трябва да разбираме, толкова по-безопасни ще бъдат действията ни.

Четимият код трябва да прехвърли по-голямата част от проверката за грешки на машина. Утвържденията за отстраняване на грешки са един от начините за това, въпреки че изискват изграждане и изпълнение. По-лошо, те може да не уловят крайни случаи, ако потребителят забрави за този път. Модулните тестове могат да бъдат по-добри при упражняването на често забравени крайни случаи, но след като потребителят направи промени, те също изискват време за изпълнение.

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

За да получим най-бързото време за изпълнение, ние използваме грешки на компилатора. Те рядко изискват пълно изграждане и дори може да се появят в реално време. Как да се възползваме от тях? Като цяло искаме да търсим ситуации, в които компилаторът става много строг. Например, повечето компилатори не се интересуват дали изразът „if“ е изчерпателен, но ще проверят внимателно операторите „switch“ за липсващи случаи. Ако потребител се опитва да добави или редактира случай, той е по-безопасен, ако всички предишни условни условия са изчерпателни превключватели. В момента, в който сменят случаите, компилаторът ще маркира всички условни изрази, които трябва да преразгледат.

Друг често срещан проблем с четливостта е използването на примитиви в условни изрази. Особено когато приложение анализира JSON, изкушаващо е да напишете много оператори if около низ или цяло число. Това не само отваря вратата за правописни грешки, но и затруднява потребителите да знаят кои стойности са възможни. Има голяма разлика между проверката на крайните случаи, когато всеки низ е възможен, и проверката на крайните случаи, когато са възможни два-три отделни случая. Дори ако примитивите са уловени в константи, потребителят е на един предстоящ краен срок от присвояването на произволна стойност. Ако използваме потребителски обекти или enums, компилаторът блокира невалидни аргументи и предоставя ясен списък с валидни.

По същия начин предпочитайте единичен enum пред множество bool флагове, ако някои комбинации от флагове са невалидни. Например, представете си песен, която може да бъде буферирана, напълно заредена или възпроизвеждана. Ако представим това като два bool флага, (loaded, playing), компилаторът разрешава невалидния вход (loaded: false, playing: true). Въпреки това, ако използваме enum, .buffering/.loaded/.playing, невалидното състояние дори не е възможно. „Деактивиране на невалидни комбинации от настройки“ ще бъде основна функция в потребителски интерфейс. Но когато пишем кода в приложението, често забравяме да си осигурим същата защита.

По време на този потребителски поток стигнахме до същите правила, които имахме в началото. Но сега имаме процес за тяхното генериране и персонализиране. Питаме се:

  • Този код ще бъде ли лесен за намиране от потребителите? Ще затрупа ли резултатите от търсенето за несвързани функции?
  • Веднъж намерен, може ли потребителят бързо да потвърди текущото поведение на кода?
  • Могат ли потребителите да разчитат на машинно валидиране, за да редактират или използват повторно този код безопасно?

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

Бележка под линия

  1. Булевите може да изглеждат по-многократно използвани, но многократната употреба предполага взаимозаменяемост. Представете си два флага, tappable и cached. Те представляват понятия в напълно различни оси. Но ако и двата флага са булеви, можем небрежно да ги сменяме, промъквайки нетривиални изрази („кеширането е свързано с възможността за докосване“) в малък ред код. С enums сме принудени да създадем изрична логика за „преобразуване на единица“, която може да се тества, когато искаме да формираме този вид връзка.