Одноклассники проти читабельності: коли зупинити зменшення коду? [зачинено]


14

Контекст

Нещодавно мене зацікавило створення кращого форматованого коду. І в кращому випадку я маю на увазі "дотримання правил, схвалених достатньою кількістю людей, щоб вважати це хорошою практикою" (оскільки, звичайно, ніколи не буде єдиного "найкращого" способу кодування).

Цього дня я здебільшого кодую в Ruby, тому я почав використовувати лінійку (Rubocop), щоб надати мені деяку інформацію про "якість" мого коду (ця "якість" визначається проектом у стилі ruby-style-guide ).

Зверніть увагу , що я буду використовувати «якість» , як в «якості форматування", не стільки про ефективність коди, навіть якщо в деяких випадках ефективність коди на насправді це залежить від того, як написаний коду.

У будь-якому випадку, зробивши все це, я зрозумів (або принаймні згадав) кілька речей:

  • Деякі мови (особливо Python, Ruby тощо) дозволяють робити великі однолінійки коду
  • Дотримуючись деяких вказівок щодо вашого коду, можна зробити його значно скороченим і все ще дуже зрозумілим
  • Однак занадто суворе дотримання цих вказівок може зробити код менш зрозумілим / легким для читання
  • Код може майже повністю дотримуватися деяких вказівок і все ще бути низької якості
  • Читання коду здебільшого суб'єктивне (як, наприклад, "те, що мені здається, може бути абсолютно незрозумілим для іншого розробника")

Це лише спостереження, а не абсолютні правила. Ви також зауважите, що читабельність коду та наступні вказівки можуть здатися непов'язаними на даний момент, але тут ці вказівки - це спосіб зменшити кількість способів переписати один фрагмент коду.

Тепер кілька прикладів, щоб зробити все це більш зрозумілим.

Приклади

Візьмемо простий випадок використання: у нас є додаток з " User" моделлю. Користувач має необов'язкову firstnameі surnameобов'язкову emailадресу.

Я хочу написати метод, який " name" поверне тоді ім'я ( firstname + surname) користувача, якщо принаймні його firstnameабо surnameє, або його emailяк резервне значення, якщо ні.

Я також хочу, щоб цей метод приймав use_emailпараметр " " як булевий, що дозволяє використовувати електронну пошту користувача як резервне значення. Цей use_emailпараметр має бути за замовчуванням (якщо він не переданий) як " true".

Найпростіший спосіб записати це в Ruby:

def name(use_email = true)
 # If firstname and surname are both blank (empty string or undefined)
 # and we can use the email...
 if (firstname.blank? && surname.blank?) && use_email
  # ... then, return the email
  return email
 else
  # ... else, concatenate the firstname and surname...
  name = "#{firstname} #{surname}"
  # ... and return the result striped from leading and trailing spaces
  return name.strip
 end
end

Цей код - найпростіший і легкий для розуміння спосіб зробити це. Навіть для того, хто не «говорить» Рубі.

Тепер спробуємо зробити це коротшим:

def name(use_email = true)
 # 'if' condition is used as a guard clause instead of a conditional block
 return email if (firstname.blank? && surname.blank?) && use_email
 # Use of 'return' makes 'else' useless anyway
 name = "#{firstname} #{surname}"
 return name.strip
end

Це коротше, все ж легко зрозуміти, якщо не простіше (захисний пункт читається більш природно, ніж умовний блок). Застереження про охорону також робить його більш відповідним вказівкам, які я використовую, тому тут безпрограшно. Ми також знижуємо рівень відступу.

Тепер скористаємося магією Ruby, щоб зробити її ще коротшою:

def name(use_email = true)
 return email if (firstname.blank? && surname.blank?) && use_email
 # Ruby can return the last called value, making 'return' useless
 # and we can apply strip directly to our string, no need to store it
 "#{firstname} #{surname}".strip
end

Навіть коротше та досконало дотримуючись вказівок ... але набагато менш зрозуміло, оскільки відсутність заяви про повернення робить це трохи заплутаним для тих, хто не знайомий з цією практикою.

