Я изучаю немного C за праздничные выходные, и я начал смотреть на другие программы, записанные в C. Я закончил тем, что смотрел на GNU Netcat, думая, что это будет хороший пример.
Я был немного потрясен видеть 600 строк main()
функция. Действительно ли это нормально? Если нормально, что это считают хорошим C кодированием методов?
Есть цитата американского президента (Линкольна?), которого спросили, как долго должны быть ноги мужчины. "Достаточно длинными, чтобы дотянуться от тела до земли", - сказал он.
Возвращаясь к теме:
Авторы книг вроде "Чистого Кода" рекламируют, что каждая функция делает только одно (что я здесь грубо упрощаю), так что теоретически ваша main()
должна вызывать функцию инициализации, а затем другую функцию, организующую работу приложения, и все.
На практике многие программисты находят множество крошечных функций раздражающими. Пожалуй, более полезной метрикой является то, что функция обычно должна помещаться на одном экране, хотя бы для того, чтобы ее было легче увидеть и обдумать.
Если программа сложна и большая часть ее функциональности находится в main()
, то кто-то не сделал достойной попытки разобраться с проблемой. По сути, нужно стремиться к управляемости, понятности и читабельности. Обычно нет веских причин для того, чтобы main() была огромной.
Мой личный стиль кодирования - пытаться использовать только основную функцию для разбора аргументов командной строки и любую инициализацию с большими буквами, которая нужна программе.
В некоторых типах приложений я часто встречаю, что main() имеет сотни строк инициализации, за которыми следуют около 20 строк цикла верхнего уровня.
Это моя привычка - не разбивать функции, пока мне не понадобится вызвать их дважды. Иногда это приводит меня к написанию 300-строчной функции, но как только я вижу, что один и тот же блок возникает дважды, я вырываю этот блок.
Что касается main, то процедуры инициализации часто бывают один раз, так что 600 строк не кажутся неразумными.
.Независимо от языка, я бы попытался ограничить метод подпрограмм примерно тем, что видно на одной странице кода, и по возможности извлечь функциональность из подпрограмм.
600 строк звучит довольно длинно для любой реализации. Возможно, есть какая-то основная причина не передавать аргументы и ясность (я не смотрел на приведенный вами пример), но это звучит как дальний конец того, что обычно практикуется, и должна быть возможность разделить эту задачу.
Я подозреваю, что она разрабатывалась путем постоянного инкрементального добавления функциональности на протяжении многих лет, и никто не останавливался и не рефакторинговал ее, чтобы она была более читабельной/достижимой. Если для этого нет юнит-тестов (а по моему опыту main()
методы не часто получают письменные тесты - по каким бы то ни было причинам) тогда будет понятное нежелание рефакторировать его.
Это ужасно, но я видел и похуже. Я видел большие, многотысячные линейные фортран-программы без подпрограмм вообще.
Я думаю, что ответ таков: она должна помещаться в окно редактора и иметь низкую цикломатическую сложность .
Если основная программа - это всего лишь серия вызовов функций или вычислений, то я полагаю, что это может быть столько, сколько нужно, и у нее может быть освобождение от ограничения окна редактора . Даже тогда я был бы немного удивлён, если бы не было естественного способа извлечения осмысленных дискретных методов.
Но если это тестирование и разветвление и return
ing и break
ing и continue
- то его нужно разбить на отдельные и индивидуально тестируемые функциональные компоненты.
Надеюсь, они планируют рефакторинг. Это выглядит очень грубо.
443 while (optind < argc) {
444 const char *get_argv = argv[optind++];
445 char *q, *parse = strdup(get_argv);
446 int port_lo = 0, port_hi = 65535;
447 nc_port_t port_tmp;
448
449 if (!(q = strchr(parse, '-'))) /* simple number? */
450 q = strchr(parse, ':'); /* try with the other separator */
451
452 if (!q) {
453 if (netcat_getport(&port_tmp, parse, 0))
454 netcat_ports_insert(old_flag, port_tmp.num, port_tmp.num);
455 else
456 goto got_err;
457 }
458 else { /* could be in the forms: N1-N2, -N2, N1- */
459 *q++ = 0;
460 if (*parse) {
461 if (netcat_getport(&port_tmp, parse, 0))
462 port_lo = port_tmp.num;
463 else
464 goto got_err;
465 }
466 if (*q) {
467 if (netcat_getport(&port_tmp, q, 0))
468 port_hi = port_tmp.num;
469 else
470 goto got_err;
471 }
472 if (!*parse && !*q) /* don't accept the form '-' */
473 goto got_err;
474
475 netcat_ports_insert(old_flag, port_lo, port_hi);
476 }
477
478 free(parse);
479 continue;
480
481 got_err:
482 free(parse);
483 ncprint(NCPRINT_ERROR, _("Invalid port specification: %s"), get_argv);
484 exit(EXIT_FAILURE);
485 }
Главная линия 600 - это бит предупреждающего знака. Но если вы посмотрите на него и не увидите другого способа разбить его на более мелкие кусочки, кроме как сделать это.
void the_first_part_of_main(args...);
void the_second_part_of_main(args...);
...
main()
{
the_first_part_of_main();
the_second_part_of_main();
...
}
Тогда вам следует оставить его в покое.
По некоторым стандартам, любая функция из 600 строк - плохая идея, но нет причин, по которым к основной следует относиться иначе, чем к любой другой функции.
Единственная причина, по которой я могу думать о такой ситуации, - это то, что программа быстро развивается, и по мере ее роста никто никогда не удосужится разделить ее на более логические единицы.
.Как можно короче. Обычно, когда есть операция, которой я могу присвоить имя, я создаю для нее новый метод.
.Я бы сказал, что ваши процедуры должны быть настолько длинными/короткими, насколько это необходимо, чтобы быть эффективными, надежными и автоматически проверенными. Рутина с 600-ю степенями, скорее всего, имеет несколько путей через нее, и комбинации процедур могут очень быстро стать очень большими. Я пытаюсь разбить функции на что-то, что делает их легко читаемыми. Функции либо "функциональные", либо "повествовательные". Все это время включая юнит-тесты.
Почти все 600-строчные функции, которые я видел, тоже были написаны глупо. Это не обязательно должно быть так.
Однако в этих случаях программист просто не смог представить какой-то увеличенный вид и дать значимые имена секциям - как высокоуровневым (например, Initialize()), так и низкоуровневым (что-то, что берет общий 3-строчный паттерн и скрывает его под одним именем, с параметрами).
В случаях крайней глупости оптимизировали выполнение вызова функции, когда это не требовалось.
. main()
, как и любая другая функция, должна быть точно такой же большой, какой она должна быть. "Как и должно быть" будет сильно варьироваться в зависимости от того, что ей нужно делать. Сказав это, она не должна иметь больше, чем пара сотен строк в большинстве случаев. 600 строк немного сложнее, и некоторые из них могут/должны быть рефакторизованы в отдельные функции.
Для крайнего примера, одной команде, в которой я работал, было поручено ускорить некоторый код для управления 3d дисплеем. Первоначально код был написан прослушивающим устройством, которое, очевидно, обучалось программированию с использованием FORTRAN старой школы; main()
было больше пяти тысяч строк кода, со случайными битами #include
ed здесь и там. Вместо того, чтобы разбивать код на функции, он просто разветвлялся на подпрограмму в пределах main()
через goto
(где-то между 13 и 15 готами, разветвляя оба направления, казалось бы, случайным образом). В качестве первого шага мы просто включили оптимизацию 1-го уровня, компилятор быстро поглотил всю доступную память и пространство подкачки и запаниковал кернел. Код был настолько хрупким, что мы не смогли ничего изменить , не взломав. В конце концов, мы сказали клиенту, что у него есть два варианта: позволить нам переписать всю систему с нуля или купить более быструю аппаратуру.
Они купили более быстрое оборудование.