Comments 145
Ваша статья нарушает Ваше правило «быть коротким».
Множество приводимых Вами альтернатив нарушает Ваше правило «быть линейным».
Это все прекрасная беллетристика, которая никак не применима к реальной жизни (к сожалению.)

Простой пример: пункт про бритву Оккама противоречит пункту про линейность. Как всегда, нужно найти компромисс и тут улинеить, а вот тут не нарушать принцип Оккама и не плодить сущности. В смысле этом ваша статья полностью подпадает под введение в нее же: красивая, стройная, неприменимая теория, обучение которой не дает ровным счетом ничего, помимо общей образованности (которая важна и нужна, разумеется).

Рецептов не существует, и именно поэтому так сложно воспитать из жуниора мастера. Смысл в том, что вы седьмым чувством определяете: тут нужно вынести вот этот кусок кода в отдельную функцию, а тут — нет. А жуниор, пока не вырастет, будет смотреть на это и думать: «чистое шаманство» (в лучшем случае), или «да он идиот, сам не знает, чего хочет, это же одинаковые куски» в худшем.

Ну и тот код, который пишут не жуниоры как правило точится по совсем другим лекалам. Как вы мне предлагаете залинеить генерацию кода? Рантайм патчинг? Несанкционированный хук? Аспекты, луа внутри песочницы? Вызов DLL на удаленной Win98, just because?

А нелинейный спагетти я прочту за минуту, вместо шестидесяти секунд. Любой не жуниор прочтет.

⇑ вот это все, разумеется, не про то, что нужно писать говнокод. Вы правильные вещи говорите, конечно. Просто бесполезные :)

Вы говорите, что описанное в статье бесполезно, потому что:
1. Не всегда возможно соблюсти все советы сразу.
2. Решения все равно принимаются «седьмым чувством».
3. «Любой не-джуниор» все равно быстро прочтет адский спагетти-код (на мой взгляд, очень странное утверждение).

Если написанное в заключении к статье ничего не проясняет, то вот еще раз мое видение по данным вопросам:

1. Из того, что нельзя сделать дело идеально, не значит, что не надо делать хорошо. И тем более, что не надо понимать, что такое хорошо.
2. Я всегда стараюсь при возникновении вопросов объяснять коллегам, почему мое решение хорошее, а другое — хуже. Если не могу объяснить, то на решении не настаиваю. «Седьмое чувство» без логического обоснования — это отмазка, за которую можно спрятать все, что угодно.
3. Умеющим читать (и править) любой говнокод за минуту, статью можно игнорировать )
Давайте я еще раз попробую объяснить, что я имею в виду. Вы не обижайтесь только, я сто раз же оговорился: годный текст :)

Знаете, бывают такие библиотеки, фреймворки, языки программирования даже, на которых авторы написали офигенную реализацию todo-списка и движка блога. Смотрите, как у нас тут все красиво, говорят они — и не поспоришь. Гораздо красивее, чем на любых существующих языках.

Но как только пытаешься решать задачу из реального мира — вылезает большая голодная крыса и съедает всю красоту необходимостью воздвигать костыльнуб подпорку для потолка шестнадцатого этажа.

Так же и ваша линейность, и декларативные подходы, и все прочее. Это все работает на вашем примере — прекрасно работает, не спорю. Только вот беда, у разработчика в реальном мире обычно есть еще десять мегабайт легаси и пять мегабайт написанного жуниорами и три мегабайта написанного им самим с похмелья, о нон работает #прямосейчас. Поэтому поломать все нафиг и написать красиво — как бы не вариант. Ну если мы про бизнес, а не про школьный класс.

Поэтому техника чтения спагетти, рефакторинга костылей (перекуем костыль на миксин, как большевики говорили), умение правильно оценить место, куда фикс воткнуть, и так далее — в миллион раз важнее, чем способность написать читаемый код (что, повторяб, тоже круто, но не всегда возможно.)

Вот вам пример из реальной жизни: живой проект, нужно воткнуть перпендикулярные существующим права доступа. Я написал такой код, что вы бы меня даже на собеседование не пригласили, наверное. Полностью на аспектах, горячие заплатки на классы, трассировка кода в реалтайме, половина генерируемго кода читается из строковых файлов и парсится регулярками. Кровь, кишки. Писал где-то пару недель. Покрыл тестами так, что не вздохнуть. Работает (как? — сам в шоке). За это время в дев-ветку накоммитили килобайт сто, все в тот код, который мне патчить. И мой некрасивый, нелинейный, костыльный код влился без единого конфликта. А красивый линейный — я бы до сих пор писал.

Без обид, спасибо за отзыв.
Просто забавно, что вы рассказываете, что все то, что я использую в своей работе ежедневно и при этом вижу результаты, оказывается, неприменимо к реальности )

Касательно вашего примера, допускаю, что при определенных обстоятельствах я бы мог поступить так же. Но при этом:
1. Осознавал бы, что время, которое я, казалось бы, выиграл, вернется в десятикратном размере в процессе поддержки кода.
2. Ни в коем случае не считал бы свою работу сделанной качественно.
3. И уж совершенно точно не стал бы утверждать, что вот это и есть реальная жизнь, а все остальное — фантазии.
fyudanov, matiouchkine
У каждого из вас просто-напросто разная практика. На деле, множество аутсорсинговых компаний не заморачивается качеством. Как правило долгосрочную поддержку они не оказывают, продукты педалятся быстро, умирают тоже быстро. В итоге в долгосрочной перспективе качество кода не важно. А в краткосрочной перспективе очень сложно объяснять почему нужен рефакторинг. Зато плохое качество кода приводит к тому, что проект не заканчивается раньше чем нужно, что является хорошим фактом с финансовой точки зрения кампании. Ну и оправдываться в стиле «очень много функционала, каждая новая фича пишется труднее» — это нормально и привычно всем. В некоторых компаниях очень заморачиваются на эстимейты, поэтому сделать больше, чем нужно — это неприемлемо для менеджмента.

Вообще, на разной длительности проекта и с ростом размеров кода нужно придерживаться разных правил. Глупо писать свою инфраструктуру/фреймворк когда вам нужно реализовать одну-две апих. И глупо не писать, когда апих становится много. Прикол заключается в том, что многие разработчики не понимают концепт «рефакторить все время», в итоге получаем легаси и говнокод во все бока.

Также стоит отметить, что отрефакторить что-то за один раз не всегда получается. А пока ты занимался чем-то другим другой разработчик приходит и дописывает еще херни.
У меня лично очень разная практика. В идеальном мире я писал проекты с нуля, в команде людей, умнее и опытнее меня, с бесконечным временем выделенным на рефакторинг.

Но чаще приходится работать в мире реальном. В проекте, который достался тебе таким, какой есть. В который другие разработчики дописывают херни, да :)

Именно поэтому все без исключения успешные стартапы были в какой-то момент переписаны с нуля. Но это эммм… не интересно же. Ну разве что с академической точки зрения. Так то куда интереснее в горящем танке порядок наводить, чем фигачить по паттернам. С последним прекрасно справляются жуниоры под присмотром.

Ну и да: качество кода не есть просто функция его читаемости. Качество кода есть функция полезности проекту. Просто чаще всего тут прямая зависимость от читаемости и новички путают эти вещи.

Есть две функции от параметра «исходный код», как минимум: его полезность (чаще всего доход или экономия финансов и/или времени) проекту и его цена (чаще всего расходы финансов и/или времени). Качество, грубо говоря, это соотношение этих функций. Читаемость тут лишь один из параметров обеих функций.
Правила, вроде, относятся к коду, а не к статьям и подаче информации впринципе.
В этом-то и заключается вся сложность: твое представление о “достойном” и “красивом” коде полностью основано на личном многолетнем опыте. Попробуй теперь передать это представление в сжатые сроки человеку с совсем другим опытом или даже вовсе без него.

А как же соответствующая литература?

(Собственно, поэтому же удивляет ваш пассаж «в книжках [...] нам рассказывают про алгоритмы, языки, принципы ООП, паттерны дизайна…». Создается ощущение, что вы просто не читали «те» книжки.)
UFO landed and left these words here
Плюсую ссылку. Не прочитавшему Макконнелла — не место в профессии.
сейчас по количеству минусиков мы узнаем, сколько человек не читали Макконнелла ;-)
Сколько людей — столько и мнений, а я вот возьму и скажу (это не значит, что я так на самом деле думаю, но все же), что Макконелл — пижон, и говорит прописные истины, а Фаулер пишет слишком сложно и академично, из за этого над его книгами засыпаешь, будет примерно такое же голословное утверждение, как и выше. Кто-то будет убеждать, что читать надо только интересные книжки издательства Manning с человечками в костюмах на обложках, где в большинстве случаев все написано простым языком и на высоком уровне. А кто-то скажет, что вообще книжек не читает, а смотрит какой-нибудь Pluralsight, и тоже будет по-своему прав.
Эти истины становятся прописными только с опытом, после кучи сломанных костылей и велосипедов. Так что чем раньше потенциальный программист эти прописные истины прочитает, тем лучше и ему и окружающим.
if (obj && obj.s) {
// do something
}

Вот так и возникают внезапные баги при obj.s == 0.
Безусловно, баги возникают, но автор говорил о применении такого подхода программистами хорошо знающими язык. Такие программисты, знают о том, что если число может быть нулевым, то проверка должна это учитывать.

Реальные проблемы начинаются, когда менее опытные коллеги работают с таким кодом.
В случае, когда код настолько не очевиден, проблемы не прекращаются и когда работает его автор.
Простите, кажется, этот код недостаточно самодокументирован. Что он символизирует?
Баг в том, что разработчик этот момент может не предусмотреть. Если obj.s может быть произвольным числом, а кодер по привычки поставил obj && obj.s как «проверку на всякую бяку», случается беда. Или, в случае не совсем новичка, если изначально obj.s обязано было быть положительным числом, а потом требования изменились.
Ах вот вы о чём, ну тогда вам вот ниже коммент оставили. НЕочевидное поведение предусматривать надо.
UFO landed and left these words here
>6.3… желательно в своей программе создавать переменные/функции/объекты, только если они имеют прототипы в предметной области.

Или, например, если в списке используемых инструментов имеется отладчик, и слова «точка останова» о чём-нибудь говорят. IMHO, весь этот пункт вообще не про красоту кода, а про микрооптимизации. Более того, не только ради отладки, но и ради ясности кода я иногда предпочитаю вводить «промежуточные» переменные.
Как обычно на любой «книжный» пример автора аудитория найдёт по 10 контр-примеров из реальных проектов.
Спрашивается — как задокументировать и привить здравый смысл и чувство прекрасного, на которые всё в итоге опирается?
Элементарно! Достаточно подождать пару лет и заставить рефакторить собственный код двухлетней выдержки. Повторять до просветления. Так буддистские монахи дзен познают.) Ну или книжки «другие» почитать.
Упертые товарищи в этом случае просто засунут в код еще немного костылей и заплаток и будут считать, что это и есть самый верный подход к работе. «Лишь бы работало»
Вы забыли про магические константы, которые следует заменять именованными с целью централизации управления ими + повышения читабельности кода
На кой хрен мне централизация управления константами, относящимися к разным местам кода, не имеющим ничего общего между собой?
Например, чтобы весь проект быстро перевести из метров и килограммов в дюймы и фунты.
Ни на кой.
А вот для управления константами, относящимися к разным местам кода, имеющим что-то общее — пригодится. Ваш к.о.
Не испытываю такого благоговения перед константами. По сути они те же переменные, только константы )) Просто еще один аспект кода. Выносить константы из всех модулей в одно общее место — странное занятие. Сгруппировать похожие по смыслу и назначению константы (например какие-то идентификаторы) близко друг от друга в коде — да, смысл есть.
Действительно. Зачем нам общая константа Math.PI? Давайте в каждом классе определим своё значение числа пи. Или 1296000 — ведь все знают, что это такое. Зачем ему давать название, да ещё и выносить в общее для всех место?
Кстати, а что это? 15 суток в секундах? Или два развёрнутых угла в угловых секундах? Не уверен, что из этого чаще встречается в программировании.
Второе — число угловых секунд в полном обороте. Встречается, когда угловая секунда — единица измерения углов во всём проекте.
В исходном сообщение, на которое я ответил, была фраза
«магические константы, которые следует заменять именованными с целью централизации управления ими».
Под «централизацией» я обычно понимаю сосредоточение в одном месте (с т.з. русского языка).
Например, не в каждой процедуре своя, а одна на все. В этом модуле. Централизация.
С чего вы взяли, что речь идет только про «выносить из всех модулей в одно общее место»? Это ваша выдумка.
Это может быть именованная константа в пределах одного модуля или одной процедуры.
6.6. Все объекты объявляем в максимально узкой области видимости.

Это зависит. Конкретно это очень важно в случае public api, в нем полное скрытие внутренней логики иногда свидетельствует о плохом class design. В грамотном случае внутренняя логика (где это уместно) может быть сделана protected. В каждом конкретном случае, нужно, конечно, смотреть отдельно.
>>Конкретно это очень важно в случае public api, в нем полное скрытие внутренней логики иногда свидетельствует о плохом class design

Приведите пример, пожалуйста. Не представляю когда сокрытие логики может быть плохим дизайном.
Хорошие примеры:
Самый банальный пример — LinkedHashMap, protected-метод removeEldestEntry — здесь на уровне класс-дизайна заложили расширенную функциональность, не вытаскивая это в public (например, конструктор с параметром int вводил бы в заблуждение как initialSize, но не maxSize).
В более сложном случае — Spring Jdbc Templates я рассматриваю как один из образцов хорошего API. Развитая структура классов, которая в том числе содержит protected-логику — что позволяет встраиваться через наследование и делать то, что через делегирование требовало бы большого количества повторенного кода, а в некоторых местах было бы невозможно без форка библиотеки.

Плохой пример — любая библиотека, которую пришлось форкнуть, потому что ее class design не позволял изменить логику нужным образом. Здесь может быть много различных оговорок, например, об уместности конкретных доработок, нюансов security (из-за которых часть API может быть приватна или final) и пр., но, надеюсь, я донес свою мысль.
----var1 = a? b: c;

это пример как писать не надо.

попробуйте поставить точку останова на ветку ELSE?

К тому же если автор использует TDD то код и так напишется чистым и красивым.

---Лишние проверки — зло, которое можно встретить практически в любой программе.

Если пишете bulletproof код, то лишние проверки нужны как для юнит тестов так и для будушей гарантии в случае рефакторинга.

Замените new Object на new myObject с возможностью возврашения null и всему коду можно ставить жирный минус.

Такое ошушение что статья писалась теоретиком, все кодирование которого сводилось к написанию академических примеров.

В реальной жизни в компании постоянно покупаются сторонние компании и в проект вливается жуткий код.
А зачем в данном коде ставить точку останова на else? Здесь простое присвоение и что присвоилось вы можете узнать остановившись на следующей строчке.
Более того, тернарный оператор это выражение, а не набор инструкций, и там нет такого понятия как ветка вообще.
Для отладки. В более сложном примере. Допустим, вам надо поймать только присвоение c, а в остальных случаях — пройти без остановки. Например, у вас var1 = b происходит десятьтыщьмильонов раз, а var1 = c — единожды. У нас в команде, например, договорились всегда разносить if с условием и ветки в один оператор как раз для установки BPтов.
А допустим, что вам надо поймать присвоение b для конкретного случая a==73, а в остальных случаях пройти без остановки. Что будете делать? Наверное, поставите условный breakpoint или временно измените код. И чем эта ситуация отличается от ловли присваивания при a==0? Да, она встречается несколько реже. Но для каждого конкретного присваивания шансы, что именно на нём придётся что-то такое ловить, составляют, скажем, 0.01% для случая a==73 против 0.1% для случая a==0. Стоит ли ради этого портить весь код, вставляя ненужные if/else вместо тернарного оператора?
— 0.01% для случая a==73 против 0.1% для случая a==0. Стоит ли ради этого портить весь код

Когда я дебажу код по присланному pdb для сервера BOA и тамошние админы полчаса настраивают среду чтобы для воспроизвести баг, я в первую очередь думаю как решить проблему крупного клиента, а не о том как лаконичен код.

В шарпе я уже понял не актуально разбирать дампы и удаленно отлаживать.

Если я не смогу поставить break по условию я не смогу помочь клиенту и меня могут попросить расстаться с работой при таком подходе.
Это более редкая задача, чем ловля одной из веток. Более того, условие может сложным и не всегда можно поставить условный останов на такое или он сильно замедляет выполнение.

Стоит ли ради этого портить весь код, вставляя ненужные if/else вместо тернарного оператора?

Стоти ли портить весь код, вставляя ненужный тернарный оператор, вместо if/else? Тернарный оператор, напомню, сложнее, чем просто if/else «в строчку» и позволяет творить интересные трюки — есть места, где он реально нужен и красив. Но не здесь. Зачем было уходить от однострочной мешанины, чтобы опять к ней вернуться?
Зачем было уходить от однострочной мешанины,

Не знаю, что понимается под «мешаниной», но когда в экранах было 24 строчки, программы с однострочными конструкциями читались гораздо легче, чем многострочные. И сейчас это позволяет охватить взглядом гораздо больший кусок кода, а не возить его по экрану взад-вперёд.
Что касается отладки, то, вероятно, код, нацеленный на наиболее эффективную отладку, должен обладать своими особенностями — не обязательно совпадающими с «лаконичностью», «читабельностью» или «эффективностью».
Насчёт того, что тернарный оператор сложнее — не понял. Он слегка другой, чем if/else — потому что является выражением, а не двумя отдельными операторами. Где-то это помогает, где-то мешает, где-то — всё равно, и можно выбрать более подходящий из двух вариантов. Конечно, если из тернарных операторов в сочетании с && и || строится конструкция длиной в 6 строк, большая часть выражений в которой имеет побочный эффект, это может быть страшно. Но может быть, и нет — если она грамотно написана, то её можно просто прочитать, как инструкцию…
Не знаю, что понимается под «мешаниной», но когда в экранах было 24 строчки, программы с однострочными конструкциями читались гораздо легче, чем многострочные

И тем не менее,
a=b; b=c; c=d; if (a==5) d = 7;

дурной тон.

Что касается отладки, то, вероятно, код, нацеленный на наиболее эффективную отладку, должен обладать своими особенностями — не обязательно совпадающими с «лаконичностью», «читабельностью» или «эффективностью».

Выражение вида
if (a==5) 
  doSomething;


позволяет ставить BP и читается лучше, чем

if (a==5) doSomething;


особенно, если doSomething и условие — массивные. Здесь одновременно лаконичный и красивый, читаемый код, и отлаживать можно.

Насчёт того, что тернарный оператор сложнее — не понял. Он слегка другой, чем if/else — потому что является выражением, а не двумя отдельными операторами.

Нет. Тернарный оператор может возвращать lvalue, и там он действительно красив.
И тем не менее,
a=b; b=c; c=d; if (a==5) d = 7;
дурной тон.

Пожалуй. Хотя про
var t=a; a=b; b=t;

(при отсутствии или нежелании использовать swap) я не был бы так уверен.

Выражение вида
if (a==5)
doSomething;

позволяет ставить BP и читается лучше, чем

if (a==5) doSomething;


Насчёт BP — вопрос к среде. Visual Studio позволяет ставить breakpoint на оператор, являющийся частью строки, в C#, но не позволяет в C++. Если условие и действие массивные, то отдельные строчки лучше (а я бы ещё и скобки поставил :( ), но если нет — предпочту одну строку.

Тернарный оператор может возвращать lvalue,

Везёт вам. В C# не может. Там и функций, возвращающих lvalue, нет :(
Везёт вам. В C# не может. Там и функций, возвращающих lvalue, нет :(
Что-то типа
a[b ? 1 : 0] = c;
— нет?
Нет, b?1:0 это всё равно rvalue. Хочется что-то вроде (a<b? a: b)=(a>b? a: b);
При использовании rvalue в качестве индекса массива в итоге получается lvalue. Ваш пример, конечно, элегантен, но его будет трудно рефакторить если что…
4.2. Техника 2. Используем break, continue, return или throw, чтобы избавиться от блока else.


Про return — довольно неоднозначный совет.
Тот же Макконел не рекомендует увлекаться множественными выходами из метода. (Вернее в одном месте книги он рекомендует делать это, а в другом советует не увлекаться).
Ну просто ничем не стоит «увлекаться», а то случится беда. Но в среднем ранний выход упрощает чтение кода (иногда — настолько, что я рефакторю код так, чтобы сложный кусок оказался в отдельном методе, где можно сделать ранний выход).
Множественные выходы из метода мешают тогда и только тогда когда функция не умещается на экране.
Если писать код маленькими функциями (как и советует Макконел), то никаких проблем нет.
Если писать код маленькими функциями (как и советует Макконел)

Макконел как раз утверждает, что нет ничего плохого в использовании относительно больших функций. Он предлагает не превращать маленький размер функций в самоцель, а определять размер функции в зависимости от задачи, которая на нее возложена и сложности внутренней структуры (та самая линейность, о которой говорит автор статьи). Ссылается он на исследования связи количества ошибок с размером функций, в которых они были найдены. От себя добавлю, что одна относительно большая функция, состоящая в основном из стандартных операторов, может быть значительно читабельней, чем пять маленьких ссылающихся друг на друга по цепочке.
Рекомендация про умещение в размер экрана у него тоже была, но мониторы нынче большие, шрифты маленькие, поэтому к маленьким функциям это отношения не имеет. Просто, как сказал lair, «ничем не стоит «увлекаться», а то случится беда».
Тут камень преткновения в том можно ли считать код с множественными return линейными, с простой структурой. Скорее нет, чем да, а потому функции с ним должны быть небольшими.
Потому что при анализе кода, меня может интересовать именно альтернативная ветка и тогда:
1) её сложнее найти визуально, так как нет явного маркера else
2) код выглядит линейным, по сути им не являясь, что вводит в заблуждение при беглом просмотре
3) если я начал читать с закрывающей скобки блока if (ну не интересне мне сейчас основной сценарий, мне надо изменить что-то в альтернативном), я сочту, что код после блока выполняется всегда, что не так.

Так что совет 4.2 — ВРЕДНЫЙ, НЕ НАДО ТАК ДЕЛАТЬ
Совет нормальный, просто не везде он подходит. Если вы проверяете, например, не пуста ли входная строка и выходите из метода, это лучше, чем заключать все тело метода в блок при чтении кода. Потому что сразу видно — вот они, условия проверки, по которым мы сразу вываливаемся.
В таком случае, соглашусь, допустимо. Но, если кроме выхода из блока в ветке ничего нет, скорее всего, более читаемо будет инвертировать условие и избавиться и от return и от else.
Тогда вся основная ветка будет вложена в if и идти с отступом. А если проверочных условий хотя бы два или три, то и с двумя или тремя отступами.
Начнем с простого вопроса: а в языке, которым вы пользуетесь, есть исключения? И если они есть, пользуетесь ли вы ими?
Исключения, по моему, лучше использовать для исключений. Строить логику алгоритма на исключениях, чаще всего, не очень хорошая идея.
Я понял. Я сосредоточился на примере кода с return и мне он не понравился. В совете речь идёт о наборе конструкций, и сам по себе совет нормальный. Как любой инструмент, применять его надо с умом, что бы не возникало перечисленных мной последствий.
Все советы неоднозначные. Кому-то не нравится множественный return. Кому-то throw в штатной ситуации. Кому-то break, применённый не совсем по назначению. Как здесь использовать continue, не очень понятно (если код не находится в конце тела цикла изначально). Ну, и в список можно добавить goto — он тоже кому-то не понравится.
пример с continue вижу как то так:
const NEAR = ....;
for( var i in arr){
   var obj = arr[i];

   if( obj.getType() !== "human" )
     continue;

   if( obj.getDistance() > NEAR )
     continue;

   obj.sendMessage(obj.name + "вы уже близко!" );
}