Саме тут ми можемо почати задавати питання: чи дійсно воно того варте? Якщо ми скажемо "ні, зробіть це читабельним і додайте return" "(знаючи, що це не буде поважати вказівки). Або ми повинні сказати "Це добре, це Рубін шлях, вивчи прокляту мову!"?

Якщо ми беремо варіант B, то чому б не зробити його ще коротшим:

def name(use_email = true)
 (email if (firstname.blank? && surname.blank?) && use_email) || "#{firstname} #{surname}".strip
end

Ось він, однолінійний! Звичайно, це коротше ... тут ми скористаємося тим, що Ruby поверне значення або інше залежно від того, яке визначено (оскільки електронна пошта буде визначена за тієї ж умови, що і раніше).

Ми також можемо написати це:

def name(use_email = true)
 (email if [firstname, surname].all?(&:blank?) && use_email) || "#{firstname} #{surname}".strip
end

Це коротко, не так важко читати (я маю на увазі, ми всі бачили, як може виглядати потворний однокласник), хороший Рубі, він відповідає правилам, якими я користуюся ... Але все ж, порівняно з першим способом написання це, набагато менш легко читати та розуміти. Можна також стверджувати, що цей рядок занадто довгий (більше 80 символів).

Питання

Деякі приклади коду можуть показати, що вибір між "повнорозмірним" кодом та багатьма його зменшеними версіями (аж до відомого однолінійного) може бути важким, оскільки, як ми бачимо, однолінійки можуть бути не такими страшними, але все-таки ніщо не переможе "повнорозмірний" код з точки зору читабельності ...

