Как стать автором
Обновить

Миф о идеальном количестве строк в методе

Программирование *Проектирование и рефакторинг *
Из песочницы
Существует миф, что если в функции больше чем n или меньше чем m строк кода, то с функцией есть проблемы в плане проектирования. К примеру, автор публикации «Размышления о принципах проектирования» говорит, что «число строк метода… является необходимым, но недостаточным условием хорошего дизайна». В этой статье я изложу причины, по которым считаю необходимость функции быть определенного размера мифом и приведу примеры, доказывающие это, но сначала давайте рассмотрим причины популярности этого мифа.

Причины популярности мифа

Если взять практически любой алгоритм из реальной жизни, то его легко можно разбить на небольшое количество операций, обычно от 3 до 10, причём эти операции служат своим уникальным микро-целям. Например, чтобы сесть за стул, стоящий возле стола, нужно 1) увидеть стул; 2) подойти к стулу; 3) отодвинуть стул; 4) сесть за стул. Такое описание действий довольно понятно и, взглянув на каждое действие, можно понять, что же за ним скрывается и какие примерно шаги нужно выполнить для его реализации. Это пример хорошего дизайна. Если бы вместо шага «увидеть стул» было бы несколько операций напрягания глазных мышц, а вместо «подойти к стулу» — цикл постепенного продвижения с постоянным редактированием маршрута, то в таком алгоритме было бы сложно разбираться; даже запомнить все детали алгоритма уже проблематично. Это пример плохого дизайна без должного контроля сложности. В таком случае лучше выделить операции, служащие одной цели – сокращению расстояние между стулом и человеком, в одну функцию.

Если взять другие алгоритмы, например, жарки яичницы или похода в кино – опять таки можно выделить до 10 более мелких операций, служащих РАЗНЫМ микро-целям. Согласитесь, что сходу придумать пример, когда операций гораздо больше 10, довольно сложно, не так ли? Если всё-таки получается довольно много операций, то наверняка можно найти для некоторых общую цель, или может быть вы слишком зациклены на обработке ошибок, которая на самом деле не является ОСНОВНОЙ частью алгоритма.

Опровержение мифа

Придумать алгоритм из жизни, в котором на верхнем уровне абстракции существует большое количество операций, которые не удастся скомпоновать в более крупные (в результате чего ваша главная функция алгоритма раздуется на много строк кода) довольно сложно, но возможно. Например, ритуальный танец какого-нибудь аборигенского племени, который представляет из себя следующие действия: 1) присесть; 2) покудахтать; 3) порычать; 4 )помахать руками; 5) привстать; 6) попрыгать… и ещё 100 или 500 хаотичных безсистемных действий.
Что такое верхний уровень абстракции
Первый уровень абстракции — это глобальное назначение кода, которое при хорошем дизайне совпадает с названием главной функции алгоритма, в данном случае пусть будет «Вызвать дождь». Первый уровень абстракции вырожденный и на нём всегда одна операция, поэтому из рассмотрения его исключаем и под высшим подразумеваем второй. На высшем (втором) уровне абстракции есть такие операции, как «Присесть» и «Покудахтать». При этом у «присесть» цель — изменить положение тела, а у «покудахтать» — издать звуки, имитирующие звуки курицы. Эти цели различные, и если искать между ними общую цель, то в данном контексте общая цель у них только одна — «Вызвать дождь», поэтому не получится их выделить в отдельную функцию, которая вызывалась бы внутри «Вызвать дождь». «Присесть» и «покудахтать» должны находиться в «вызвать дождь» непосредственно. Поэтому они находятся на высшем уровне абстракции


Всё ещё хотите, чтобы ваша функция была меньше n строк кода? Тогда придётся разбить метод на 1) выполнить первую часть ритуального танца; 2) выполнить вторую часть – и так далее, а это пример плохого дизайна, ведь непонятно, что скрывает за собой та или иная функция. Приведу пример аналогичной ситуации, но уже из области программирования. Допустим, есть модуль, скачивающий страницы с некоторого сайта и распарсивающий их, с целью получения информации о закупке, внутри которой есть протоколы, лоты, и много чего другого. Главный метод модуля выглядит примерно так:

