Pull to refresh

Comments 76

Подобные статьи воспринимаемы, если сопровождаются парными примерами «хорошо/плохо».
Согласен. Правда мне кажется, что всегда найдется разработчик, который на ваше «хорошо» поставит резолюцию «плохо», что грозит перерасти в бесконечные споры.
Я думал об этом, но кроме раздела «Удобочитаемость» не нашел куда их воткнуть, а там и так одни прописные истины перечислены, на каждую по паре десятков статьей написано уже, если не больше.

В остальных случаях — в отрыве от контекста примеры больше споров вызовут, чем пользы. Скажем, в разделе про «простоту» кода даже и нельзя написать, что это «хорошо», а вот это «плохо», например про класс из двух слабосвязанных методов по сравнению с просто двумя функциями. Вроде и так и так можно сделать, а с классом даже «правильнее», если рассматривать сферический код в вакууме, как по учебникам.
На деле же, суммарную сложность проекта такие «правильные» конструкции могут повысить в несколько раз, ну и как следствие — сложность поддержки, рост числа ошибок и т.д.
Писать хороший код с нуля — очень просто. Рассказал бы кто, как писать хороший код в ентерпрайзе с многолетними слоями легаси, велосипедами на дизельном топливе и местами нулевой/чрезмерно высокой абстракцией.
Так я рассказываю — упрощайте код и избавляйтесь от дубликатов каждый раз когда появляется хоть малейшая возможность, любым способом, даже если он вам кажется «кривоватым» или «примитивным». Вода камень точит. Иначе и правда надо все сносить и с нуля, что во многих случаях малореально.
А как же простота? Начнете избавляться от всех дубликатов — неизбежно усложните проект.
Это дубликаты собственно и усложняют проект. Когда вносите изменения в один а в некоторые забываете начинается магическое поведение программы.
Это если совсем копипаста. Такое случается, но не часто. Гораздо чаще бывают случаи когда два куска кода работают «почти одинаково», есть мелкие детали их отличающие. Вот в этом случае и возникает усложнение при устранении дублирования.
Тогда это не дублирование кода, раз есть детали различающие функционал.
Дублирование кода, это когда дублируется один и тот же функционал в нескольких местах кода, скорей всего даже не копи-пастой и даже код может значительно отличаться. Например, вычисление количества дней в месяце указанного года. Задача может быть решена разными путями, но если в проекте есть две функции которые решают эту задачу хоть и разными способами — это есть дубликаты. Хуже когда не знаешь о таком дублировании и в одном из них будет проявляется редкая ошибка — выводишь календарь в нем одно, а в отчете расчет ведется исходя из другого количества дней в феврале.
Представь что есть две реализации быстрой сортировки в проекте. Первая сортирует числа по возрастанию, и берет средний элемент в качестве опорного. А вторая сортирует объекты по ключу по убыванию и берет случайный элемент в качестве опорного.

Отличие двух сортировок буквально в 3-4 строках. Но эффективно обобщить не плучится. Придется изобретать функторы сравнения и выбора элементов. Вызов сортировки станет многословным. В итоге приведет к усложнению.

Хорошо если библиотека дает такие функции, а если это не сортировка, а более сложный и изменчивый код?

Увы баланс простоты кода и DRY — непростая вещь.
Это два разных алгоритма, и по функциональности в том числе, никакого дублирования.
Я тут писал уже habrahabr.ru/post/206868/#comment_7129502 — правильное устранение дубликатов не усложняет код. Если у вас есть конкретный пример кода, где устранение дубликатов может привести к неоправданному усложнению — можете сюда выложить или ссылку дать. Вместе подумаем над этим, в качестве разминки для мозга.
Почитал. Согласно терминологии статьи, при удалении дубликатов мы повышаем «структурную сложность», зато сильно понижаем «количественную» и «сложность изменений» (нет проблем с несогласованной правкой дубликатов), а также частично «алгоритмическую сложность» (за счет вынесения дублирующихся частей алгоритмов в абстракции в виде функций и методов). Моя субъективная оценка — оно того стоит, т.к. сумма трех понижаемых сложностей выше, чем одной повышаемой. В итоге общая сложность проекта уменьшается.
У вас может быть другая субъективная оценка. Доказать свою правоту математически, измерив количественно повышение/понижение сложности, я не берусь.
зато сильно понижаем… «сложность изменений»