Тож ось справжнє питання: де зупинитися? Коли короткий, досить короткий? Як дізнатися, коли код стає "занадто коротким" та менш читабельним (маючи на увазі, що він досить суб'єктивний)? І навіть більше: як завжди кодувати відповідно і уникати змішування одноклассників з "повнорозмірними" фрагментами коду, коли мені просто подобається?

TL; DR

Основне питання тут: коли потрібно вибрати між "довгим, але чітким, читабельним і зрозумілим фрагментом коду" та "потужним, коротшим, але важче читати / розуміти однолінійку", знаючи, що ці два - це верх і внизу шкали, а не два єдині варіанти: як визначити, де знаходиться межа між "достатньо чітким" та "не таким чітким, як має бути"?

Головне питання - це не класичний "Одноколісні та читальні: який краще?" але "Як знайти баланс між цими двома?"

Редагуйте 1

Коментарі в прикладах коду мають бути «ігноровані», вони є тут, щоб уточнити, що відбувається, але не повинні враховуватися при оцінці читабельності коду.


7
Занадто короткий для відповіді: тримати ітераційно рефакторинг, поки не будете впевнені, що він кращий за попередню ітерацію, тоді зупиніть та скасуйте останню рефакторизацію.
Дом

8
Я вважаю за краще варіант 3 із returnдоданим ключовим словом . Ці сім символів додають моїх очей трохи ясності.
cmaster - відновити моніку

2
Якщо ви відчуваєте себе жахливо, ви можете написати це все як [firstname,surname,!use_email].all?(&:blank?) ? email : "#{firstname} #{surname}".strip… тому що false.blank?повертається правда, а потрійний оператор економить вам кілька символів ... ¯ \ _ (ツ) _ / ¯
DaveMongoose

1
Гаразд, я мушу запитати: яку ясність returnслід додати ключовому слову ?! Він не надає жодної інформації . Це чистий безлад.
Конрад Рудольф

2
Поняття про те, що стислість набуває ясності, не тільки страждає від закону зменшення прибутку, але і повертається, коли його підштовхують до крайності. Якщо ви переписуєте, щоб скоротити функцію коротше, ви витрачаєте свій час, і те саме стосується спроби виправдати практику.
sdenham

Відповіді:


26

Незалежно від того, який код ви пишете, краще читати. Короткий - другий найкращий. І читабельна зазвичай означає досить коротку, щоб ви могли зрозуміти код, добре названі ідентифікатори та дотримуючись загальних ідіом мови, якою написаний код.

Якби це мовна агностіка, я думаю, що це, безумовно, буде грунтуватися на думці, але в межах рубінської мови, я думаю, ми могли б відповісти на це.

По-перше, особливістю та ідіоматичним способом написання Ruby є опущення returnключового слова при поверненні значення, якщо тільки не повертається раніше з методу.

Ще одна особливість та ідіома, поєднана - це використання трансляційних ifоператорів для підвищення читабельності коду. Однією з рушійних ідей у ​​Ruby є написання коду, який читається як натуральна мова. Для цього ми переходимо до Покровного посібника по Рубі _why, глава 3 .

Прочитайте наступне вголос собі.

5.times { print "Odelay!" }

В англійських реченнях розділові знаки (такі як періоди, вигуки, дужки) безшумні. Знаки пунктуації додають значення слів, допомагають дати підказки щодо того, що автор задумав речення. Тож давайте прочитаємо вище: «П’ять разів надрукуйте« Одела! ».

Враховуючи це, приклад коду №3 є найбільш ідіоматичним для Ruby:

def name(use_email = true)
  return email if firstname.blank? && surname.blank? && use_email

  "#{firstname} #{surname}".strip
end

Тепер, коли ми читаємо код, він говорить:

Поверніть електронну пошту, якщо ім'я порожнє, а прізвище - порожнє та використовуйте електронну пошту

(повернення) позбавлене імені та прізвища

Що досить проклято близько до фактичного коду Ruby.

Це лише 2 рядки фактичного коду, тому він досить лаконічний, і він дотримується ідіоми мови.


Приємний момент. Це правда, що питання не мало бути орієнтоване на Рубі, але я погоджуюся, що тут не можна відповісти на мовний агностик.
Судюкіл

8
Мені здається, що ідея створення коду звучить як природна мова значно завищена (а часом навіть проблемна). Але навіть без цієї мотивації я приходжу до того ж висновку, що і ця відповідь.
Конрад Рудольф

1
Є ще один твік, який я б вирішив зробити з кодом. Тобто ставити use_emailперед іншими умовами, оскільки це змінна, а не виклик функції. Але знову ж таки струнна інтерполяція все одно переповнює різницю.
Джон Дворак

Структурування коду за природними мовними структурами може змусити вас потрапити в мовні пастки. Наприклад, коли ви читаєте наступні вимоги do send an email if A, B, C but no D, дотримуючись вашої передумови було б природно набрати 2, якщо / else блоки, коли, ймовірно, буде простіше кодувати if not D, send an email. Будьте уважні до моменту читання природної мови та перетворіть її на код, тому що це може змусити вас написати нову версію "Невідомої історії" . За допомогою класів, методів та змінних. Зрештою, це не велика справа.
Лаїв

@Laiv: Перетворення коду як натуральної мови не означає буквально перекладати вимоги. Це означає писати код таким чином, що при читанні вголос він дозволяє читачеві зрозуміти логіку, не читаючи кожного коду, символу для символу, мовної конструкції для побудови мови. Якщо кодування if !Dкраще, це добре, як lond, оскільки Dмає значущу назву. І якщо !оператор загубиться серед іншого коду, то NotDбуло б доречним мати ідентифікатор, що викликається .
Грег Бургхардт

15

Я не думаю, що ви отримаєте кращої відповіді, ніж "використовуйте своє найкраще рішення". Словом, вам слід прагнути до ясності, а не до короткості . Часто найкоротший код також є найяснішим, але якщо зосередитись лише на досягненні ясності, можна постраждати. Це явно так і в двох останніх прикладах, для розуміння яких потрібно більше зусиль, ніж попередні три приклади.

Важливе врахування - аудиторія коду. Зрозумілість, звичайно, повністю залежить від того, хто читає. Чи справді люди, яких ви очікуєте, щоб прочитати код (поруч із собою), насправді знають ідіоми мови Рубі? Ну, це питання не є чимось випадковим випадком, коли люди в Інтернеті можуть відповісти, це лише ваше власне рішення.


Я погоджуюся з точкою аудиторії, але це частина моєї боротьби: оскільки моє програмне забезпечення часто є відкритим кодом, аудиторію можуть складати як початківці, так і «рубінні боги». Я міг би зробити це простим, щоб зробити його доступним для більшості людей, але це відчуває себе як марно користь, яку пропонує мова.
Судюкіл

1
Оскільки хтось, кому довелося перейняти, розширити та підтримувати якийсь справді жахливий код, чіткість має виграти. Запам’ятайте стару приказку - Напишіть свій код так, ніби обслуговуючим особою є мстивий Пекло Ангел, який знає, де ви живете і де ваші діти ходять до школи.
uɐɪ

2
@Sudiukil: Це важливий момент. Я пропоную вам в цьому випадку прагнути до ідіоматичного коду (тобто припускати добре знання мови), оскільки навряд чи початківці будуть сприяти відкритому коду в будь-якому випадку. (Або якщо вони зробляться, вони будуть готові докласти зусиль, щоб вивчити мову.)
Жак

7

Частина проблеми тут - "що є читанністю". Для мене я переглядаю ваш перший приклад коду:

def name(use_email = true)
 # If firstname and surname are both blank (empty string or undefined)
 # and we can use the email...
 if (firstname.blank? && surname.blank?) && use_email
  # ... then, return the email
  return email
 else
  # ... else, concatenate the firstname and surname...
  name = "#{firstname} #{surname}"
  # ... and return the result striped from leading and trailing spaces
  return name.strip
 end
end

І мені важко читати, оскільки він сповнений "галасливих" коментарів, які просто повторюють код. Роздягніть їх:

def name(use_email = true)
 if (firstname.blank? && surname.blank?) && use_email
  return email
 else
  name = "#{firstname} #{surname}"
  return name.strip
 end
end

і це зараз набагато читабельніше. Після цього, читаючи його, я думаю, "гм, мені цікаво, чи підтримує Рубі потрійний оператор? У C # я можу записати це як:

string Name(bool useEmail = true) => 
    firstName.Blank() && surname.Blank() && useEmail 
    ? email 
    : $"{firstname} {surname}".Strip();

Чи можливо щось подібне в рубіні? Переглядаючи вашу посаду, я бачу:

def name(use_email = true)
 (email if (firstname.blank? && surname.blank?) && use_email) || "#{firstname} #{surname}".strip
end

Всі добрі речі. Але це для мене не читабельно; просто тому, що мені потрібно прокрутити, щоб побачити весь рядок. Тож давайте виправимо це:

def name(use_email = true)
 (email if (firstname.blank? && surname.blank?) && use_email) 
 || "#{firstname} #{surname}".strip
end

Тепер я задоволений. Я не зовсім впевнений у тому, як працює синтаксис, але можу зрозуміти, що робить код.

Але це тільки я. Інші люди мають дуже різні ідеї щодо того, що приємно читати фрагмент коду. Тому вам потрібно знати свою аудиторію під час написання коду. Якщо ви абсолютно початківі викладачі, то вам захочеться зробити це просто і, можливо, написати це як ваш перший приклад. Якщо ви працюєте серед безлічі професійних розробників з багаторічним досвідом роботи в рубіні, тоді напишіть код, який використовує мову та не зважайте на неї. Якщо це десь посередині, то націліться на десь посередині.

Хоче сказати одне: остерігайтеся "розумного коду", наприклад, у вашому останньому прикладі. Запитайте себе, чи [firstname, surname].all?(&:blank?)додає нічого, крім того, щоб ви відчували себе розумними, тому що це демонструє свої навички, навіть якщо зараз це читати трохи важче? Я б сказав, що цей приклад, ймовірно, повністю входить у цю категорію. Якщо ви порівнювали п'ять значень, я вважав би це хорошим кодом. Отже, тут абсолютно немає абсолютної лінії, просто пам’ятайте, що ви занадто розумні.

Отже, підсумовуючи: читабельність вимагає, щоб ви знали свою аудиторію та відповідно орієнтували свій код і писали лаконічний, але чіткий код; ніколи не пишіть "розумний" код. Тримайте його коротко, але не надто коротко.


2
Ну, я забув це згадати, але коментарі повинні були бути "проігноровані", вони тут лише для того, щоб допомогти тим, хто не знає Рубі добре. Дійсна точка, хоча щодо аудиторії, я про це не думав. Що стосується версії, яка робить вас щасливою: якщо значення має довжина рядка, третя версія мого коду (та, яка має лише одне повернення заяви) це робить, і це навіть трохи зрозуміліше, ні?
Судюкіл

1
@Sudiukil, не будучи розробником рубіну, виявив, що найважче читати, і це не відповідає тому, що я шукав (з точки зору іншої мови) як "найкраще" рішення. Однак для того, хто знайомий з тим, що рубін - одна з тих мов, яка повертає значення останнього виразу, він, ймовірно, являє собою найпростішу, найпростішу для читання версію. Знову ж таки, це все стосується вашої аудиторії.
Девід Арно

Не є розробником Ruby, але це має набагато більше сенсу для мене, ніж відповідь, яка голосує вгорі, яка говорить: "Ось, що я повернуся (виноска: при конкретному довгому стані]). на вечірку." Логіка, яка по суті є лише випадком справи, повинна писатися як єдиний єдиний випадок випадку, а не поширюватися на кілька, здавалося б, не пов'язаних тверджень.
Пол

Особисто я пішов би з вашим другим блоком коду, за винятком того, що я поєднав би два твердження у вашій іншій гілці в одне:return "#{firstname} #{surname}".strip
Павло

2

Це, мабуть, питання, на яке важко не дати відповіді, заснованої на думці, але ось два мої центи.

Якщо ви виявите, що скорочення коду не впливає на читабельність або навіть покращує його, перейдіть до цього. Якщо код стає менш читабельним, то вам слід врахувати, чи є досить вагома причина, щоб залишити його таким чином. Робити це просто тому, що це коротше, або круто, або просто тому, що ви можете, є прикладами поганих причин. Вам також потрібно врахувати, якщо скорочення коду зробить його менш зрозумілим для інших людей, з якими ви працюєте.

То що б було вагомою причиною? Дійсно, це виклик судження, але приклад може бути чимось на зразок оптимізації продуктивності (звичайно, після тестування ефективності, звичайно, не заздалегідь). Щось, що отримує певну вигоду, яку ви готові заплатити за зниженої читабельності. У такому випадку ви можете зменшити недолік, надавши корисний коментар (який пояснює, що робить код, і чому його потрібно було зробити трохи виразним). Ще краще, ви можете витягнути цей код в окрему функцію зі значущою назвою, так що це лише один рядок на сайті виклику, який пояснює, що відбувається (через назву функції), не вникаючи в деталі (однак, люди мають різні думки з цього приводу, тому це ще один заклик, який ви повинні зробити).


1

Відповідь трохи суб’єктивна, але ви повинні запитати себе з усією чесністю, яку ви можете назвати, якщо ви зможете зрозуміти цей код, коли повернетесь до нього через місяць-два.

Кожна зміна повинна покращити здатність пересічної людини розуміти код. Щоб зробити код зрозумілим, допомагає використовувати наступні вказівки:

  • Поважайте ідіоми мови . C #, Java, Ruby, Python мають усі бажані способи робити те саме. Ідіоматичні конструкції допомагають зрозуміти код, який вам не знайомий.
  • Зупиніться, коли ваш код стане менш читабельним . У прикладі, який ви надали, це сталося під час натискання останніх пар скорочувального коду. Ви втратили ідіоматичну перевагу попереднього прикладу та ввели багато символів, які потребують багато роздумів, щоб по-справжньому зрозуміти, що відбувається.
  • Використовуйте коментарі лише тоді, коли вам доведеться виправдовувати щось несподіване . Я знаю, що ваші приклади були для пояснення конструкцій людям, менш знайомим з Рубі, і це нормально для запитання. Я вважаю за краще використовувати коментарі для пояснення несподіваних правил бізнесу та уникати їх, якщо код може говорити сам за себе.

Однак, є випадки, коли розширений код допомагає зрозуміти, що відбувається краще. Один із прикладів цього - C # і LINQ. LINQ - це чудовий інструмент і може підвищити читабельність у деяких ситуаціях, але я також зіткнувся з низкою ситуацій, коли це було набагато заплутаніше. Я мав деякі відгуки в експертній оцінці, які пропонували перетворити вираз у цикл з відповідними, якщо заяви, щоб інші могли підтримувати його краще. Коли я поступився, вони мали рацію. У технічному відношенні LINQ є більш ідіоматичним для C #, але є випадки, коли він погіршує зрозумілість, і більш багатослівне рішення покращує його.

Я кажу все це, щоб сказати це:

Поліпшити, коли можна зробити свій код кращим (зрозумілішим)

Пам'ятайте, що вам або комусь, як ви, пізніше доведеться підтримувати цей код. Наступного разу, коли ви зіткнетеся, це може бути місяцями вниз. Зробіть собі прихильність і не переслідуйте скорочення підрахунку рядків ціною розуміння вашого коду.


0

Читання - це властивість, яку ви хочете мати, маючи-багато-один-вкладиші - це не. Тому замість "однолінійного читання читання" питання має бути таким:

Коли однолінійки збільшують читабельність, а коли вони завдають шкоди?

Я вважаю, що однолінійки корисні для читабельності, коли вони виконують ці дві умови:

  1. Вони занадто специфічні, щоб бути вилученими до функції.
  2. Ви не хочете переривати "потік" читання навколишнього коду.

Наприклад, скажімо, це nameбуло не дуже добре ... ім'я для вашого методу. Це поєднання імені та прізвища або використання електронної пошти замість імені не були природними завданнями. Тож замість nameнайкращого, що ви могли придумати, вийшло довге і громіздке:

puts "Name: #{user.email_if_there_is_no_name_otherwise_use_firstname_and_surname(use_email)}"

Таке довге ім’я означає, що це дуже специфічно - якби було більш загальним, ви могли б знайти більш загальне ім’я. Таким чином, обертання його методом не допомагає ні з читабельністю (це занадто довго), ні DRYness (занадто специфічним, щоб використовуватись деінде), тому краще просто залишити код там.

Все-таки - навіщо робити його однолінійним? Зазвичай вони менш читабельні, ніж багаторядковий код. Тут ми повинні перевірити мою другу умову - потік оточуючого коду. Що робити, якщо у вас є щось подібне:

puts "Group: #{user.group}"
puts "Title: #{user.title}"
if user.firstname.blank? && user.surname.blank?) && use_email
  name = email
