Книга: Рефакторинг. Зачем?

Признаки необходимости выделения функции

Признаки необходимости выделения функции

Как я уже написал выше — тема довольно сложная и неоднозначная. Подобных признаков может быть сколько угодно, у каждого они свои, но какие–то общие рекомендации на основании своего опыта я постараюсь дать.

1. Размер функции. Первое, что должно насторожить — это слишком большой размер функции. Проще всего измерять их в экранах. Экран в данном случае — это количество кода, которое вы можете увидеть без использования прокрутки. Лучше всего читаются функции, влезающие на экран полностью.

И это не связано с «лишней работой» в виде листания текста. Дело в том, что даже в художественной литературе, для полного понимания, приходится возвращаться к началу абзаца. Что уж говорить о коде, который, зачастую, куда менее линеен.

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

2. Непонятный код. Если вы, разбираясь в том, как работает функция, наткнулись на кусок кода, в котором пришлось разбираться дольше, чем обычно — подумайте о том, чтобы вынести его в отдельную функцию с понятным названием.

Действительно, если вы сейчас уже потратили время и разобрались в чём–то, почему бы не закрепить результат, чтобы оградить себя (и других в случае коллективной разработки) от совершения той же самой работы в бедующем? Как правило это не очень сложно и быстро окупается.

3. Локальные переменные. Если в вашей функции есть фрагмент кода, в котором инициализируются и используются локальные переменные, которые не используются за пределами этого фрагмента — это также является сигналом к тому, чтобы попытаться вынести данный код в отдельную функцию.

В качестве иллюстрации — можете посмотреть пример к прошлой главе. Там мы благополучно избавились от переменных Width и Height в функции RectsLength. Опятьь же из опыта скажу, что большое количество локальных переменных в функции усложняет восприятие.

4. Внутри функции выполняется какое–то законченное, осмысленное действие. Даже если три строки, вычисляющие периметр не кажутся вам сложным фрагментом кода, рекомендую его всё равно вынести в отдельную функцию. Причин для этого можно назвать несколько:

— Через какое–то время ваш фрагмент и функция в целом может стать значительно сложнее, в середину понятного ранее куска кода могут попасть посторонние, не относящаеся к нему строки. В результате этого на минутное изначально дело можно потратить в разы большее количество времени. При этом риск допустить ошибку будет также выше;

— Возрастает вероятность повторного использования кода. Если вы один раз вычислили периметр квадрата внутри функции, то, вполне вероятно, что и в следующий раз вы не вынесете её наружу. В результате, вполне возможно, что одинаковый фрагмент кода будет встречаться у вас многократно. Это само по себе не очень хорошо и может значительно увеличить суммарный объём кода, но, если в этом коде ещё и допущена ошибка или его нужно поменять по какой–то другой причине — можно наткнуться на серьёзные, долгоживущие проблемы. Совершенно типичная ситуация — поменяли в одном месте, не поменяли в другом. Где–то в третьем месте поменяли, но иначе. В результате код расползается, происходит рассинхронихация и прочие весьма неприятные вещи.

— Код станет проще читать.

Следует отметить, что код выполняющий отдельное осмысленное действие в общем случае не обязан идти подряд.

function RectsLength(Rects: array of TRect; MinLength: Integer): Integer;
var
I: Integer;
Len: Integer;
Widths, Heights: array of Integer;
begin
Result:= 0;
SetLength(Widths, Length(Rects));
SetLength(Heights, Length(Rects));
for I:= 0 to Length(Rects) — 1 do
begin
Widths[I]:= Rects[I].Right — Rects[I].Left;
Heights[I]:= Rects[I].Bottom — Rects[I].Top;
end;
for I:= 0 to Length(Rects) — 1 do
begin
 Len:= 2 * Widths[I] + 2 * Heights[I];
if Len >= MinLength then
 Result:= Result + Len;
end;

end;

Это та же самая функция расчёта суммы периметров из прошлой главы. Мы уже видели несколько вариантов её реализации, но этот, пожалуй, наиболее сложный и избыточный. Понятно, что тут легко избавится от второго цикла, что значительно упростит конструкцию, но ведь между циклами может быть ещё много другого кода. Тогда всё станет куда менее очевидно. В этом случае, знание того, что для расчёта суммы периметров прямоугольников, надо так или иначе рассчитать периметр каждого из них, может сослужить хорошую службу.

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

procedure AddPointToRect(x, y: Integer; Rect: TRect);
begin
if (x >= Rect. Left) and (x <= Rect. Right) and (y >= Rect. Top) and (y <= Rect. Bottom) then
AddPoint(x, y);
end;
Лучше заменить на:
function PointOnRect(x, y: Integer; Rect: TRect): Boolean;
begin
Result:= (x >= Rect. Left) and (x <= Rect. Right) and (y >= Rect. Top) and (y <= Rect. Bottom);
end;
procedure AddPointToRect(x, y: Integer; Rect: TRect);
begin
if PointOnRect(x, y, Rect) then
AddPoint(x, y);
end;

Однако для функции PointOutsideRect, добавляющей точку за пределами прямоугольника, лучше не писать «if PointOutsideRect(x, y, Rect) then», а написать «if not PointOnRect(x, y, Rect) then».

6. Высокий уровень вложенности. Бывает функция как матрёшка. Блок кода, в нём ещё блок кода, в нём ещё и так далее. Читать это также довольно трудно. Пример приводить не буду, чтобы не захламлять текст, отмечу лишь, что блок целиком (текст между скобками «begin end» или «{}» для C-подобных языков) очень часто легко переносится в отдельную функцию.

Оглавление книги


Генерация: 1.171. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз