Comments 46
Я не спорю с вами — это мое имхо что написано, с чем я собственно полностью согласен на уровне — сначала идет definition того что делается, а уже другой очередью идет реализация — как бы разные процессы мыследеятельности. Вот об этом «заборе» мне кажется он и пишет — а что за забором это уже вообще не имеет значения без точечного контекста.
Я представляю себе как бы два разных читателя кода. Один — босс, ему вечно некогда, он вечно торопится. Ему надо только знать, что сделано. И не важно — как. А второй — технарь, вот ему важно знать, как именно сделана конкретная вещь. Но не важно, в рамках какой большой высокоуровневой задачи. От неё вообще должно быть как можно меньше зависимостей.
Я знаю, в разных умных книжках эти вещи по разному называются. «Бизнес-логика» и «низкоуровневая реализация», или ещё как-нибудь. Но практически все авторы имеют ввиду если не одно и то же, то нечто близкое. В больших сложных проектах уровней может быть и побольше, этакая фрактальная структура.
Так вот… О чём это я? А! «Логические» функции я стараюсь делать короткими. Это как тезисы доклада для босса. А вот «физические» — они могут быть и короткими, и длинными. Зависит от алгоритма, от предметной области — да мало ли от чего. Не всегда технарю удобно нырять по стеку всё ниже и ниже каждые 5 строчек. А босс редко спросит про детали. Ну 2, максимум 3 уровня — дальше он не полезет вникать.
И не важно, что обычно босс и технарь — это я сам. Такая вот шиза.
+ В С/С++ если мелкие функции разбросаны по разным единицам трансляции, то лишь применение IPO/LTO заинлайнит такие вызовы. А оно примеряется далеко не всегда.
А если функции разбросаны по разным проектам, которые собираются независимо друг от друга на разных машинах, то такие вызовы не сможет заинлайнить вообще никакая оптимизация.
По-моему, из контекста всё очевидно. Сомнения в том, выделять код в отдельную функцию или не выделять, возникают только тогда, когда все итоговые функции будут находиться в одном файле… и, скорее всего, даже в одном классе. Когда код нужно вызывать из других файлов, его по-любому придётся оформить в виде отдельной функции. К сожалению (к счастью), goto между файлами не переходит.
Расскажите это разработчикам на node.js. То, что вы считаете плохим признаком, у них является ключевым моментом методологии разработки.
Выгода в структурированности исходного кода, а не результата.
Код в первую очередь должен быть понятным на уровне предметной области. А преждевременная оптимизация — зло. Будь то функции по 100+ строк или вынос каждого оператора в отдельную функцию.
Код в первую очередь должен быть читабелен человеком.
Глобальный контекст (глобальные переменные) — это палка о двух концах, где-то оправдан, а где-то вреден. В любом случае, с глобальным контекстом читающему код уж точно сладко не придется, потому что придется листать вверх-вниз, чтобы понять что куда присвоилось и когда, и зачем.
В случае с передачей контекста в функцию придется либо оформлять его в структуру/класс/коллекцию, либо передавать в виде пачки параметров. Если на каждую функцию делать свою структуру, например, то это может вылиться в кучу структур, которые еще в дополнение к вызову метода надо будет инициализировать. А если передавать как пачку параметров, то в итоге можем получить ситуацию, когда вызов функции со всеми параметрами длиннее, чем сама функция.
Я, конечно, утрирую, но, думаю, смысл понятен; маленькие функции по… гмм… «методологии» автора могут в такой же степени злом, как и добром.
Просто берется код и сворачивается чтобы глаз не мозолил лишний раз. Не более того. Обычно это отлаженный код, который легко проверить по входным данным и результату и причин углубляться в него нет никаких.
Но подсказки моего анализатора кода по поводу слишком длинного метода принимаю во внимание всегда, как знак того что может быть здесь имеет смысл порефакторить чуток. Но если смысла нет, у меня никогда не зудит, что метод длинный.
Никогда не уменьшаю длину функции в ущерб читаемости и понятности. Никогда не выношу блок кода в отдельную функцию ТОЛЬКО ради того, чтобы другая функция стала короче.
И лично для меня, «попахивают» не длинные функции, а советы «не делать методы длинней X строк». Можно (и нужно) дать совет не делать длинные функции, не забывать про декомпозицию и пр., но когда в таком совете появляется цифра — то на свалку такой совет.
Сначала кажется, что с такими мелкими методами, кода будет гораздо больше и будет множество методов. Но в итоге все сворачивается и становится простым и лаконичным.
А выносить часть кода в функцию, только ради избавления копипасты — это в корне не верно.
public void requestSomethingParseAndSave(String parameter) {
List<Integer> data = this.getAPI().getData(parameter);
this.getObservers().notify('data received')
List<Integer> newData = new ArrayList<>();
for (Integer i : data) {
newData.push(i + 15);
}
this.getObservers().notify('data parsed', newData)
this.getAPI().saveData(newData);
}
Получается что-нибудь
private void notifyObserversThatDataIsReceived() {
this.getObservers().notify('data received')
}
private List<Integer> getNewData() {
return new ArrayList<>();
}
private List<Integer> parseSomething(List<Integer> data) {
// TODO rewrite, too many lines
List<Integer> newData = getNewData();
for (Integer i : data) {
newData.push(doParsing(i));
}
return newData;
}
public void requestSomethingParseAndSave(String parameter) {
List<Integer> data = this.requestSomething(parameter);
List<Integer> newData = this.parseSomething(data);
this.saveData(newData);
}
private Integer doParsing(Integer i) {
return i + 15;
}
private List<Integer> getData(String parameter) {
return this.getAPI().getData(parameter);
}
private void saveData(List<Integer> newData) {
this.getObservers().notify('data parsed', newData)
this.getAPI().saveData(newData);
}
private List<Integer> requestSomething() {
List<Integer> data = this.getData(parameter)
this.notifyObserversThatDataIsReceived()
return data;
}
Так что мне кажется выносить функцию которую используешь только один раз — блажь.
Конкретно в вашем примере может иметь смысл вынести логику преобразования элемента в отдельную функцию. А для преобразования массивов в Java 8 есть стандартные средства.
public void requestSomethingParseAndSave(String parameter) {
List<Integer> data = this.getAPI().getData(parameter);
this.getObservers().notify('data received')
List<Integer> newData = data.stream().map(MyClass::parseData).collect(Collectors.toList());
this.getObservers().notify('data parsed', newData)
this.getAPI().saveData(newData);
}
private static int parseData(int x) {
return x + 15;
}
- Request
- Parse
- Save
Рано или поздно нам придётся писать ещё одну бизнес функцию, в которой будет получение данных и их сохранение.
Магическое число семь плюс-минус два
The Magical Number Seven, Plus or Minus Two
Если больше, то блоками, т.е. сделает виртуальный рефакторинг и будет воспринимать блок кода как заинлайненную функцию.
В функции используются параметры/поля класса, которые тоже требуют внимания.
Также можно посмотреть на функцию обработки сообщений от ОС. Она обычно выглядит как свитч по одному параметру, где case блоки независимы. С такой функцией можно работать, даже если она состоит из сотен строк.
Если блоки выстроены в конвейер (каждый блок оперирует только с переменными, записанными предыдущим блоком), то их может быть больше 7, но это синтетический пример.
Поэтому наличие связей между блоками также влияет на количество оперируемых строк.
Даже функцию на 3 строки можно сделать нечитаемой:
auto Process(auto var1, auto var2, ..., auto var7)
{
Prepare(var1, var2, var4, var6, var7);
Update(var2, var3, var4, var5, var6);
Save(var1, var3, var5, var7)
}
В этой функции столько связей, что она практически нечитаема.1) автодокументирование, пояснять большой кусок кода комментариями больше становится не нужно, название функции прекрасно само говорит за себя
2) тестирование, маленькие куски выполняют очень мало работы, следовательно покрытие тестами становится весьма тривиальной задачей
Если принять это во внимание, то можно отбросить фломастерный вопрос по поводу чистоты и приглядности кода.
1) автодокументирование, пояснять большой кусок кода комментариями больше становится не нужно, название функции прекрасно само говорит за себя
Вообще не факт. Вот есть у вас функция «GetHeader», ну получает она какой-то заголовок, и что? То, что вместо комментария //get header list for generating report column headers вы назвали кусок кода GetHeader, не делает этот код понятнее ни на грамм.
2) тестирование, маленькие куски выполняют очень мало работы, следовательно покрытие тестами становится весьма тривиальной задачей
Если оно при этом снижает читаемость, то это сомнительный tradeoff. Я не против тестирования, я против догмы «куча маленьких функций заведомо понятнее и лучше читается».
2) тестирование, маленькие куски выполняют очень мало работы, следовательно покрытие тестами становится весьма тривиальной задачей
Если эти маленькие функции — приватные, то на тестирование это вообще не влияет. (это случай, когда код из длинного публичного метода в классе вынесли в несколько приватных в том же классе).
Как-то читал "Чистый Код" Роберта Мартина, он в 3-й главе (помимо прочего) привёл соображение, что на одну функцию должен быть один уровень абстракции ("one level of abstraction per function"). Далее, на примере листинга длинного метода testableHtml, он утверждает, что:
- вызов getHtml() — находится на очень высоком уровне абстракции;
- вызов String pagePathName = PathParser.render(pagePath) — на среднем;
- вызов (чего-то там).append("\n") — на весьма низком.
И вроде бы интуитивно это более-менее понятно, но… Кому-нибудь известны конкретные правила, по которым можно не сильно напрягаясь распределить код функции по уровням абстракции? Типа, берём уровень абстракции рассматриваемой функции, как базовый, и понимаем, что один участок кода на 3 уровня абстракции ниже, а другой — всего-лишь на 1, тогда как третий — аж на 10 выше (это, вообще, возможно?).
Длина функции