Не факт, кстати. Меняя просто кусок кода мы может быть уверены, что затронет это только окружающий код (если нет глобальных переменных и т. п., в общем кусок представляет собой чистую функцию). А меняя функцию нам нужно проверять все места её вызова. Как по мне, то оно того стоит, но математически доказать это я не берусь :)
Угу. Слишком дедуплицированный код как правило требует постоянного рефакторинга для внесения изменений в конкретные части бизнес логики. В то же время унылый говнокод с кучей копипаста легко исправляется для 1го конкретного случая. Не то чтобы я за то чтобы плодить говнокод, но компромис тут есть.
Зачем вы мне пишите то, о чем я сам писал в соседней ветке обсуждения habrahabr.ru/post/206868/#comment_7133274 в ответ на ваш же комментарий?
Мне начинает казаться, что я сам с собой веду беседу :)

Если резюмировать мое мнение по этому вопросу — дубликаты всегда выносим в функцию/метод, т.к. проблем от проверки мест вызова функции меньше, чем от проверки изменений и возможных ошибок при несогласованном изменении дубликатов. А если у нас неповторяющийся кусок кода, который теоретически можно обособить и спрятать за абстракцией в виде функции — серьезно подумаем, стоит ли это делать, т.к. в этом случае выигрыш далеко не столь очевиден.
DRY VS KISS вообще неразрешимая делема. В интернетах по этому поводу можно почитать нехилый такой холивор.
рефакторинг через тестирование?
Точнее, рефакторинг вместе с тестированием.
Смерть через туки-туки?

Просите, не удержался…
Правда хотите?
Думаю, не только я смогу выступить в роли К. О., тщательно разжевав слово «рефакторинг» :)
Об этом есть замечательная книга M Feathers «Working Effectively with Legac Code».

В целом подход такой. Возникает необходимость изменить легаси:
1) сделайте простейшие преобразования для dependency injection
2) напишите unit тесты для того, что собираетесь менять (это и отладку облегчит — не надо запускать и проходиться по всему приложению)
3) выполняйте стандартные рекоммендации KISS локально для той области где производите изменения.
4) если что-то сломалось в результате ваших изменений — напишите unit тест воспроизводящий проблему и сделайте так, чтобы он проходил.

проверено — через некоторое время код становится гораааздо приятней для понимания и изменения. Главное не увлекайтесь изменениями — меняйте только ту область которую требуется менять в соотвтествии с текущей задачей — иначе начнете запарывать сроки.
Dependency injection и тесты спасают, это да. Но вот

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

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

В тестах используете конструктор, вытаскивающий зависимости наружу, в старом коде использование класса не меняете до тех пор, пока не наткнетесь на код, где происходит создание объекта (т.е. по мере натыкания на плохой код — проводите рефакторинги). И так итеративно до тех пор, пока не окажется что старый конструктор нигде не используется. В плохо написаном коде это случается довольно быстро — т.к. объекты обычно очень громоздкие (все в одном — нарушение single responsibility) и мест, откуда они используются не так уж и много.

Если метод, над которым работаете — слишком огромен и сложен, попробуйте авторефакторинги по вытаскиванию методов из кода — среды сейчас довольно неплохо это делают.
Самый правильный путь с легаси кодом – это «понять и простить», ну а если простить не получается – порефакторить. Я когда встречаю большое кол-во легаси стараюсь делать структурные диаграмки которые помогают понять что к чему там работает. Если вижу г…код – исправляю. Также иногда бывает подход в корне неправильный, тогда выдираю всю подсистему и пишу как надо. А еще, иногда помогает написание тестов на чужой код – так начинаешь понимать, работает ли система так, как ты ожидаешь. Но основная проблема с легаси (как и везде) – временные рамки. От вас требуют новый код обычно, а не фиксы на старый.
Понятия «хорошего» и «плохого» кода субъективны. Рассуждать о них — это пустая трата времени.
Код нужно делить на «код с недочётами/ошибками» и «код без недочётов/ошибок» и никак иначе.

Ваша статья, в общем — это попытка научить других людей писать код так, как нравится Вам.
А Вы считаете, что нельзя писать код без ошибок?
Я считаю, что невозможно написать более-менее большой проект и утверждать что в нём 100% нет ошибок.
Вы всё идеализируете. По вашей логике любой проект можно назвать говнокодом.
Постойте. Это ваша логика. В моей логике нет аксиомального предположения, что код содержащий ошибки это говнокод.
И идеализируете вы, считая, что бывает хороший код, который не содержит ошибок и плохой код, который их содержит.

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

Во-вторых, я написал, что наличие ошибок/недочётов есть критерий, по которому нужно судить о качестве кода, а не по субъективным понятиям — «хорошо-плохо написано».

В-третьих, какого чёрта, Вы давите на то, что «более-менее большой проект будет содержать ошибки»? Что это за бабский аргумент?
Что тогда значат ошибки/недочёты?
Использование алгоритма с экспоненциальной сложностью вместо алгоритма с линейной является ошибкой/недочётом?
А когда в каждой функции дублируется какой-то функционал, который можно вынести в отдельную функцию, и для того что бы внести изменение надо потратить неделю вместо 2-х часов работы это ошибка/недочёт?

В-третьих, какого чёрта, Вы давите на то, что «более-менее большой проект будет содержать ошибки»? Что это за бабский аргумент?

И это, осторожней на поворотах.
Ошибка — неправильно работающий код (участок кода), т.е. не выполняющий необходимых действий.
Недочёт — не оптимальный код (участок кода) для данной области приложения.

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

Вы написали «более-менее большой проект будет содержать ошибки». И что? Это и ежу понятно. Но это не значит, что нельзя построить градацию качества кода по количеству ошибок.
Читаем дальше… От Вас следует следующий такой же глупый аргумент-вопрос про то, какой код нужно считать оптимальным.
Ответ: тот который подходит под условия области приложения. На очевидный вопрос очевидный ответ.
Например, дублирование кода в ряде условий (многие скриптовые языки) более эффективно, чем сведение к единым функциям. В других условиях, сведение к функциям более эффективно. То есть всё зависит от области приложения.
А Вы считаете, что нельзя писать код без ошибок?

Вы написали «более-менее большой проект будет содержать ошибки». И что? Это и ежу понятно. Но это не значит, что нельзя построить градацию качества кода по количеству ошибок.

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

Градация по количеству найденных (и это важно) ошибок это одно.

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

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

Ну а ваши бесконечные попытки задеть меня выглядят жалкими для взрослого человека.
— Код может быть без ошибок.
— Если в коде нет ошибок, то он не обязательно единственно верный.
Или Вы хотите сказать, что в любом «hello world!» можете найти ошибки?

Вы продолжаете в аргументацию в том же бабском стиле.

Вам же выше сказали — «жалкими».

Остальным, кто еще не читали классику — «Факты и заблуждения профессионального программирования» и «Мифический человекомесяц» — марш читать, а то так и будете считать, что все субъективно и все просто пытаются заставить вас писать так, как нравится «им».
Во-первых, я не говорил, что всё субъективно, а на оборот привёл объективные аргументы.

Во-вторых, походу дела, Shoonoise действительно «баба», как только я ему задали вопрос, на который ему нужно отвечать против своих доводов, то он смело меня проминусовал и ушел в игнор. Ну, чем не баба?
У вас, очевидно, какие-то проблемы с женщинами. Впрочем, к делу это явно не относится.

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

А Вы сами можете привести алгоритм оценки качества кода не привязанный к ошибкам? Скорее всего, тоже нет.
Я, подобно автору и вашему предыдущему оппоненту, считаю, что главный критерий качества кода — его отзывчивость к изменениям. Ошибки — это человеческий фактор, а не фактор кода, результат дурной аналитики задачи, проблема в коммуникации между постановщиком задачи и разработчиком. Что угодно — только не проблема непосредственно кода.

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

Увы, этот критерий работает лишь пост-фактум. Впрочем, многие опытные разработчики способны предсказать, что тот или иной код долго не протянет, кинув на него один взгляд. И наличие/отсутствие ошибок никак не влияют на это, если честно.
Я не могу понять, как Вы связываете «время работы кода», его отзывчивость к изменениям и его качество(?).

По вашим словам, качественный код может без потери работоспособности может претерпеть многочисленные изменения на протяжении какого-то времени. Но такой же подход можно использовать и в отношении моих аргументов. Мол, качественный код сможет сохранить работоспособность, как можно дольше при изменениях, если в нём изначально меньше ошибок. Ошибки имеют свойство накапливаться и когда их станет достаточно много, код станет неработоспособным.
Что Вы скажете на такой поворот?

Потом, ваше заявление «наличие/отсутствие ошибок никак не влияют на это [как долго протянет код], если честно» допускает, что «качественный» код может содержать многочисленные ошибки. Вам это кажется нормальным?
Еще раз, наличие ошибок в качественном коде — следствие человеческого фактора. Так что да, это нормально, просто кто-то кого-то недопонял. Опять же, вы сами подтвердили, что не можете описать алгоритма выявления всех ошибок в коде. А значит, любой код, даже тот, который вам кажется безошибочным, может эти самые ошибки содержать.
Конечно, речь идет не о hello world'е (хотя, вы на C писали, например? думаете, сможете написать там идеальный хеловорлд?), а о чем-то посерьезнее.

Мол, качественный код сможет сохранить работоспособность, как можно дольше при изменениях, если в нём изначально меньше ошибок.

Речь не о работоспособности. Речь о способности выдерживать изменения.

Накапливающиеся ошибки? Это еще как? Это когда я пишу код и не правлю баги, потому что fuck you that's why? Не понимаю, увы.
«наличие ошибок в качественном коде — следствие человеческого фактора»
«Накапливающиеся ошибки? Это еще как? Это когда я пишу код и не правлю баги, потому что fuck you that's why? Не понимаю, увы.»
— Я, откровенно, удивляюсь аргументации всех спорящих со многой. Мол, ошибки в коде не влияют на качество, но в качественном коде их быть не должно. Это какой-то разрыв мозга.

Давайте разложим всё по полочкам.
Код можно условно разделить на абстрактные 3 составляющие: алгоритм, написание (синтаксис и семантику) и стиль.

Какие бывают ошибки? Бывают семантические и синтаксические ошибки, т.е. ошибки написания команд, описки и неправильные данные. Бывают алгоритмические ошибки — выбор не оптимального или неверного (дающего неправильный результат) алгоритма.
Стиль написания кода — понятие изначально субъективное. Каждому нравиться свой стиль написания кода.
В итоге из объективных доводов остаются только ошибки.

Вопрос: что я не учёл? Где спрятан критерий качественного кода? Хотелось бы получить ответ от Quilin, marapper и Shoonoise
Ну, то есть, вы считаете плохим кодом тот, который работает неправильно или не работает вообще. Мы все тут тоже считаем его плохим кодом, хотя некоторые не считают его кодом вообще. Но еще мы считаем плохим кодом тот код, который сложно читать, в который трудно и долго вносить изменения. Ваш подход тоже неплохой, наверное. Но он позволяет отвечать критикам кода, которые уверяют вас, что где-то код попахивает bad design'ом, мол, отвали, все работает.
Весь смысл в том, что вы акцентируете на симптомах, а не на причинах, которые их вызывают.

Если говорить о программах как таковых — никого не волнует, сколько строк кода там (да, было время, когда за SLOC платили, но идея быстро изжила себя). Для бизнеса/пользователя важно, чтобы было реализовано достаточное количество функционала, доступ к нему был максимально простой, скорость исполнения высокой, и в процессе не возникало ошибок (здесь можно заметить, что можно разделить собственно ошибки на много видов: неясности вроде ошибок с цифровыми кодами вместо сообщений, несогласованности при ошибках в интеграции систем, просто ошибки — например, возраст может быть отрицательным, ошибки критические — когда пользователь теряет время, деньги или не получает результата вовсе). Т.е. возможных метрик (а ошибки могут быть метрикой кода — я, например, не зря привел в пример программы NASA, количество ошибок в коде которых минимальнейшие из всех софтверных компаний) куда больше, чем просто «количество ошибок». Кроме того, количественно-качественное отношение здесь сложно вывести, т.к., на самом деле, нужны дополнительные инструменты в метрике. Справедливости ради, некоторые отчеты в Джире я так и смотрю — количество багов на период, разбитых по уровню сложности (тривиал-критикал) и влиянию на систему, дополнительно вводя срезы по компонентам.

То есть ошибки как бы могут говорить что-то о коде. Но (и если вы читали книги, о которых я упомянул выше), вы бы понимали, что: а) любое приложение имеет баги, т.к. 100% покрытие автоматическими/ручными тестами невозможно (при сложности, стремящейся к увеличению), б) можно подсчитать количество обнаруженных багов, но не скрытых, в) невозможно прогнозировать количество багов в будущее на основе имеющегося количества (т.к. у нас метрика — окончательный результат, а не что-то общее и для блоков, которые будут написаны позднее и т.п.).

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

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

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

