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

Въведете избор на стил: направете кода да се чете като родния език на програмистите, които го поддържат. В тази статия ще се насочим към английския, защото много хора го говорят и това е моят роден език. 😄

ОТКАЗ ОТ ОТГОВОРНОСТ: Всеки избор на стил, направен при писане на код, идва с компромиси. Четивността има значение, но зависи от вашите предпочитания и случаи на употреба. Използвайте дискретността си и решете дали този стил е подходящ за вас и вашия екип.

Какво означава тук „четете като английски“?

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

„Запишете този файл в S3.“
Тук имаме глагол от write, субект от this file и допълнение от S3. Ако напишем това като код, то ще се чете като write(thisFile).toS3(). Без странност, без странен ред; това се чете като английски!

„Пишете на S3, този файл.“
Звучи като Йода, това е така. Не е често срещан английски, прави ли това. В кода това излиза като writeToS3(thisFile). Някои естествени езици съставят изявления по този начин, но това не прави правилния английски. Този модел обаче е изключително често срещан в кода.

Защо толкова напрегнато?

Вероятно вече знаете какво е време, но ако не знаете, Merriam Webster определя „време“ като: форма на глагол, използван за показване на минало, настояще или бъдеще време на действието или състоянието, което обозначава.

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

Те шредират документите.
Те шредират документите.
Те ще шредират документите.

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

Трябва да пазите тайните.
Пазете тайните.
Ще пазите ли тайните?

Няма да навлизам в нюансите на разликите тук, главно защото изобщо не съм специалист по лингвистика и нямам време да го изследвам! 😅 Искам обаче да отбележа, че най-подобната на команда форма е най-приблизителната за това как се формират „изявленията“ в кода. Това има значение най-вече, защото всъщност не използваме пасивната форма на израз в програмирането.

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

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

import * as fs from 'fs';

const PersistenceUtils = {
  outputJson: (obj) => {
    const json = JSON.stringify(obj, null, 2);
    return {
      toFile: (filename) => {
        fs.writeFileSync(`output/${filename}.json`, json, 'utf8'); 
      },
    }
  },
  outputJsonToFile: (obj, filename) => {
    const json = JSON.stringify(obj, null, 2);
    fs.writeFileSync(`output/${filename}.json`, json, 'utf8'); 
  },
  jsonToFile: (obj) => {
    const json = JSON.stringify(obj, null, 2);
    return (filename) => {
      fs.writeFileSync(`output/${filename}.json`, json, 'utf8'); 
    }
  }
}

export default PersistenceUtils

В този файл/модул експортирам един обект, наречен PersistenceUtils, който мога да импортирам на други места. Той съдържа няколко помощни функции за извеждане на json във файл. Това произтича от намерението да се намали малко дублирането.

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

Нека разгледаме как изглежда извикването на тези помощни програми.

PersistenceUtils.outputJson(myObj).toFile('myFile')
PersistenceUtils.outputJsonToFile(myObj, 'myFile')
PersistenceUtils.jsonToFile(myObj)('myFile')

Първата опция използва някаква специална структура, за да направи възможен такъв специфичен API. outputJson връща обект, към който можем да добавим свойства. В този случай той съдържа едно свойство, наречено toFile. Тук стойността на това свойство е функция, която ни позволява да го извикаме. Това е, което ни позволява да получим усещането за естествен език „чете се като английски“! 😎

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

Нека помислим за някои от уникалните компромиси, които това ни дава...

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

Ние отделяме параметрите един от друг. Подходът с къри също прави това, но по-специално му липсват предимствата на наименуването, тъй като вътрешната функция е анонимна стрелкова функция (известна още като ламбда). Този подход по същество е по-изящна операция с къри, която ни позволява да украсим функционалността, която опаковаме!

Чрез разделянето на какво от как увеличихме гъвкавостта чрез разширяемост. Представете си, че искаме да изпратим резултата json към файл в кофа Simple Storage Service (S3) в Amazon Web Services (AWS) вместо към локален файл. Всичко, което трябва да направим, е да дефинираме друго свойство вътре във върнатия обект, може би като toS3. Няма нужда да пишете нова функция, която дублира обвързването на входния обект myObj; можем да останем фокусирани върху това, което добавяме. Освен това не променяме съществуващата функционалност, така че не нарушаваме никакви тестове или увеличаваме потенциала за грешки.

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

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

Ето пример за това как можем да използваме този модел за писане на обвивки, които са фокусирани само върху четливостта.

const writeToFile = (something) => { /* some S3 stuff */ }
const writeToS3 = (something) => { /* some file stuff */ }

const write = (something) => ({
  toS3: concreteFnWritingToS3,
  toFile: concreteFnWritingToFile,
})

const myObj = {
  1: 2,
  a: 'b',
  foo: 'bar',
}

// same thing, different style
writeToFile(myObj)
writeToS3(myObj)

write(myObj).toS3()
write(myObj).toFile()

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

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

Като бонус ще ви оставя още един кодов фрагмент. Този използва подобна стратегия, която също използва функционален стил на програмиране, за да увеличи четливостта на някои кодове. Той също така използва нещо като „трик“, за да започне веригата от трансформации, като обвие началната точка (масив) с друг масив.

const sortWithoutSideEffects = (arr) => [...arr].sort()
const deDuplicate = (arr) => [...new Set(arr)]
const correctFixableIssues = (arr) => { /* use your imagination 🌈 */}
const pruneUnfixableProblems = (arr) => { /* you cant win them all! */ }

const hopesAndDreams = [] // <-- insert actual things here

const dontGiveUpOnYour = (arr) => {
  const sorted = sortWithoutSideEffects(hopesAndDreams)
  const deDuplicated = deDuplicate(sorted)
  const corrected = correctFixableIssues(deDuplicated)

  return pruneUnfixableProblems(corrected)
}

const achieveYour = (arr) => {
  return [hopesAndDreams]
    .map(sortWithoutSideEffects)
    .map(deDuplicate)
    .map(correctFixableIssues)
    .map(pruneUnfixableProblems)
    .flatMap(e => e)
}

dontGiveUpOnYour(hopesAndDreams)
achieveYour(hopesAndDreams)



// alternatively...
const nailedIt = [
  sortWithoutSideEffects,
  deDuplicate,
  correctFixableIssues,
  pruneUnfixableProblems,
].reduce((acc, curr) => curr(acc), hopesAndDreams)

Помислете за множеството изявления срещу единичната функционална верига.

В първото има много дублиране! Имената на променливите също не са техническиправилни, тъй като не отчитат предишните действия, предприети върху колекцията. напр. „коригирано“, когато всъщност може да бъде „sortedDeDuplicatedCorrected“ 🤪

Кое вие предпочитате?