Про пример в 4.4. А потом выясниться, что метода do_a() иногда меняет состояние check и получим такой вкусный баг…
На мой взгляд, на примере C# на красивый код должны посмотреть:

1) сам программист
2) его коллеги при код-ревью
3) решарпер
4) стайл-коп
5) юнит-тесты
6) NCrunch

когда у всех шести нет замечаний — код красивый
Как в AirBnB — сам написал, сам и проверяй как хочешь, но чтобы через неделю было в бою :)
UFO landed and left these words here
По-моему, это краткое вольное изложение содержимого книги «Веревка достаточной длины, чтобы выстрелить себе в ногу» Ален И. Голуб.
В сущности, в контексте упомянутой книги и данной статьи красивым следует назвать просто правильно написанный код. И это так и есть, согласно принципу бритвы Оккама.
В соответствии с этим же принципом данная статья должна быть этой бритвой отрезана — она лишняя сущность.
>Ну а нужны комментарии в случаях, когда написать код хорошо по тем или иным причинам не представляется возможным.

И опять это глупое, безапелляционное упрощение ситуации с комментариями.
Вы ни разу не видели хороший код со сложной предметной областью?
То, что многие используют каменты в стиле КО, не означает их полную ненужность и ни в коем случае не говорит о плохом коде. Ситуации и проекты разные бывают, это надо просто понимать.
>Ну а нужны комментарии в случаях, когда написать код хорошо по тем или иным причинам не представляется возможным.

Вы ни разу не видели хороший код со сложной предметной областью?


И где здесь логика? Правильнее было бы спросить «Вы ни разу не видели плохой код, которому не нужны комментарии?» А так совершенно непонятно, с чем вы спорите.
Вообще-то я ни с чем не спорю)

Просто смешно выглядят потуги неопытных людей, выдать чьи-то фантазии за вселенскую мудрость.
Фишка в том, что проблема не в комментариях, а в людях.
Как говорят в одном подкасте: «Технологии великолепны, просто есть люди мудаки».

P.S. А спросил я абсолютно правильно. В хорошем коде, со сложной предметной областью, комментарии более чем уместны.
Но процитированная фраза про хороший код ничего не говорила! Она про плохой код.

А что дадут комментарии в коде со сложной предметной областью? Разве что «вывод и смысл этой формулы описан на стр. 284 такого-то тома технической документации»?
Меня сейчас начальник просит прокомментировать некоторые фрагменты (не слишком сложные, так — несколько интерполяций в рантайме). Но я не понимаю, как это сделать, чтобы хоть чем-то ему помочь.
Сейчас работаю на проекте, где не признают множественных точек выхода из функции, и как следствие операторы возбуждения исключений. Такой БОЛИ я еще никогда не испытывал. Постоянные вложенные проверки на «if ( not $error)» только для того чтоб сделать один return… И ни здравый смысл, ни сухие цифры (составлял им отчет о цикломатической сложности двух версий одного и того же куска программы написанного так как пишу я и как хотят они, где разрыв был от 1.3 до 1.6 раз) не их не убедили. Говорят мол, нас в универе учили что это плохо. Facepalm.png
Отлична книга, как раз перечитываю. Мне нравится логичный и структурированный подход Дядюшки Боба к созданию ПО. В отличие от большинства из нас, он смог после хождения по граблям сделать выводы и придумать, как уменьшить вероятности наступить на грабли в дальнейшем, не нарушая чистоты кода. Большим плюсом книги является также тот факт, что многие примеры плохого и хорошего кода взяты из реальных проектов (в основном, из FitNesse).
Если мы используем Java 8, то вместо StringUtils::lowerCase можно написать просто String::toLowerCase, чтобы не зависеть от внешней библиотеки.
Collection<String> lowercaseStrings = somestrings.stream()
.map( StringUtils::lowerCase ).collect(Collectors.toList());
procedure proc1() {
init();
do1();
}
procedure proc2() {
init();
do2();
}

proc1();
proc2();

запишем:

init();
do1();
do2();

А вот Фаулер советует прямо противоположное, и я с ним согласен. Он рекомендует вместо
for(int i=0; i<N; i++)
{
doA();
doB();
}

писать:
for(int i=0; i<N; i++)
{
doA();
}

for(int i=0; i<N; i++)
{
doB();
}

в случаях, если doA() и doB() по смыслу делают разные вещи.
Насколько я помню, в «Рефакторинг»е. Хотя сейчас полистал и не нашёл этого места в книге. Может быть ошибся с книгой.
Там основная аргументация была в том, что for(int i=0; i<N; i++){sum += коллекция[i];} и for(int i=0; i<N; i++){коэффициент *= (коллекция[i]+i*коллекция[i / 2])/N;} делают совершенно разные по смыслу вещи и не стоит лепить их в одну кучу. Тогда проще произвести выделение метода, и в целом код становится более самодокументированным и структурированным.
Особенно если вместо «sum += коллекция[i]» 10 строчек кода и вместо «коэффициент *= (коллекция[i]+i*коллекция[i / 2])/N» ещё 15 строчек. Тогда сначала разбиваем этот цикл на два цикла, а потом выделяем два метода РассчитатьСумму(Коллекция) и РассчитатьКоэффициент(Коллекция).

procedure proc1() {
init();
do1();
}