На более высоких уровнях,- бизнеса, архитектора, лида, прожект-менеджера — код должен не только работать.
Аргумент принят. Спорить больше не буду. Достаточно было первой фразы.
Количество ошибок лишь одна из составляющих качества кода. Важная, но не определяющая.
Понятия «хорошего» и «плохого» кода субъективны. Рассуждать о них — это пустая трата времени.
Понятия «простоты кода» и «DRY» — полностью объективны. Простота/сложность — это количество языковых и смысловых конструкций, а также сущностей, которыми оперирует программа. Оно может быть избыточным (что плохо), или достаточным (что хорошо). С DRY все еще проще — если код дублируется (причем я не говорю только про посимвольное совпадение, естественно, речь про совпадение функциональности) — это всегда плохо. Потому что увеличивается количество сущностей, нужно вносить правки в нескольких местах, резко повышается вероятность ошибки (в одном месте поправили, в другом — нет).
Насчет «удобочитаемости» — это некоторое общественное соглашение. Как правила литературного русского языка например, или этикета. Математически они не объясняются, просто вот так люди договорились.
Что считать избыточным, а что достатчным? Так что простота и чистота кода — это понятие, всё-таки, субъективное. Но рассуждать о них в любом случае можно и нужно.
Простота и DRY противоречат друг другу иногда. Что делать в этом случае?

«Концептуальность» вообще слабо формализуемое понятие, глядя на код как понять насколько он «концептуален»?

«Удобочитаемость» это не то же самое, что «удобопонимаемость».
Особенно доставил последний пункт. Если есть неочевидный код, то почему бы его не написать чтобы он стал очевидным, вместо написания комментариев?
Простота и DRY противоречат друг другу иногда. Что делать в этом случае?
Я про это писал в статье — достижение DRY простыми и очевидными методами помогает в большинстве случаев. Если же выходит так, что без объектов, динамически генерируемых через цепочку фабрик согласно XML-конфигурации, дублирования не избежать, надо задуматься — а все ли вообще хорошо с архитектурой? Т.е. на каком-то этапе этот процесс перестает быть линейным и требует подняться на уровень выше.

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

«Удобочитаемость» это не то же самое, что «удобопонимаемость».
Особенно доставил последний пункт. Если есть неочевидный код, то почему бы его не написать чтобы он стал очевидным, вместо написания комментариев?
Не любой фрагмент кода можно сделать очевидным для каждого, кто его будет смотреть. Бывает объективная сложность описываемого алгоритма или бизнес-процесса, которую не снизить за счет правильного подбора языковых конструкций. Вот повысить можно запросто :)
Если же выходит так, что без объектов, динамически генерируемых через цепочку фабрик согласно XML-конфигурации, дублирования не избежать, надо задуматься — а все ли вообще хорошо с архитектурой?

Что-то вас из крайности в крайность кидает. Надо смотреть на средний случай. Есть много «похожего» кода, но в каждом свои детали. Код простой и линейный. Можно дублирование устранить, сделав функцию\объект с кучей параметров, которые покрывают все случаи. Это убирает повторения, но делает код сложнее. Какой вариант лучше? Почему в статье об этом не сказано?

Если концепция общепринятая, она понятна и так

Есть список «общепринятых» концепций?

Не любой фрагмент кода можно сделать очевидным для каждого, кто его будет смотреть

Если можно написать комментарий, то можно и код написать по структуре комментария и он будет читаться не хуже самого комментария. Если же сам алгоритм сложен, то лучше всего правильно назвать метод и в remarks вставить ссылку на описание алгоритма.
Есть много «похожего» кода, но в каждом свои детали. Код простой и линейный. Можно дублирование устранить, сделав функцию\объект с кучей параметров, которые покрывают все случаи. Это убирает повторения, но делает код сложнее. Какой вариант лучше?
Код не становится заметно сложнее при правильном подходе. Например, если у нас после попытки убрать дублирование появляется длинная функция со switch внутри, который содержит в своих ветках практически исходные куски кода — это неправильный подход. Функция должна по возможности оперировать параметрами напрямую, а не на уровне условного goto. Т.е. cube(n) делает внутри себя return n * n * n, грубо говоря, а не if(n == 2) return 8, if(n==3) return 27
Если же не получается так, значит фрагменты кода слишком сложны для унификации, и убирать дублирование надо на уровне их отдельных частей. Либо посмотреть на архитектуру в целом — почему получается такая ситуация.