1) Узнать информацию о поставщике;
2) Узнать информацию о лотах закупки;
3) Узнать информацию о аукционе закупки;
4) Узнать информацию о протоколах закупки;
5) Узнать информацию о последней дате изменения состояния закупки.
… и ещё подобные методы. При этом каждый метод скачивает разные страницы и применяет различные алгоритмы парсинга. Каждая операция достойна быть выделена в отдельный метод и никак не получится объединить некоторые из них в отдельную функцию с понятным именем (вариант «Узнать информацию о поставщике и лотах и аукционе» не предлагать). Предметную модель и количество страниц на сайте-источнике можно увеличивать до бесконечности (равно как и количество фронт-энд разработчиков сайта, вносящих всё больше специфики в разные страницы сайта). Другой пример, опровергающий миф – это семейство криптографических алгоритмов. Чем большее число n-максимум допустимого размера метода вы бы не назвали, тем более длинный криптографический алгоритм можно придумать.

Обратная сторона мифа

Существует и другая интерпретация данного мифа – если в методе меньше, чем m строк кода (например,50), то что-то с ним не так. Как могла зародиться такая точка зрения? Представьте себе код, не имеющий стройной архитектуры, в котором названия классов и методов или не отражают назначения сущностей, или и вовсе вводят в заблуждения. Возможно, изначально код был хорош, но затем кто-то внёс изменения в функцию «Узнать информацию о последней дате изменения состояния закупки» и теперь она также ещё и сохраняет информацию в базу данных и шлёт пользователям уведомления на почту, но вот название функции не изменилось. Или кто-то внёс изменения в сам алгоритм поиска даты изменения, но внёс их не в эту функцию, а в какое-либо другое место, тогда функцию нужно было бы переименовать в «Узнать информацию о последней дате изменения ЧАСТИ состояния закупки» или в «Узнать информацию о журнале событий» (теперь это ЧАСТЬ операции поиска даты изменения, и метод должен называться соответственно), но, увы и ах, функция не была переименована. В результате к именам методов в таком коде нет доверия, и чтобы узнать, ЧТО В ДЕЙСТВИТЕЛЬНО ЗДЕСЬ ПРОИСХОДИТ, нужно провалиться в каждый из них. А если код раздроблен на большое количество методов и глубина вложенности велика, то нужно проваливаться всё глубже и глубже… В результате в коде легко запутаться, как в лабиринте. А вот если бы весь код класса был в одной гигантской функции, то он хотя бы был виден, как на ладони, да и заведомо ложные имена функций не вводили бы в замешательство.

Теперь представим себе вымышленного программиста по имени Маркус. Маркус не особо усердствует в изучении проектирования и каждый день работает с вышеописанным несуразным кодом. Постепено Маркус начинает замечать, что в «большом коде проще разбираться», а «мелкодроблёный» код начинает у него ассоциироваться с головной болью. Затем кто-то мельком говорит ему о принципе «не плодите лишние сущности». Какая сущность лишняя, а какая — нет, Маркус объяснить не может, но берёт принцип на вооружение. Затем Маркус откуда-то узнаёт о принципе KISS, и решает, что раз «чем меньше сущностей, тем проще разобраться», то «чем меньше сущностей, тем больше код соответствует KISS».

Вот пример статьи, персонажа которой тоже зовут Маркус, и который написал класс, который умеет печь любой сорт хлебо-булочной продукции по любому рецепту на любой печке с любым источником топлива, и этот класс имеет всего один метод. Насколько я понял, у него во всей программе два класса – хлеб (который на самом деле может быть и пирожком) и Manager (который при встрече может сказать «Я всё могу!» и не соврать). Наш Маркус (наш-который из этой статьи) согласен и считает, что это BEST PRACTICE и следование принципу KISS и если вы не плодите God object-ы по 1000 строк кода в каждом, то вам есть чему у него поучиться.
Лично я считаю, что нет правила, что метод обязан быть больше m строк, и что в 99,9% случаев очень даже возможно написать аккуратненькие небольшие функции, по внешнему виду которых можно сказать, что происходит внутри них, каков их контракт и какой цели они служат. При этом для поиска нужного функционала не уйдёт много времени и не придётся просматривать весь код.

А как же нужно?