Тут похожая ситуация. Есть некое действие proc1(), которое заключается в init() и do1(). А завтра оно может заключаться уже в do1() и doSomethingElse(), но по смыслу это всё равно будет proc1(). Тут автор заметил, что есть некая procedure proc2() {init();do2();} и предложил заменить код «proc1(); proc2();» на «init(); do1(); do2();». Это оптимизация с потерей качества кода, но никак не улучшение качества кода. По смыслу тут должно быть proc1(); proc2();, а как они внутри делаются — не столь важно. Это называется инкапсуляцией.
Хотя, конечно, если они и прям буквально называются в точности proc1() и proc2(), то конечно же придётся заинлайнить эти методы с бессмысленными именами, если не удастся подобрать им более подходящие названия. Но будем считать, что автор дал им такие имена просто в силу схематичности примера.
Есть некое действие proc1(), которое заключается в init() и do1(). А завтра оно может заключаться уже в do1() и doSomethingElse(), но по смыслу это всё равно будет proc1().

Вот тут, собственно, и вопрос. Может быть, это действие proc1, а может быть, это действие do1, а proc1 создана только для удобства инициализации (потому что все всегда забывали). А еще зависит от того, насколько связаны вызовы proc1 и proc2
proc1 создана только для удобства инициализации (потому что все всегда забывали)
Значит это проблема на уровне архитектуры. И использование попеременно и на усмотрение автора то do1, то proc1 только добавят путаницы в коде.
В таком случае proc1 будет называться InitializeAndDo1(), что говорит о нарушении srp. Кроме того, тут введён лишний уровень абстракции.
Не совсем понимаю, почему автор утверждает, что тут есть дублирование кода. Да, Init() вызывается в нескольких местах в программе, ну и что? Согласен, с функцией нужно что-то делать, потому что она, по всей видимости, нарушает srp, но дублирование кода, имхо, тут ни при чём. И не всегда нужно сваливать с первого взгляда похожие части кода в одну кучу, если по смыслу они делают разные вещи.
Как вариант, тут можно или Init() поместить в конструктор, или поставить в начало функций Do1() и Do2() метод CheckInitialize(), который бы проверял, инициализирован ли уже объект и инициализировал бы его при необходимости (а-ля защитное программирование). Избавиться от функции InitializeAndDo1(), во всех местах вызывать Do1().
procedure proc1(obj) {
if (!is_valid(obj)) return;
obj.call1();
}
… Не смотря на то, что иногда такая стратегия может быть оправдана (особенно, если proc1() и proc2() экспортируются в качестве API), во многих случаях это просто засорение кода.

Лучше было бы описать, в каких случаях так делать надо, а в каких не надо, вместо того чтобы говорить, что во многих случаях так делать не надо.
Пункт 4.8 — не понял как клиенту говорится bye в последнем исправлении.
Эй-эй, это что за хренотень? :)

find_defects();

if (defects_found()) {

}


У вас что, функция хранит результат своей работы в каком-то внешнем состоянии? Может быть, вы ещё Stateful-код пишете и глобальными переменными балуетесь? А с параллелизацией никогда не имели дел?

Функция, выполняющая работу, должна возвращать инкапсулированный объект, из которого можно достать результат операции. Должно быть так:

# псевдокод
if ( find_defects().getResult().isSuccess() ) {… }

Или хотя бы так:

# псевдокод
try { find_defects(); } catch Exception $e {… }

А совсем правильно — так:

# псевдокод:
successManager.subscribe('find_defects.results.success');
failManager.subscribe('find_defects.results.fail');

function find_defects() {
# do some work…
dispatch(results)
}

Так оно будет правильно работать в распределённых системах.
В данном примере функциональщина ни при чём, это базовые принципы разработки ПО. Совершенно очевидно, что с оригинальным find_defects() пропустить ошибку настолько просто, насколько это вообще возможно — для этого даже не нужно писать код. Лучше использовать исключения. И «совсем правильный вариант» мне тоже не нравится, т.к. нужно озаботиться получением экземпляров successManager и failManager (ну и названия!) и не забыть подписаться на соответствующие события — и это только для find_defects().
В данном примере функциональщина ни при чём, это базовые принципы разработки ПО.

… которые отличаются в зависимости от используемого стиля.

Лучше использовать исключения.

Если обнаружение дефектов — это нормальная ветка сценария, то исключения бросать нельзя. В алгоритме выше дело обстоит именно так, поэтому забудьте про исключения.

А теперь представьте себе, что поиск дефектов заканчивается не просто признаком «нашли/не нашли», а одним из трех состояний (не нашли/нашли, но можно принять в работу/нашли и в работу принимать нельзя), причем для второго и третьего из них нужен список найденных дефектов (а для третьего — еще и разбитый на приемлимые и неприемлимые). Вот в ФП я это реализую легко:

type DefectInspectionResult =
    | None
    | Acceptable of defects : seq<Defect>
    | Unacceptable of acceptable : seq<Defect> * unacceptable : seq<Defect>


И что важно, когда я буду делать по нему pattern matching, компилятор предупредит меня, если я не учту вариант.

А в ООП (на примере C#)? Мне надо либо порождать дополнительный класс-результат с кучкой полей, которые все равно надо проверять, либо использовать out (который геморроен), либо просто использовать поля класса, в котором уже происходит обработка. Собственно, если у нас на один процесс обработки создается один экземпляр класса, никаких фундаментальных проблем с этим не будет. Здравствуй, рефакторинг Method Object.
Если обнаружение дефектов — это нормальная ветка сценария, то исключения бросать нельзя. В алгоритме выше дело обстоит именно так, поэтому забудьте про исключения.
Точно, это меня переклинило, и я отчего-то решил, что find_defects() должен работать как check_defects(), а забыв о семантике слова find.

А принцип «явное лучше неявного» одинаково хорошо работает и в ООП, и в ФП, и совет про возвращение значения из функции вместо установки поля класса — как раз из этой серии.
А принцип «явное лучше неявного» одинаково хорошо работает и в ООП, и в ФП, и совет про возвращение значения из функции вместо установки поля класса — как раз из этой серии.

Лучше-то лучше, но не всегда удобно.
По семантике find_defects как раз должна возвращать результат поиска (пускай пустой), а вот check_defect может изменять состояние какого-то объекта (например, заполняя его приватный defect_list), не возвращая ничего.
На самом деле автор прав лишь отчасти. Требования к «хорошему» коду очень сильно зависят от требования непосредственно к продукту, способа его использования, перспектив поддержки, перспектив развития.

Пример — надо быстро выйти на рынок, а спустя три месяца после начала написания первого релиза для первого заказчика мы уже запланировали ресурсы для переписать все по другому. Первый релиз не будет поддерживатся и фикситься — его пишем для «застолбить место». — Так и нечего тратить время на красивый код — важно писать быстро.

Другой пример — мы вообще плохо представляем предметную область или пишем а-ля библиотеку или нечто что должно строиться из кубиков — тут начинаются фабрики, стратегии, фасады, интерфейсы.

Или все тоже самое но поддерживать это будут 100 студентов — там будет совсем другое.

Или пишем нечто абстрактное в академических целях или статью про говнокод :)
UFO landed and left these words here
Ваш окончательный вариант алгоритма ремонта автомобиля
Код
procedure negotiate_with_client() {
    check_cost();
    if (!client_agree())  {
        pay_for_wash();
        return;
    }

    find_defects();

    if (defects_found()) {
        say_about_defects();
        if (!client_agree()) {
            pay_for_wash_and_dyagnosis();
            return;    
        }
    }

    create_request();
    take_money();
}