Есть список «общепринятых» концепций?
Есть — на уровне ЯП, отрасли, команды в которой работаешь и мира IT в целом — в качестве примеров я уже приводил автолоад классов и UNIX-конвейеры.

Если же сам алгоритм сложен, то лучше всего правильно назвать метод и в remarks вставить ссылку на описание алгоритма.
Ну так ссылка это и есть комментарий.
Есть много «похожего» кода, но в каждом свои детали. Код простой и линейный. Можно дублирование устранить, сделав функцию\объект с кучей параметров, которые покрывают все случаи. Это убирает повторения, но делает код сложнее. Какой вариант лучше?


В общем случае лучше, имхо, делать так:
— то, где детали ещё/уже не различаются, выносим в отдельные «подфункции» или приватные методы
— для каждого случая пишем свою функцию, где вызываются «подфункции»
— смотрим можно ли параметризировать эти функции в одну. Грубо говоря, пытаемся сделать универсальную функцию с пятью параметрами, а для неё пяток оберток с одним-двумя параметрами.
Итого:
Было две функции, пусть по 20 строк, которые отличались в 3-4 строках
Стало 1 функция к куче параметров, и ветвлений, 4-5 подфункций, еще 4-6 оберток с малым кол-вом параметров.

Суммарное количество кода уменьшилось незначительно, сложность возрасла в разы.

Для примера возьмите две быстрые сортировки, как писал тут: habrahabr.ru/post/206868/#comment_7131792
И сравните метрики до и после.
вот пример противоречия из недавнего проекта. (C#)
есть некий код одинаковый в двух независимых сборках. сборки абсолютно независимы, распространяются с MEF.
если код скопировать -> дальнейшее изменение этого кода может повлечь проблемы
если вынести в отдельную зависимую сборку -> появляется дополнительная зависимость
если вынести в существующую зависимую сборку -> потребуется обновление многих прочих сборок
если разделить файл с классом на две сборки -> тоже проблема, т.к. проекты пишутся разными разработчиками.

получается, проще всего скопировать код -_-
Ну вообще в статье шла речь про код в рамках одного проекта, а в вашем случае это скорее «подключаемая библиотека» (использую такой термин, чтобы не привязываться к конкретному языку), если код одинаков, либо просто «повторное использование наработанного решения», если он все-таки отличается в разных проектах. Если же подключение библиотеки приводит к проблемам типа Dependency_hell, то это уже не к коду вопрос, и решать его надо на другом уровне.
Понятное дело, если всю систему за пару дней не изменить, а релиз завтра — можно просто скопировать код, да. Но надо помнить, что можно сделать лучше.
я встречал еще один способ: писать код в одной из сборок, а во вторую эти же файлы добавить не как копии, а как «линки» — тогда и зависимостей нету и код не дублирован
Лучше написать спагетти-код, чем спагетти-архитектуру ©
Что есть спагетти-архитектура?
Судя по иллюстрации, Джеки Чан на старости лет решил заняться программированием.
Джеки Чан привык по жизни все делать качественно, решил и в новой для себя отрасли сразу разобраться, что к чему.
В комментариях к статье про качественный код должна быть упомянута книжка Clean Code супер-полезная для всех, кто хочет писать более понятный код.

В ней, кстати, рекомендуется писать короткие методы — хоть и из 3-х строчек — такие методы должны делать ровно одну вещь. Имя метода должно явно говорить, что это за вещь. При этом, на мой вкус, не надо бояться названий более чем из 3-х слов, если ситуация того требует. Тот же принцип, как у автора статьи — максимально короткое название, но не короче, чем нужно чтобы ясно описать что делает метод или содержит поле.
CFSocketCreateConnectedToSocketSignature — вполне читаемо по мне, и такое везде в CoreFoundation. А уж про Objective-C я промолчу.
Не оборачивайте в функцию код из 3-х строк, который нигде более в приложении не используется повторно (и не будет использоваться в ближайшем будущем, а не через 150 лет).

В корне не согласен. Например, три (значащие) строки
$sum = 0;
foreach ($array as $item) {
  $sum += $item;
}

лучше заменить на одну функцию array_sum($array) (если в библиотеке её не было бы), даже если повторного использования 100% не будет — читающему код не нужно вникать в детали цикла, пока не придется разбираться в его деталях.

решайте проблему дублирования кода сразу после ее обнаружения (но не раньше!)

Но не стоит забывать, что вынос кода в функции это не только способ уменьшить дублирование кода, но и способ уменьшить его сложность на данном уровне абстракции — абстрагирование главный инструмент программиста. В примере выше обычно нам не важны детали реализации суммирования, достаточно понять, что оно происходит, что лучше передаётся именем функции, а не лаконичной конструкций пускай даже типа array.each(sum), не говоря о цикле
Есть несколько очень похожих блоков кода из 5-10 строк, отличающихся лишь парой условий и переменных, но вы пока не уверены, как лучше завернуть их в функцию? Используйте циклы.

Заверните их в две разные функции.
Не знаете, в какой класс/объект положить код нового метода? Просто создайте функцию и положите ее в подключаемую библиотеку до тех времен

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

Используйте наиболее лаконичные из доступных стандартных конструкций языка. Например, тернарный оператор вместо if-else для блока кода, помещающегося в одну строчку. Не используйте операторы нестандартным, неизвестным основной массе разработчиков образом

Спорный совет :) Лаконичная конструкция может быть стандартной, но вот основная масса разработчиков её с ходу не прочитает. Чаще всего затрудняют чтение: большая вложенность (особенно с тернарным оператором ;), манипулирование точным знанием приоритетов (кроме разве что арифметических операций) и порядка вычислений (приоритеты и порядок вещь стандартная, но интуитивно читаются слева направо) без использования скобок (да и с ними не всегда очевидно), неявные преобразования типов, использования побитовых операций не для битовых масок и без явной необходимости оптимизации.

