Когда я разделяю большие методы (или процедуры, или функции - этот вопрос не является специфическим для ООП, но, поскольку я работаю на языках ООП в 99% случаев, это терминология, которая мне наиболее удобна) на множество маленьких Я часто бываю недоволен результатами. Об этих маленьких методах становится сложнее рассуждать, чем когда они были просто блоками кода в большом, потому что, когда я их извлекаю, я теряю много базовых предположений, которые исходят из контекста вызывающей стороны.
Позже, когда я смотрю на этот код и вижу отдельные методы, я не сразу знаю, откуда они вызываются, и думаю о них как об обычных частных методах, которые можно вызывать из любого места в файле. Например, представьте, что метод инициализации (конструктор или другой) разделен на серию маленьких: в контексте самого метода вы четко знаете, что состояние объекта все еще недопустимо, но в обычном частном методе вы, вероятно, исходите из предположения, что объект уже инициализирован и находится в действительном состоянии.
Единственное решение, которое я видел для этого, - это where
предложение в Haskell, которое позволяет вам определять небольшие функции, которые используются только в «родительской» функции. В основном это выглядит так:
len x y = sqrt $ (sq x) + (sq y)
where sq a = a * a
Но другие языки, которые я использую, не имеют ничего подобного - самым близким является определение лямбды в локальной области, что, вероятно, еще более запутанно.
Итак, мой вопрос - вы сталкиваетесь с этим, и вы вообще видите, что это проблема? Если да, то как вы обычно решаете эту проблему, особенно в «основных» языках ООП, таких как Java / C # / C ++?
Редактировать дубликаты: как уже заметили другие, уже есть вопросы, обсуждающие методы расщепления, и небольшие вопросы, которые являются однострочными. Я читаю их, и они не обсуждают проблему базовых предположений, которые могут быть получены из контекста вызывающей стороны (в примере выше, объект инициализируется). В этом суть моего вопроса, и именно поэтому мой вопрос отличается.
Обновление: если вы следили за этим вопросом и обсуждением под ним, вам может понравиться эта статья Джона Кармака по этому вопросу , в частности:
Помимо понимания фактического выполняемого кода, встроенные функции также имеют то преимущество, что они не позволяют вызывать функцию из других мест. Это звучит смешно, но в этом есть смысл. По мере того, как кодовая база растет с годами, у вас будет много возможностей использовать ярлык и просто вызывать функцию, которая выполняет только ту работу, которую, по вашему мнению, необходимо выполнить. Может существовать функция FullUpdate (), которая вызывает PartialUpdateA () и PartialUpdateB (), но в некоторых случаях вы можете осознать (или подумать), что вам нужно всего лишь выполнить PartialUpdateB (), и вы эффективны, избегая других Работа. Много-много ошибок проистекает из этого. Большинство ошибок являются результатом того, что состояние выполнения не совсем то, что вы думаете.
Ответы:
Ваша забота обоснована. Есть другое решение.
Сделать шаг назад. Какова основная цель метода? Методы делают только одну из двух вещей:
Или, к сожалению, оба. Я стараюсь избегать методов, которые делают оба, но многие делают. Предположим, что произведенный эффект или произведенная стоимость являются «результатом» метода.
Вы заметили, что методы вызываются в «контексте». Что это за контекст?
По сути, вы указываете: правильность результата метода зависит от контекста, в котором он вызывается .
Мы вызываем условия, необходимые перед началом тела метода, чтобы метод дал правильный результат, своими предварительными условиями , и мы вызываем условия, которые будут созданы после того, как тело метода вернет свои постусловия .
По сути, вы указываете: когда я извлекаю блок кода в его собственный метод, я теряю контекстную информацию о предусловиях и постусловиях .
Решение этой проблемы состоит в том, чтобы сделать предварительные условия и постусловия явными в программе . Например, в C # вы можете использовать
Debug.Assert
или кодовые контракты для выражения предварительных условий и постусловий.Например: раньше я работал над компилятором, который прошел несколько «этапов» компиляции. Сначала код будет лексирован, затем проанализирован, затем типы будут разрешены, затем иерархии наследования будут проверяться на циклы и так далее. Каждый бит кода был очень чувствителен к своему контексту; например, было бы катастрофическим задавать вопрос: «Этот тип обратим в этот тип?» если бы граф базовых типов еще не был ацикличен! Поэтому каждый бит кода четко документировал свои предварительные условия. В
assert
методе, который проверял на конвертируемость типов, мы уже прошли проверку «ациклических базовых типов», и читателю стало ясно, где метод может быть вызван и где его нельзя вызвать.Конечно, есть много способов, которыми хороший дизайн метода смягчает проблему, которую вы определили:
источник
string
и сохраняет ее в базе данных, вы рискуете ввести SQL, если забудете ее очистить. Если, с другой стороны, ваша функция принимает aSanitisedString
, и единственный способ получить aSantisiedString
- это вызыватьSanitise
, то вы исключили ошибки внедрения SQL в конструкцию. Я все чаще и чаще пытаюсь заставить компилятор отклонять некорректный код.Я часто вижу это и согласен с тем, что это проблема. Обычно я решаю это путем создания объекта метода : нового специализированного класса, членами которого являются локальные переменные из исходного, слишком большого метода.
Новый класс, как правило, имеет имя, например «Exporter» или «Tabulation», и ему передается любая информация, необходимая для выполнения этой конкретной задачи из более широкого контекста. Тогда можно свободно определять даже меньшие фрагменты вспомогательного кода, которые не могут быть использованы ни для чего, кроме табулирования или экспорта.
источник
Многие языки позволяют вам использовать такие функции, как Haskell. Java / C # / C ++ на самом деле являются относительными выбросами в этом отношении. К сожалению, они настолько популярны , что люди приходят , чтобы думать, «Он должен быть плохой идеей, иначе мой любимый„основной“язык позволил бы его.»
Java / C # / C ++ в основном считают, что класс должен быть единственной группой методов, которая вам когда-либо понадобится. Если у вас так много методов, что вы не можете определить их контексты, есть два основных подхода: сортировать их по контексту или разбивать по контексту.
Сортировка по контексту - это одна из рекомендаций, сделанных в Чистом коде , где автор описывает шаблон «TO абзацев». По сути, это помещает ваши вспомогательные функции сразу после вызывающей их функции, поэтому вы можете читать их как абзацы в газетной статье, получая больше подробностей по мере прочтения. Я думаю, что в своих видео он даже отступает от них.
Другой подход - разделить ваши классы. Это не может быть принято слишком далеко из-за назойливой необходимости создавать экземпляры объектов перед тем, как вы сможете вызывать на них какие-либо методы, а также из-за проблем, связанных с определением того, какой из нескольких крошечных классов должен владеть каждым фрагментом данных. Однако, если вы уже определили несколько методов, которые действительно подходят только для одного контекста, они, вероятно, являются хорошим кандидатом для включения в их собственный класс. Например, сложная инициализация может быть выполнена в шаблоне создания, таком как строитель.
источник
Я думаю, что ответ в большинстве случаев является контекстом. Как разработчик, пишущий код, вы должны предполагать, что ваш код будет изменен в будущем. Класс может быть интегрирован с другим классом, может заменить его внутренний алгоритм или может быть разделен на несколько классов для создания абстракции. Это то, что начинающие разработчики обычно не принимают во внимание, что вызывает необходимость в грязных обходных путях или в дальнейшем капитальном ремонте.
Методы извлечения это хорошо, но в некоторой степени. Я всегда стараюсь задавать себе эти вопросы при проверке или перед написанием кода:
В любом случае всегда думайте о единоличной ответственности. Класс должен нести одну ответственность, его функции должны обслуживать одну постоянную службу, и если они выполняют ряд действий, эти действия должны иметь свои собственные функции, поэтому их легче дифференцировать или изменить позже.
источник
Я не понимал, насколько серьезной была эта проблема, пока не принял ECS, который поощрял большие, зациклившиеся системные функции (единственными системами, имеющими функции) и зависимости, направленные на необработанные данные , а не абстракции.
Это, к моему удивлению, привело к тому, что кодовую базу было намного проще рассуждать и поддерживать по сравнению с кодовыми базами, с которыми я работал в прошлом, когда во время отладки вам приходилось отслеживать все виды маленьких маленьких функций, часто через абстрактные вызовы функций через чистые интерфейсы, ведущие к тому, кто знает, где, пока вы не проследите в него, только для порождения некоторого каскада событий, которые ведут в места, о которых вы никогда не думали, что код когда-либо приведет
В отличие от Джона Кармака, моей самой большой проблемой с этими кодовыми базами была не производительность, так как у меня никогда не было такого сверхжесткого требования к задержке игровых движков AAA, и большинство наших проблем с производительностью относились больше к пропускной способности. Конечно, вы также можете начать усложнять оптимизацию «горячих точек», когда работаете в более узких и более узких рамках более мелких и младших функций и классов, не мешая этой структуре (требуя, чтобы вы слили все эти маленькие кусочки обратно к чему-то большему, прежде чем вы сможете даже начать эффективно заниматься этим).
И все же самой большой проблемой для меня была неспособность уверенно рассуждать об общей правильности системы, несмотря на все прохождения тестов. Было слишком много, чтобы принять в мой мозг и понять, потому что система такого типа не позволяла вам рассуждать об этом, не принимая во внимание все эти крошечные детали и бесконечные взаимодействия между крошечными функциями и объектами, которые происходили повсюду. Было слишком много «что, если?», Слишком много вещей, которые нужно было назвать в нужное время, слишком много вопросов о том, что произойдет, если их называют неправильным временем (которые начинают подниматься до паранойи, когда вы иметь одно событие, запускающее другое событие, вызывающее другое, которое ведет вас во все виды непредсказуемых мест) и т. д.
Теперь мне нравятся мои 80-строчные функции с большими задницами здесь и там, при условии, что они по-прежнему выполняют исключительную и четкую ответственность и не имеют 8 уровней вложенных блоков. Они приводят к ощущению, что в системе меньше вещей для тестирования и понимания, даже если меньшие, нарезанные кубиками версии этих больших функций были только частными деталями реализации, которые никто другой не мог бы вызвать ... все же, каким-то образом, обычно создается впечатление, что по всей системе происходит меньше взаимодействий. Мне даже нравится какое-то очень скромное дублирование кода, если это не сложная логика (скажем, всего 2-3 строки кода), если это означает меньше функций. Мне нравится рассуждения Кармака о том, что эта функциональность невозможна для вызова в другом месте исходного файла. Там'
Простота не всегда уменьшает сложность на большом уровне изображения, если опция находится между одной мясной функцией и 12 сверхпростыми, которые вызывают друг друга со сложным графом зависимостей. В конце дня вам часто приходится размышлять о том, что происходит за пределами функции, приходиться рассуждать о том, что эти функции складываются в конечном итоге, и может быть сложнее увидеть эту общую картину, если вам нужно вывести ее из маленькие кусочки головоломки.
Конечно, очень универсальный код библиотечного типа, который хорошо протестирован, может быть освобожден от этого правила, поскольку такой универсальный код часто функционирует и хорошо работает сам по себе. Кроме того, он имеет тенденцию быть крошечным по сравнению с кодом, немного более близким к области вашего приложения (тысячи строк кода, а не миллионы), и настолько широко применимым, что начинает становиться частью ежедневного словаря. Но с чем-то более конкретным для вашего приложения, где общесистемные инварианты, которые вы должны поддерживать, выходят далеко за рамки одной функции или класса, я склонен считать, что по какой-либо причине это помогает иметь более простые функции. Мне гораздо легче работать с большими кусочками головоломки, пытаясь понять, что происходит с большой картиной.
источник
Я не думаю, что это большая проблема, но я согласен, что это хлопотно. Обычно я просто помещаю помощника сразу после его бенефициара и добавляю суффикс «Помощник». Этот плюс
private
спецификатор доступа должен прояснить его роль. Если есть какой-то инвариант, который не сохраняется, когда вызывается помощник, я добавляю комментарий во помощник.Это решение имеет неприятный недостаток, заключающийся в том, что он не захватывает область действия функции, которая ему помогает. В идеале ваши функции малы, так что, надеюсь, это не приведет к слишком большому количеству параметров. Обычно вы решаете эту проблему, определяя новые структуры или классы для объединения параметров, но необходимое для этого количество стандартного шаблона может быть дольше, чем сам помощник, и тогда вы вернетесь к тому, с чего начали без очевидного способа ассоциирования. структура с функцией.
Вы уже упоминали другое решение - определите помощника внутри главной функции. Это может быть несколько необычной идиомой в некоторых языках, но я не думаю, что это будет сбивать с толку (если ваши коллеги не смущены лямбдами в целом). Это работает, только если вы можете легко определить функции или функционально-подобные объекты. Я бы не стал пробовать это в Java 7, например, так как анонимный класс требует введения 2 уровней вложенности даже для самой маленькой «функции». Это как можно ближе к предложению
let
илиwhere
, как вы можете получить; Вы можете ссылаться на локальные переменные перед определением, и вспомогательный объект не может быть использован вне этой области.источник