else
  name = "#{firstname} #{surname}"
  name.strip
end
puts "Name: #{name}"
puts "Age: #{user.age}"
puts "Address: #{user.address}"

Сам мультилінійний код читабельний, але коли ви намагаєтеся прочитати навколишній код (друкуючи різні поля), багаторядкова конструкція перериває потік. Це легше читати:

puts "Group: #{user.group}"
puts "Title: #{user.title}"
puts "Name: #{(email if (user.firstname.blank? && user.surname.blank?) && use_email) || "#{user.firstname} #{user.surname}".strip}"
puts "Age: #{user.age}"
puts "Address: #{user.address}"

Ваш потік не переривається, і ви можете зосередитися на конкретному виразі, якщо вам потрібно.

Це ваш випадок? Точно ні!

Перша умова є менш актуальною - ви вже вважали, що це достатньо загальне, щоб заслуговувати метод, і придумали назву цього методу, який можна прочитати набагато легше, ніж його реалізація. Очевидно, ви знову не отримаєте його до функції.

Щодо другої умови - чи перериває вона потоки навколишнього коду? Ні! Навколишній код - це декларація методу, що вибір nameє єдиною метою. Логіка вибору імені не перериває потік навколишнього коду - це сама мета навколишнього коду!

Висновок - не робіть все функціональне тіло однолінійним

Однокласники хороші, коли ви хочете зробити щось трохи складне, не перериваючи потік. Оголошення функції вже перериває потік (щоб він не був перерваний, коли ви викликаєте цю функцію), тому перетворення всього корпусу функції в один вкладиш не допомагає читати.

Примітка

Я маю на увазі "повномасштабні" функції та методи - не вбудовані функції чи лямбда-вирази, які зазвичай є частиною оточуючого коду та потребують вміщення в його потоці.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.