P.S. известную всем с детства таблицу умножения
Лайфхак: если хотите чтобы ребенок выучил таблицу умножения быстрее — сделайте ему таблицу, начинающуюся не с 2х2, а с 1х1: в отличии от всем известной с детства такая таблица не прячет геометрического (и матаналитического :) смысла умножения — площадь прямоугольника (интеграл :). Операцию умножения из неё можно вывести даже не умея складывать, а лишь умея считать натуральные числа — просто посчитать сколько квадратиков (предполагается, что она строго квадратная) входит в соответствующий прямоугольник.
Я поясню насчет функции — в том примере который вы привели, типа array_sum, конечно можно и нужно выносить, т.к. это универсальная, библиотечная по сути функция. И как раз маловероятно, что она будет использована только один раз во всем приложении.
Речь про неповторяющийся кусок бизнес-логики, который специфичен для того конкретного участка кода (функции, метода, не важно), который мы пишем/читаем в данный момент. И проблема с функцией в том, что она обычно (хотя зависит от языка конечно) имеет глобальную область видимости. Соответственно, если мы оформляем фрагмент в функцию, мы автоматически создаем вопрос для читающего код — а где эта функция еще используется? Если мне надо ее поменять, не сломается ли программа где-нибудь еще? Конечно, есть IDE, тесты и т.п., но эту проблему можно просто не создавать, чтобы потом не приходилось ее решать.

Насчет двух функций — то же самое, нам надо устранять дублирование, а не просто переносить его в другое место. Было 2 похожих куска кода, стало 2 похожих функции — смысл?

Насчет тернарного оператора — я не первый раз уже слышу про то, что какой-то там средний программист не может это прочитать и воспринять, и каждый раз тихо фигею. Это что, какой-то «know how» прием или хакерский финт — тернарный оператор? Или все-таки стандартная задокументированная конструкция языка? Такие тезисы мне напоминают разговоры пятилетней давности о том, что «у пользователя в браузере может быть отключен яваскрипт». Может быть отключен, да, только он тогда не сможет 90% сайтов пользоваться, и это — нормально.

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

Да, есть такой момент, что функцию нужно менять осторожно. Но оно того стоит, по-моему. Если мы можем в двух-трех словах объяснить, что происходит в нескольких строках кода, если основной результат единственный, то зачем оставлять эти строки в коде, а не заменить их на эти два-три слова, скрыв детали реализации и оставив только чистую семантику в виде этих двух-трех слов? В общем, когда я вижу функцию строк из 50 в коде доступном до редактирования и де-факто, и де-юре, то ищу причины не разбивать её на части строк по 10 максимум. Чаще всего не нахожу :)

