Это плохая практика иметь длинный метод инициализации?

Многие люди спорили о размере функции. Говорят, что функции в целом должны быть довольно короткими. Мнения варьируются от примерно 15 строк до "примерно одного экрана", который сегодня, вероятно, составляет около 40-80 строк.
Кроме того, функции всегда должны выполнять только одну задачу.

Тем не менее, есть один вид функции, которая часто не работает в обоих критериях в моем коде: функции инициализации.

Например, в аудиоприложении должно быть установлено аудиооборудование /API, аудиоданные должны быть преобразованы в подходящий формат, и состояние объекта должно быть должным образом инициализировано. Это, очевидно, три разные задачи, и в зависимости от API они могут легко занимать более 50 строк.

Особенность init-функций заключается в том, что они обычно вызываются только один раз, поэтому нет необходимости повторно использовать какие-либо компоненты. Вы все еще разбили бы их на несколько меньших функций? Считаете ли вы, что большие функции инициализации в порядке?

9 ответов

Решение

Я все равно разбил бы функцию по задачам, а затем вызвал бы каждую из функций более низкого уровня из моей общедоступной функции инициализации:

void _init_hardware() { }
void _convert_format() { }
void _setup_state() { }

void initialize_audio() {
    _init_hardware();
    _convert_format();
    _setup_state();
}

Написание кратких функций так же важно, как изоляция ошибок и изменений, так и обеспечение читабельности. Если вы знаете, что ошибка в _convert_format()Вы можете отследить ~40 строк, ответственных за ошибку, немного быстрее. То же самое применимо, если вы фиксируете изменения, которые касаются только одной функции.

И последнее, я использую assert() довольно часто, поэтому я могу "часто терпеть неудачу и рано выходить из строя", и начало функции - лучшее место для пары утверждений о проверке работоспособности. Сокращение функции позволяет вам более тщательно тестировать функцию, основываясь на ее более узком наборе обязанностей. Очень сложно протестировать 400-строчную функцию, которая делает 10 разных вещей.

Если разбиение на более мелкие части делает код лучше структурированным и / или более читаемым - делайте это независимо от того, что делает функция. Дело не в количестве строк, а в качестве кода.

Я бы все же попытался разбить функции на логические единицы. Они должны быть такими же длинными или короткими, как это имеет смысл. Например:

SetupAudioHardware();
ConvertAudioData();
SetupState();

Назначение им четких имен делает все более интуитивно понятным и понятным. Кроме того, их разделение облегчает повторное использование будущими изменениями и / или другими программами.

Во-первых, вместо функции инициализации должна использоваться фабрика. То есть, а не иметь initialize_audio(), у тебя есть new AudioObjectFactory (Вы можете придумать лучшее имя здесь). Это поддерживает разделение проблем.

Однако будьте осторожны и не абстрагируйтесь слишком рано. Очевидно, у вас уже есть две проблемы: 1) инициализация аудио и 2) использование этого аудио. Например, до тех пор, пока вы не абстрагируете аудиоустройство, которое нужно инициализировать, или способ настройки данного устройства во время инициализации, ваш фабричный метод (audioObjectFactory.Create() или что-то в этом роде), следует придерживаться только одного большого метода. Ранняя абстракция служит только для запутывания дизайна.

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

В такой ситуации, я думаю, все сводится к личным предпочтениям. Я предпочитаю, чтобы функции делали только одно, поэтому я бы разделил инициализацию на отдельные функции, даже если они вызываются только один раз. Однако, если бы кто-то захотел сделать все это в одной функции, я бы не стал сильно беспокоиться об этом (пока код ясен). Есть более важные вещи, о которых нужно спорить (например, принадлежат ли фигурные скобки отдельной отдельной строке).

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

В зависимости от структуры кода, возможно, стоит подумать об абстрагировании ваших больших функций инициализации. Я все еще согласен с точкой зрения Мигар, хотя, что разбивка функционирует в _init_X(), _init_Y()и т. д. это хороший способ пойти. Даже если вы не собираетесь повторно использовать комментарии в этом коде для своего следующего проекта, когда вы говорите себе: "Как я инициализировал этот X-компонент?", Вам будет гораздо проще вернуться назад и выбрать его. меньшего _init_X() функции, чем было бы, чтобы выбрать его из более крупной функции, особенно если X-инициализация разбросана по всей ней.

Я думаю, что это неправильный подход, чтобы попытаться посчитать количество строк и определить функции на основе этого. Для чего-то вроде кода инициализации у меня часто есть отдельная функция для него, но в основном так, чтобы функции Load или Init или New не загромождались и не сбивали с толку. Если вы можете разделить его на несколько задач, как предлагали другие, тогда вы можете назвать это чем-то полезным и помочь организовать. Даже если вы вызываете его только один раз, это не плохая привычка, и часто вы обнаруживаете, что бывают другие моменты, когда вы можете захотеть заново инициировать вещи и снова использовать эту функцию.

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

Одной альтернативой этому является использование структуры внедрения зависимостей (например, Spring, Castle Windsor, Guice и т. Д.). У этого есть определенные плюсы и минусы... хотя работа с одним большим методом может быть довольно болезненной, у вас, по крайней мере, есть хорошее представление о том, где все инициализируется, и нет необходимости беспокоиться о том, что может произойти "волшебство"., С другой стороны, инициализация не может быть изменена после развертывания (как это может быть сделано с файлом XML для Spring, например).

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

Длина функции, как вы отметили, является очень субъективным вопросом. Однако стандартная передовая практика заключается в выделении кода, который часто повторяется и / или может функционировать как его собственная сущность. Например, если ваша функция инициализации загружает библиотечные файлы или объекты, которые будут использоваться определенной библиотекой, этот блок кода должен быть модульным.

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

Надеюсь, это поможет,
Карлос Нуньес

Другие вопросы по тегам