listen_client();
if (!is_clean()) {
    wash();
}

negotiate_with_client();
bye();


не удовлетворяет принципу единой ответственности и, как следствие, правильному именованию функций. Процедура negotiate_with_client() делает больше, чем говорит.

Правильней было бы сделать как-то так:
Код
function negotiate_with_client() {
    if (!is_clean()) {
        wash();
        add_washing_to_rendered_services();
    }    

    check_cost();
    if (!client_agree())
        return false;

    find_defects();
    add_diagnostic_to_rendered_services();

    if (defects_found()) {
        say_about_defects();
        if (!client_agree())
            return false;    
    }

    return true;
}


listen_client();

if (negotiate_with_client()) {
    create_request();
    take_money();
}
else
{
    pay_for(rendered_services);
}

bye();

Экономя строку на переносе скобки — вы тратите мои драгоценные минуты на поиск этой скобки по коду, когда она не подсвечивается или находится за пределами половины одного экрана.
Попробуйте найти или написать тулзу, которая будет форматировать код по тем кодстайлам, которые нравятся именно вам.
Вообще единственная веская причина для выбора конкретного варианта отступов, положения скобочек, пробелов и прочего это кодстайл конкретного проекта.

Моё личное мнение: ориентироваться лучше не по скобочкам как таковым, а по уровню отступа. С такой точки зрения перенос скобки плох, ибо скобка воспринимается отдельным выражением. А к экономии места по вертикали можно сказать, что обычно оно и так ограничено существенно в сравнении с горизонтальным. И далеко не всегда можно перевернуть монитор.
Тут можно было бы спорить, что скобка слишком мала, чтобы восприниматься выражением. Но читабельность — это прежде всего. Объясню почему: если я могу бегло читать код, не останавливаясь и не разбирая где же это начало блока и где конец, то мне не прийдётся листать постоянно вверх-вниз и достаточно одного прохода по коду, чтобы уловить суть не вникая при этом в детали реализации и не ища глазами в строка скобку начала блока — это тормозит и сбивает запоминание. В таком случае неважно насколько вертикально растянут код, когда в голове последовательно сложена картинка.
Везёт же человеку: достаточно определённым образом скобки расставить, и он может за один проход понять суть кода любого качества. Как вы приобрели этот волшебный навык?
Если стоит задача переспорить оппонента любой ценой, то и в этом случае вы скорее проиграете спор с сильным спорщиком. Если вы хотите по какой либо причине выиграть спор, где не подразумевается правильного ответа, то для начала надо осознать, что этого самого правильного ответа нет. Пусть это и кажется очевидным, но одно уже это знание даёт вам возможность посмотреть на вопрос со стороны. Например даёт возможность использовать в разгар спора «аргументы ниже пояса», «авторитетное мнение», «давление на жалость» и прочее вроде «а давайте тогда использовать golang — там же тоже скобочки не переносятся. Нет? Ну вот и я говорю, что в c# принято скобочки переносить на следующую строку».

То, о чём вы говорите есть причины субъективные, равно как и мои. Единственная причина выбирать субъективное решение — абсолютное большинство голосов в сторону этого решения.

Единственная абсолютно объективная причина использовать определённый способ расставления скобок — результат подброса монетки.

Я не спорю, а выразил своё субъективное мнение и обосновал почему. Если для вас всё иначе — можете выразить почему именно так. Ну, ещё можете минусов расставить. В любом случае, если есть реакция, значит я по-своему прав.
Если для вас всё иначе — можете выразить почему именно так.

У меня есть даже маленький пример для понимания моей точки зрения. Из двух этих листингов я выберу первый:
def add(x, y):
    print "x is {0} and y is {1}".format(x, y)
    return x + y

def add(x, y):

    print "x is {0} and y is {1}".format(x, y)
    return x + y

Я вижу начало блока не в скобке, а в его «заголовке», но при этом отчётливо вижу конец блока, будто это ruby:
def sum(x, y)
    x + y
end


Открывающая скобка для меня похожа на пустую строку, которой я разделяю код по вертикали операторы, которые идут последовательно.

Как с вашим кодстайлом читается вот такой код:
var a = 5
{
    fmt.Println("a:", a)
}
var a = 5
{
fmt.Println(«a:», a)
}

На C++ я специально для таких случаев писал
#define block
чтобы слово block можно было поставить перед открывающейся скобкой. Тогда оно смотрелось не совсем дико.
К счастью, такие ситуации встречаются не чаще, чем раз в несколько лет.
Это какое-то написание кода в стиле «итальянской забастовки». Можно извратить всё что угодно таким подходом. В вашем случае в code-style-ref будет прописано, что после определения функции пустую строчку вставлять не надо. И как в примере ниже, будет прописано, что комментарий пишется над функцией, а не между её объявлением и телом. Везде надо находить золотую середину.