Насчет двух функций — то же самое, нам надо устранять дублирование, а не просто переносить его в другое место. Было 2 похожих куска кода, стало 2 похожих функции — смысл?

Так хотя бы проще анализировать, реально ли есть дублирование. Одинаковы ли сигнатуры, например и помогает абстрагироваться от семантики модуля. Скажем, если в одном месте суммируются данные из массива $income_orders, а в другом $outcome_orders, то код синтаксически неодинаков, но если абстрагироваться от имен переменных, то семантически одинаков. Ну и для меня выделения кусков кода в функции главной целью имеет не устранение дублирования, а облегчение читаемости.

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

Конструкцию типа
$value = isset($value) ? $value : 'default';
, конечно, любой должен прочитать не спотыкаясь, но вот уже что-то типа
$value = isset($value) ? is_valid($value) ? $value++ : $value-- : 'default';

с ходу не осилишь, особенно если нюансы подобных конструкций в основных языках отличаются. Скажем, в одном выполняться могут обе ветви, просто отбрасывая ненужный результат, а в другом выполняться может только нужная ветвь, даже не пытаясь получить ненужный результат.
А вот авторы Growing Object Oriented Software Guided by Tests рекомендует сразу же создавать классы, при малейшем намёке на отдельную сущность, даже если там будет на первых порах один метод. Как правило боязнь создания «лишних» классов это какая-то дедовская болезнь, проистекающая от полного непонимания SOLID и ООП вообще. И да, с таким подходом никогда не возникает сотен классов с одним методом внутри. Навигация никогда не становится хуже.
Насчёт методов из трёх строк. Всегда (почти) предпочту метод из трёх строк методу из 20. Чаще всего, когда ты читаешь чей-то код, ты пытаешься понять смысл того, что делает этот код, а не детализированную имплементацию, поэтому, если программист писал множество мелких функций, то функции верхнего уровня читаются действительно как «хорошо написанная проза», а если нужны детали — всегда можно заглянуть в детали. Не надо меня кормить деталями, если я вас об этом не просил, я хочу иметь выбор: просто понять быстро смысл написанного или спуститься для изучения деталей.
Насчёт методов из трёх строк. Всегда (почти) предпочту метод из трёх строк методу из 20. Чаще всего, когда ты читаешь чей-то код, ты пытаешься понять смысл того, что делает этот код, а не детализированную имплементацию, поэтому, если программист писал множество мелких функций, то функции верхнего уровня читаются действительно как «хорошо написанная проза», а если нужны детали — всегда можно заглянуть в детали.
У меня вот наоборот как-то получается — понять смысл написанного обычно просто, а дальше нужно в коде что-то поменять, потому что мы его не ради развлечения же читаем, а с какой-то целью. И вот на этом этапе — слишком много маленьких методов и функций создают проблемы, т.к. надо в каждую заглянуть и потом еще половину из них поменять. Что тянет за собой кучу зависимостей из тех мест, где эти функции еще используется. Либо надо все выкидывать и переписывать метод заново.
Поймите правильно, я не за методы по 100-500 строк, которые пытаются делать все на свете. Но и не за методы, в которых вызывается 30 других методов, которые внутри себя вызывают еще в сумме 50-100 функций по 1-3 строки. Я за разумную середину.
А вот авторы Growing Object Oriented Software Guided by Tests рекомендует сразу же создавать классы, при малейшем намёке на отдельную сущность, даже если там будет на первых порах один метод.


Тут надо учитывать, что на Java невозможно создать «просто функцию», которая не привязана ни к какому классу. Если бы такая возможность была в языке (или если бы авторы писали код на каком-нибудь Python), не думаю, что они бы так настаивали на создании классов на каждый маленький кусочек кода. А так получается, что код всё равно должен лежать в каком-то методе какого-то класса.
Ну вот PHP вроде позволяет, но стараюсь просто функции не создавать, прежде всего из-за того, что нужно будет вручную управлять зависимостями (вставлять require_once в каждый файл, где функция используется или инклудить каждый раз независимо от того нужна она или нет). То есть возможность вроде есть, но пользоваться ею не хочется.
Sign up to leave a comment.

Articles