Мы уже знаем, как не нужно делать – слепо доверять количеству строк в методе. Возникает закономерный вопрос – а как же нужно? Как узнать, что нужно что-то добавить в функцию, или что-то из неё убрать? Узнать нам помогут принцип «Low coupling & High cohesion» и два smell-а: «Стрельба дробью» и «Расходящиеся модификации». Если при изменении какого-то типа функционала нужно подправить кусочек этой сущности и кусочек другой сущности, то, значит, что в коде завелась «Стрельба дробью» и «Low cohesion» и неплохо бы эти две сущности слить в одну. Если же при изменении какого то типа функционала у нас всегда меняется вон та часть сущности и никогда не меняется вот эта часть сущности, то код пахнет «Расходящейся модификацией» и возможно стоит разбить сущность на две. Для пояснения используем немного измененный пример из начала статьи: если при смене способа перемещения у робота, приближающегося к стулу, у вас постоянно меняется часть алгоритма, касающаяся выбора маршрута и передвижения (в зависимости от того, перемещается ли робот по земле, под землей или над землей), то нужно выделить отдельную функцию «приблизиться к столу». В итоге там где была одна сущность, появляется две. Также нужно давать понятные названия сущностей, чтобы по одному только названию можно было понять, что эта сущность делает (и чего не делает).

P.S. Все вышесказанное является всего лишь моим личным мнением. Все персонажи вымышленные.

Здесь статься заканчивается и далее идёт пример ещё одного длинного алгоритма, плохо поддающегося декомпозиции, добавленный по просьбе читателей. Итак, для того, чтобы зашифровать некоторый текст по одному известному криптографическому алгоритму нужно:
1)Разбить исходный текст на блоки по 64 бита
2)Для каждого такого блока
{
2.1)Переставить биты в блоке местами по определённому алгоритму
2.2)Разбить блок на два блока длиной 32 бита (далее — левый и правый)
2.3)Повторить 16 раз:
{
2.3.1)Вычислить Ki из ключа шифрования K и номера итерации по определённому алгоритму.
2.3.2)Высичлить блок E длиной 48 из правого блока длиной 32 по определенному алгоритму.
2.3.3)F = побитовая сумма Ki и E
2.3.4)левый блок длиной 32 = правый блок длиной 32 бита на прошлой итерации.
2.3.5)правый блок длиной 32 = левый блок 32 на прошл итерации, побитово сложенный с F
2.3.6)Добавить левый блок в конец результата шифрования
2.3.7)Добавить правый блок в конец результата шифрования
}
}
Добавлю, что описание алгоритма вычисления Ki на википедии не поместилось на мой монитор, поэтому не думаю, что в программе это будет одной строчкой кода. Если вы создадите функцию «вычислить Ki», то это будет плохим дизайном, т.к. непонятно что же делает эта функция и непонятно что такое Ki. Однако, если у Ki есть некоторое общепринятое название, и Ki — некоторая общепринятая абстракция, то функция «вычислить Ki» имеет право на существование. Для того, чтобы всё таки произвести декомпозицию, сами разработчики алгоритма таких абстракций и насоздавали, причём не очень удачно, их названия похожи на «Первый кусок алгоритма» и «Кусок имени такого-то». Согласитесь, ужасный дизайн. Однако этот дизайн общепринят и является предметной областью, так что если хотите его использовать, то всё нормально. Однако давайте представим, что нужно немного модифицировать алгоритм с целью создания собственного уникально алгоритма, обладающего свойствами схожими и исходным алгоритмом (это может быть полезно, ведь злоумышленнику сложнее взломать «неизвестный науке» алгоритм). В новом алгоритме «Кусок имени такого-то» модифицирован, и уже «Куском имени такого то» не является. В таком случае лучше не разбивать приведённый алгоритм на мелкие функции, а оставить его как есть, иначе однажды (через неделю после написания кода) вы запутаетесь в этом коде как в лабиринте. Приведенный здесь алгоритм — это DES. «Первый кусок» — это «Начальная перестановка», «Кусок имени такого то» — это «Функция Фейстеля», E — это «Функция расширения». В модифицированном алгоритме всё это отличается. Возможно произвести частичную декомпозицию модифицированного DES-а, например выделить методы «Побитово сложить блоки», «Выполнить перестановку» (для Блока, из Матрицы перестановок), и т.д., но вот метод «Зашифровать блок длиной 64» всё-равно будет неприлично большим. Выделение метода «Зашифровать блок длиной 32» — неудачная идея, т.к. вы не сможете его дешифровать; шифрование и дешифрование применимо к блокам 64. Уверен, что DES — не самый длинный из алгоритмов, возможно найти (или придумать) и более длинные, такие, что у них на высшем уровне абстракции будет большое количество операций, цели и назначение которых отличаются друг от друга.
Теги: проектирование по
Хабы: Программирование Проектирование и рефакторинг
Всего голосов 55: ↑31 и ↓24 +7
Комментарии 115
Комментарии Комментарии 115

Похожие публикации

Лучшие публикации за сутки