Функция, используемая для примера, решает задачу разбиения периода времени, заданного начальной и конечной датой, на отрезки, принадлежащие различным месяцам. Пусть в исходном варианте вид функции будет таким -ВАРИАНТ0:
Функция РазбитьПериод0(ДатаС, ДатаПо) Экспорт
МассивПериодов = Новый Массив;
Дата1 = ДатаС;
Дата2 = ДобавитьМесяц(НачалоМесяца(Дата1), 1);
Пока Истина Цикл
Дата3 = Дата2 - 1;
Если Дата3 > ДатаПо Тогда
Дата3 = ДатаПо
КонецЕсли;
ЗаписьПериода = Новый Структура;
ЗаписьПериода.Вставить("НачалоПериода", Дата1);
ЗаписьПериода.Вставить("КонецПериода", Дата3);
МассивПериодов.Добавить(ЗаписьПериода);
Если Дата3 = ДатаПо Тогда
Прервать
КонецЕсли;
Дата1 = Дата2;
Дата2 = ДобавитьМесяц(Дата1, 1)
КонецЦикла;
Возврат МассивПериодов
КонецФункции
Здесь можно сделать следующее
-
Можно не выполнять присваивания "Дата1 = ДатаС". Вместо этого достаточно использовать сам параметр «ДатаС» как переменную, а чтобы после выполнения функции его значение не оказалось «испорченным», перед ним в заголовке функции нужно использовать лексему «Знач». Это означает, что параметр передается не «по ссылке», когда используется ранее выделенная переменной память, а «по значению», когда создается новый экземпляр переменной.
-
Можно сэкономить на придумывании осмысленного названия переменной, предназначенной для вычисляемого значения функции. Если результат функции строится в ходе ее выполнения и должен быть как-то заранее назван, лучше не выдумывать нового термина, так как у функции уже есть имя, а назвать результат выполнения функции в процессе его вычисления просто «Результат» или «Ответ».
-
Последовательность операторов «Дата3 = Дата2 - 1;Если Дата3 > ДатаПо Тогда Дата3 = ДатаПо КонецЕсли» это всего лишь присвоение значения переменной «Дата3» с учетом ограничения ее значения сверху значением переменной «ДатаПо». Эта задача проще и нагляднее решается с помощью функции «Мин». То есть, если результат выражения должен учесть ограничения на результат сверху или снизу, следует использовать функцию минимум или максимум, которые читаются легче и пишутся короче, чем условный оператор «если» или условный оператор с вопросом.
-
Конструктор структуры позволяет сразу проинициализировать ее элементы, поэтому вставку элементов можно сократить. Для создания структуры и инициализации ее полей можно пользоваться соответствующей формой конструктора, куда сразу передать имена полей через запятую и значения полей. Так создаются структуры периодов, добавляемых в массив.
-
Можно не создавать переменную для ссылки на создаваемую структуру. Так как она будет использована лишь однажды – в следующей строке программы. Вместо этого результат можно сразу записать на место его использования. Тогда не нужно будет «вымучивать» названия лишних переменных. Исключением являются случаи, когда промежуточный результат требует пояснения или контроля в процессе отладки. В нашем случае строка и без этого легко читается: в массив-результат добавляется новая структура из полей «НачалоПериода», «КонецПериода», имеющих соответствующие значения.
В результате функция оказывается приведенной к виду ВАРИАНТ1:
Функция РазбитьПериод1(Знач ДатаС, ДатаПо) Экспорт
Ответ = Новый Массив;
Дата2 = ДобавитьМесяц(НачалоМесяца(ДатаС), 1);
Пока Истина Цикл
Дата3 = Мин(Дата2 - 1, ДатаПо);
Ответ.Добавить(Новый Структура("НачалоПериода, КонецПериода", ДатаС, Дата3));
Если Дата3 = ДатаПо Тогда
Прервать
КонецЕсли;
ДатаС = Дата2;
Дата2 = ДобавитьМесяц(ДатаС, 1)
КонецЦикла;
Возврат Ответ
КонецФункции
Анализируя эту запись, можно придти к выводу, что переменные «ДатаС», «Дата2» и «Дата3» связаны простой зависимостью. "ДатаС" – это начало периода или начало текущего месяца, "Дата2" – это начало следующего, а "Дата3" – конец текущего месяца. Выполним подстановку и
-
Избавимся от лишних, то есть связанных простой зависимостью, переменных.
В результате этого получаем ВАРИАНТ2:
Функция РазбитьПериод2(Знач ДатаС, ДатаПо) Экспорт
Ответ = Новый Массив;
Пока ДатаС < = ДатаПо Цикл
Ответ.Добавить(Новый Структура("НачалоПериода, КонецПериода", ДатаС, Мин(КонецМесяца(ДатаС), ДатаПо)));
ДатаС = КонецМесяца(ДатаС) + 1
КонецЦикла;
Возврат Ответ
КонецФункции
Внимание: Из-за того, что знак "меньше или равно" в тексте функции неправильно отрабатывается при редактировании статьи на этом сайте, в него добавлен пробел!
Похожий вариант уже публиковался на Инфостарте [?], вот он - ВАРИАНТ3:
Функция РазбитьПериод3(ДатаС, ДатаПО) Экспорт
Ответ=Новый Массив;
Мес=НачалоМесяца(ДатаС);
Пока Мес< =НачалоМесяца(ДатаПО) Цикл
Период=Новый Структура;
Период.Вставить("НачалоПериода", ?(НачалоМесяца(ДатаС)=Мес,ДатаС,Мес));
Период.Вставить("КонецПериода", ?(НачалоМесяца(ДатаПО)=Мес,ДатаПО,КонецМесяца(Мес)));
Ответ.Добавить(Период);
Мес=ДобавитьМесяц(Мес,1);
КонецЦикла;
Возврат Ответ;
КонецФункции
Здесь само собой напрашивается применение приемов 3, 4 и 5, в результате чего получается более красивый и компактный ВАРИАНТ4
Функция РазбитьПериод4(ДатаС, ДатаПО) Экспорт
Ответ = Новый Массив;
Мес = НачалоМесяца(ДатаС);
Пока Мес < = ДатаПО Цикл
Ответ.Добавить(Новый Структура("НачалоПериода, КонецПериода", Макс(ДатаС,Мес), Мин(ДатаПО,КонецМесяца(Мес))));
Мес = ДобавитьМесяц(Мес, 1)
КонецЦикла;
Возврат Ответ
КонецФункции
В общем, результат в варианте 2 уже можно было бы принять, однако функция «Мин» смотрится инородно. Возможно, это от того, что она срабатывает лишь однажды – на самом последнем периоде. И если периодов много – эти сравнения оказываются лишними. Также, кажется, что можно избавиться от переменной «ДатаС», которая всегда на единицу больше конца периода, только что записанного в массив. А если попробовать сначала записать в массив исходный интервал, а затем при необходимости делить его на две записи, выделяя последний месяц в следующую запись? Алгоритм уместился в одном предложении. Возможно, и его запись окажется понятнее и проще?
Чтобы при этом не приходилось затем удалять записи, если интервал «отрицательный», выполним в этом случае обмен «ДатаС» и «ДатаПо». Здесь повторим прием 3 и обойдемся без условных операторов: началом периода будем считать минимум, а концом – максимум значений этих переменных. С точки зрения пользователя функции, это будет удобнее: программисту при записи вызова функции не нужно будет помнить, какую дату записывать первой – меньшую или большую, период можно будет задать и так и так. Функция станет более «робастной», то есть станет лучше защищена от ошибок в исходных данных. Конечно, это будет иметь свою цену - так отсекается возможность использования функции для контроля, что «ДатаС» меньше «ДатаПо». В результате получаем следующий ВАРИАНТ5:
Функция РазбитьПериод5(ДатаС, ДатаПо) Экспорт
Ответ = Новый Массив;
Ответ.Добавить(Новый Структура("НачалоПериода, КонецПериода", Мин(ДатаС, ДатаПо), Макс(ДатаС, ДатаПо)));
Пока НачалоМесяца(Ответ[0]["НачалоПериода"]) < НачалоМесяца(Ответ[0]["КонецПериода"]) Цикл
Ответ.Вставить(1, Новый Структура("НачалоПериода, КонецПериода", НачалоМесяца(Ответ[0]["КонецПериода"]), Ответ[0]["КонецПериода"]));
Ответ[0]["КонецПериода"] = Ответ[1]["НачалоПериода"] - 1
КонецЦикла;
Возврат Ответ
КонецФункции
Рассматривая этот результат, можно заметить, что текст «НачалоПериода», «КонецПериода», "Ответ[0]" часто повторяется. Поэтому,
-
Если запомнить повторяющиеся константы и сложные ссылки в переменных с короткими названиями, длину кода можно сократить.
-
При этом некоторые переменные, обозначающие в данной функции константы, лучше вообще сделать параметрами функции и проинициализировать в описании параметров. В данном случае это касается названия полей структуры. Кроме сокращения записи, это придает функции дополнительную гибкость, расширяя варианты ее использования.
Прием 8 заслуживает демонстрации еще одним примером. Вот функция для получения количества элементов массива, равных образцу, с использованием приема 8.
Функция Сколько(Массив, Образец, Ответ = 0) Экспорт
Для Каждого Элемент Из Массив Цикл
Ответ = Ответ + Число(Элемент = Образец)
КонецЦикла;
Возврат Ответ
КонецФункции
Если массив «Слова» хранит слова программного кода некоторой функции, то выражение
Сколько(Слова, «Цикл», Сколько(Слова, «Тогда», 1))
рассчитает показатель цикломатической сложности функции.
Возвращаясь к исходной функции, применение приемов 7 и 8 дает следующий результат - ВАРИАНТ6:
Функция РазбитьПериод6(ДатаС, ДатаПо, Поле1 = "НачалоПериода", Поле2 = "КонецПериода") Экспорт
Ответ = Новый Массив;
Э = Новый Структура(Поле1 + "," + Поле2, Мин(ДатаС, ДатаПо), Макс(ДатаС, ДатаПо));
Ответ.Добавить(Э);
Пока НачалоМесяца(Э[Поле1]) < НачалоМесяца(Э[Поле2]) Цикл
Ответ.Вставить(1, Новый Структура(Поле1 + "," + Поле2, НачалоМесяца(Э[Поле2]), Э[Поле2]));
Э[Поле2] = Ответ[1][Поле1] - 1
КонецЦикла;
Возврат Ответ
КонецФункции
Рассматривая текущий вариант функции, можно заметить повтор в четырех местах названия функции «НачалоМесяца». Если это название разделить на два слова и сделать второе из них параметром функции, то она станет более универсальной, сможет выполнять разбивку на «Минуты», «Часа», «Дни», «Недели», «Месяца», «Квартала», «Года» (по понятным причинам придется смириться с некоторым диссонансом в произношении названий периодов). Для этого нам понадобится функция «Выполнить», которая воспринимает код как строку и выполняет его. В исходном коде название функции заменяется на «_», то есть «впишите что хотите», а перед выполнением туда вписывается нужный интервал заменой подстроки. В данном случае применяется наиболее спорный, ухудшающий понимание программы, прием
-
Универсализации кода с использованием оператора «Выполнить». Этот прием следует применять с осторожностью.
Получаем следующий ВАРИАНТ7:
Функция РазбитьПериод7(ДатаС, ДатаПо, НаЧто = "Месяца", Поле1 = "НачалоПериода", Поле2 = "КонецПериода") Экспорт
М = Новый Массив;
Э = Новый Структура(Поле1 + "," + Поле2, Мин(ДатаС, ДатаПо), Макс(ДатаС, ДатаПо));
М.Добавить(Э);
Выполнить(СтрЗаменить(
"Пока _(Э[Поле1])< _(Э[Поле2])Цикл М.Вставить(1,Новый Структура(Поле1+"",""+Поле2,_(Э[Поле2]),Э[Поле2]));Э[Поле2]=М[1][Поле1]-1 КонецЦикла"
, "_", "Начало" + НаЧто));
Возврат М
КонецФункции
Переменная «Ответ» заменена однобуквенной переменной «М» для сокращения длины строковой константы, содержащей шаблонизированный код.
Оглядев результат, видим, что в последнем варианте совершенно потеряна простота и понятность кода, которая ранее оправдывала менее компактную запись функции в варианте 5. Поэтому вернемся к варианту 2 и применим к нему приемы 7, 8 и 9. Здесь мы используем прием
-
Сохранение промежуточных вариантов улучшения кода и возврат к ним из тупиковых ветвей процесса рефакторинга.
В итоге получили следующую универсальную функцию, решающую поставленную задачу (знак "_" заменен словом "Край", что обеспечивает читабельность функции внутри строки ), ВАРИАНТ8:
Функция РазбитьПериод8(Знач А, Б, Чего = "Месяца", Поля = "НачалоПериода, КонецПериода") Экспорт
М = Новый Массив;
Выполнить(СтрЗаменить("Пока А< =Б Цикл М.Добавить(Новый Структура(Поля,А,Мин(Край(А),Б)));А=Край(А)+1 КонецЦикла", "Край", "Конец" + Чего));
Возврат М
КонецФункции
Можно предложить еще дальше и переписать функцию на английский. Получим последний, десятый ВАРИАНТ9
Function РазбитьПериод9(Val A, B, What = "Месяца", Fields = "НачалоПериода, КонецПериода") Export
M = New Array;
Execute(StrReplace("While A<=B Do M.Add(New Structure(Fields,A,Min(Edge(A),B)));A=Edge(A)+1 EndDo", "Edge", "Конец" + What));
Return M
EndFunction
В прилагаемой к статье обработке приведены все вышеописанные функции с возможностью их тестирования.
Нужно сказать, что улучшение кода – процесс, результат которого не всегда можно оценить однозначно. Объективная составляющая оценок представлена метриками кода. Значения метрик компактности и цикломатической сложности [Анализ цикломатической сложности кода] для всех промежуточных вариантов рассматриваемой функции приведены в таблице.
Номер варианта | Число строк | Число знаков | Цикломатическая сложность | Произвольный период |
0 | 21 | 494 | 4 | Нет |
1 | 14 | 348 | 3 | Нет |
2 | 8 | 255 | 2 | Нет |
3 | 12 | 385 | 4 | Нет |
4 | 9 | 279 | 2 | Нет |
5 | 9 | 469 | 2 | Нет |
6 | 10 | 399 | 2 | Нет |
7 | 9 | 407 | 1 | Да |
8 | 5 | 263 | 1 | Да |
9 | 5 | 242 | 1 | Yes! |
Субъективная составляющая оценок отражает простоту и понятность кода. Применяя все перечисленные приемы исключительно в погоне за компактностью, можно пропустить момент утраты понятности кода, то есть важно вовремя затормозить. Но когда? Наверное, каждый должен решать сам. В конце концов, кто-то считает, что «тормоза придумали трусы», а кто-то: «тише едешь – дальше будешь».