// function foo
void foo()
{
}

void boo()
// function boo
{
}
Читать просто глазами и читать с помощью IDE или редактора с подсветкой скобок — это очень разные вещи. Я читаю программы, по крайней мере на Java, исключительно с помощью IDE — там ещё есть такая полезная штука как подсветка переменных. Опять-таки, по части вынесения части кода в метод с говорящим названием — IDE фактически превращает код программы в гипертекст, и читаемость повышается от наличия кучи коротких методов вместо одного длинного.
Если речь идёт только о форматировании, а не об организации кода в принципе — это вообще не проблема. Любая вменяемая IDE или даже редактор позволяют сделать «как удобно» в один шорткат :)
Только о форматировании. Организация кода — отдельная тема. Но сколько бы любая IDE не делала код красивым, такой код, например, нельзя будет закомитить (имею в виду чужой код, если пришлось исправить в нём ошибку по таске), такой код ужасно читать в mc.
Закомитить, наверное, можно — если известен тамошний кодинг стайл. Во всяком случае отформатировать по-новому перед комитом особых проблем нет.
А насчёт mc не понял. У меня всё форматирование настроено одинаково и для эклипса (основная ide), и для комоды, и для других редакторов, и в том же mc всё нормально читается.
Кто-то предложил его редактировать? Как раз наоборот, надо после своих правок вернуть всё как было.

Вопрос только в сложности этой процедуры и в том, как этот стайл описан (идеальный вариант, конечно, если он в принципе поддаётся автоматизации, и описан в виде набора ключей к каким-нибудь astyle/*indent*/*tidy/greatcode etc)
На мой взгляд писать внутри чужого кода своим стилем — плохо. По крайней мере, такой контраст зрительно мешает.
Попытка номер три :)

1) Открыл чужой исходник, нажал на кнопочку, отформатировал как тебе удобно.
2) Попоравил, отладил — нажал на другую кнопочку, отформатировал как было.

А при постоянной работе с источником можно всё это и в виде хуков организовать.
Проблема: когда «отформатировал как было», могли измениться положения отдельных элементов текста — комментарии, например. И текстовое сравнение выдало кучу мелких изменений, которых в действительности не существует.
А в каких ide возможна описанная картина — с двумя альтернативными настройками форматирования, переключаемыми по кнопочке?
Форматирование комментариев тоже вполне описывается правилами, поддающимися автоматизации. Не, конечно, есть «авторское», но это уже из разряда исключений :)

С текстовыми сравнениями — так есть же немало утилит, которые вполне позволяют настраивать всякие вещи: meld, tkdiff, diffmerge, diffuse, smartsynchronize etc. Во всяком случае всякие мелочи типа побелов/табуляций/пустых строк они отлавливают, а всё остальное — и есть значимые различия в *одинаково отформатированых* исходниках.

>А в каких ide возможна описанная картина

В любых, которые позволяют, например, подключать внешние утилиты. То есть во всех, включая просто редакторы. Ну, я про линуксы, как в других не знаю.
Поэтому надо 1) писать не в mc, а в IDE с настраиваемым стилем 2) на всём проекте унифицировать стиль.
не всегда есть IDE под рукой, чаще Sublime какой-нибудь. Да и если я открыл в IDE, то она знает мой стиль, но не знает стиля, которым написан открываемый код. Обратно вернуть к стилю не получится.
Для своих проектов стиль почти всегда унифицируется — это правило перед началом работы.
О способе форматирования должен знать сам проект и передавать его вместе с исходниками. Либо в виде общеупотребительных названий (например, для сей — «BSD/Allman», «GNU», «K&R», «Whitesmiths»), либо в виде ключей к стандартным утилитам, начиная с банальных *indent.
А уж повесить вызор форматёра с нужными ключами на кнопку — это мало кто нынче не умеет :)
За свою практику я вывел одно жёсткое правило: когда код написан разборчиво (а именно: с пробелами вокруг операторов, переносом скобок, пробелами между атомарными блоками кода и без сокращений в именах переменных), то сделать такой код говно-кодом тяжело, в то же время в таком коде можно легко найти даже неправильную логику лихого школьника. Приучайте писать код читабельным и красивым.
Для того, чтоб код был читабельным и красивым и писать его было при этом легко — нужны IDE с автоматическим настраиваемым форматированием и рефакторингом плюс привычка «причёсывать» код.
ох, как сложно найти баланс между Дартом Копипастиусом и Дартом Ифусом…
еще и Дарт Тринариус рядом ручки потирает…
Оператор return не позволяет вынести этот участок кода в отдельную процедуру.
Вы так интересно пишете, что кто то де будет читать мою программу? Может и будет конечно, но обычно не правильный класс просто выбрасывается и на его место пишется новый с теми же public. Все, что за рамками public просто никого не интересует. Все красивости только в интерфейсе. Внутри разбираться некому, некогда и не зачем. Может кто то и посмотрит в качестве примера при переписывании. У каждого свое видение и логики и красивости. Это где такое интересное место, чтобы кому то смотреть внутрь кривого класса?
Как минимум, придётся читать ваш код, чтобы бы понять, а что, собственно, реализовывать в новом классе, перед тем как выбросить ваш. Тестов же, покрывающих все граничные случаи нет? Или, может, имена точно описывают реализацию? Включая такие нюансы, как что возвращает метод типа find, если ничего не находит — null, false, отрицательный код ошибки, пустой массив или ещё что?
С большим интересом прочитал статью.
Рекомендую желающим сопоставить предлагаемые в статье правила с эргономическими правилами визуального языка ДРАКОН. bit.ly/1UQ4zuU
Вы обнаружите большое (хотя и не полное) сходство

С уважением,
Владимир Данилович Паронджанов, Москва, Роскосмос
Mobile: +7-916-111-91-57
Viber: +7-916-111-91-57
E-mail: vdp2007@bk.ru
Skype: vdp2007@bk.ru
Website: drakon.su
Webforum: forum.drakon.su
Only those users with full accounts are able to leave comments. Log in, please.