Содержание
Правило № 1: не пишите все в одной функции
Правило № 2: соберите собственную библиотеку повторно используемого кода
Правило № 3: пишите чистые функции
Правило № 4: не называйте переменные зарезервированными в языке терминами
Правило № 5: вложенность условий — не больше двух
Стас Ганиев, автор ТГ-канала OneSCast | 1С Предприятие, а также сооснователь IT-сообщества DevDV.ru и автор контента на Infostart.ru, поделился правилами написания кода, которые сделают ваши конфигурации на порядок понятнее, проще в сопровождении, а их будущие доработки — дешевле.
Любая конфигурация в процессе доработки стремится к хаосу, который, подобно гангрене, «разъедает» все модули.
Уберечь программный продукт от этого можно, если научиться писать код, который:
- легко понять,
- легко сопровождать,
- легко дорабатывать.
Для экономии места и времени на чтение при упоминании процедур и функций используется более общий термин «Функция» вместо сложного «Процедура или функция».
Правило № 1: не пишите все в одной функции
Проблема
Универсальная функция, которая делает сразу все, может быть удобна на этапе ее разработки, когда нужно быстро выдать результат заказчику. Но со временем для осуществления доработок придется приложить усилия, чтобы разобраться в ее коде, причем даже самому автору. Такая функция будет сложна в дальнейшем сопровождении и доработке, а использовать ее можно будет только для единственной задачи.
Как исправить
Определите, можно ли использовать функцию в других задачах, ничего в ней не дорабатывая. Если появляется необходимость использовать функцию еще раз, но при этом в ней нужно что-то «докрутить», попробуйте разделить ее на несколько отдельных функций или обыграть ситуацию через параметры.
- Разделите весь функционал на блоки и выделите их в отдельные функции, решающие точечные микрозадачи.
- Каждая отдельная функция должна решать только одну конкретную задачу (соблюдаем принцип единственной ответственности).
- Опишите формальные параметры таким образом, чтобы функцию можно было использовать в других задачах, не дорабатывая ее (делаем код повторно используемым).
Пример № 1
Допустим, нам нужно получить массив поставщиков произвольных товаров. При этом перечень товаров может храниться как в массиве, так и в списке значений. Решим задачу в лоб (универсальная функция):
Функция НайтиПоставщиков(Товары) МассивТоваров = Новый Массив; Если ТипЗнч(Товары) = Тип("СписокЗначений") Тогда МассивТоваров = Товары.ВыгрузитьЗначения(); Иначе МассивТоваров = Товары; КонецЕсли; Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ | |"; Запрос.УстановитьПараметр("МассивТоваров", МассивТоваров); РезультатЗапроса = Запрос.Выполнить(); Если РезультатЗапроса.Пустой() Тогда ВывестиСообщение(НСтр("ru = 'Поставщики не найдены'")); Возврат Неопределено; КонецЕсли; Результат = РезультатЗапроса.Выгрузить().ВыгрузитьКолонку("Ссылка"); Возврат Результат; КонецФункции
Преобразуем код в более удобный и лаконичный вид. Выделяем три отдельные подзадачи в этой функции:
- получение массива товаров из универсальной коллекции;
- выполнение запроса с параметром для поиска поставщиков;
- обработка результата и получение массива значений из запроса.
В результате такого разделения наш код мог бы выглядеть примерно так:
Тогда основная функция примет следующий вид:
Функция ПолучитьМассивИзКоллекцииЗначений(Товары) Результат = Новый Массив; Если ТипЗнч(Товары) = Тип("СписокЗначений") Тогда Результат = Товары.ВыгрузитьЗначения(); Иначе Результат = Товары; КонецЕсли; Возврат Результат; КонецФункции Функция СформироватьЗапросПоставщикиТоваров(МассивТоваров) Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ | |"; Запрос.УстановитьПараметр("МассивТоваров", МассивТоваров); РезультатЗапроса = Запрос.Выполнить(); Возврат РезультатЗапроса; КонецФункции Функция ОбработатьРезультатЗапроса(РезультатЗапроса) Результат = Неопределено; Если РезультатЗапроса.Пустой() Тогда ВывестиСообщение(НСтр("ru = 'Поставщики не найдены'")); Возврат Результат; КонецЕсли; Результат = РезультатЗапроса.Выгрузить().ВыгрузитьКолонку("Ссылка"); Возврат Результат; КонецФункции
Пример № 2
Нам нужно получить все договоры с типами, применимыми для поставщиков. Мы пишем такую функцию:
Функция ПолучитьДоговораПоставка() ОтборТипы = Новый Массив; ОтборТипы.Добавить(Перечисления.ТипыДоговоров.СПоставщиком); ОтборТипы.Добавить(Перечисления.ТипыДоговоров.СКомитентом); ОтборТипы.Добавить(Перечисления.ТипыДоговоров.Прочее); Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ | Договоры.Ссылка КАК Ссылка |ИЗ | Справочник.Договоры КАК Договоры |ГДЕ | Договоры.Тип В (&ТипыДоговоров) | И НЕ Договоры.ПометкаУдаления |"; Запрос.УстановитьПараметр("ТипыДоговоров", ОтборТипы); Результат = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка"); Возврат Результат; КонецФункции
После выделения получения типов договоров в отдельную функцию получим более оптимальный код:
Функция ТипыДоговоровСПоставщиком() Результат = Новый Массив; Результат.Добавить(Перечисления.ТипыДоговоров.СПоставщиком); Результат.Добавить(Перечисления.ТипыДоговоров.СКомитентом); Результат.Добавить(Перечисления.ТипыДоговоров.Прочее); Возврат Результат; КонецФункции Функция ПолучитьДоговораПоТипу(ТипыДоговоров) Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ | Договоры.Ссылка КАК Ссылка |ИЗ | Справочник.Договоры КАК Договоры |ГДЕ | Договоры.Тип В (&ТипыДоговоров) | И НЕ Договоры.ПометкаУдаления |"; Запрос.УстановитьПараметр("ТипыДоговоров", ТипыДоговоров); Результат = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка"); Возврат Результат; КонецФункции
А исходная функция преобразуется к виду:
Функция ПолучитьДоговораПоставка() ТипыДоговоров = ТипыДоговоровСПоставщиком(); Результат = ПолучитьДоговораПоТипу(ТипыДоговоров); Возврат Результат; КонецФункции
Полученную функцию ПолучитьДоговораПоТипу() можно теперь использовать не только для получения договоров поставки, но и для договоров с покупателями. А также использовать собственный произвольный перечень типов и передать его в параметр.
Правило № 2: соберите собственную библиотеку повторно используемого кода
Проблема
Существенное время разработчика зачастую тратится на изобретение собственных «велосипедов» — фрагментов кода, которые уже были написаны, но их нужно найти на просторах конфигурации и адаптировать под локальную задачу.
Как исправить
Собирайте свои небольшие функции, предназначенные для решения точечных микрозадач, в отдельные общие модули (либо в расширение или подсистему).
Не нужно организовывать для этого отдельный проект. Просто договоритесь в своей команде, что будете так делать начиная со следующей задачи.
При этом придерживайтесь следующих правил:
- Старайтесь делать функции чистыми (подробнее об этом в следующем совете).
- Имя функции должно отражать более общую задачу, а не конкретную проблему, решаемую здесь и сейчас (в предыдущем примере конкретная задача ПолучитьДоговораПоставка() приобрела более общую функцию ПолучитьДоговораПоТипу()).
- Снабжайте экспортные функции комментарием, документирующим ее назначение и результат.
Пример
Давайте доработаем функцию ПолучитьМассивИзКоллекцииЗначений() из первого примера в библиотечную универсальную функцию. В качестве расширения возможностей ее использования добавим проверку типа параметра на значения «Структура» и «Соответствие»:
// Возвращаем массив значений, содержащий значения произвольной коллекции // // Параметры: // КоллекцияЗначений - Массив, СписокЗначений, Структура, Соответствие - // исходная коллекция значений // // Возвращаемое значение: // Массив - Значения из КоллекцияЗначений, преобразованные в массив // Функция ПолучитьМассивИзКоллекцииЗначений(КоллекцияЗначений) Экспорт Результат = Новый Массив; Если ТипЗнч(КоллекцияЗначений) = Тип("СписокЗначений") Тогда Результат = КоллекцияЗначений.ВыгрузитьЗначения(); ИначеЕсли ТипЗнч(КоллекцияЗначений) = Тип("Структура") ИЛИ ТипЗнч(КоллекцияЗначений) = Тип("Соответствие") Тогда Для Каждого КлючЗначение Из КоллекцияЗначений Цикл Результат.Добавить(КлючЗначение.Значение); КонецЦикла; ИначеЕсли ТипЗнч(КоллекцияЗначений) = Тип("Массив") Тогда Результат = КоллекцияЗначений; Иначе ВызватьИсключение "Неизвестный тип коллекции"; КонецЕсли; Возврат Результат; КонецФункции
Правило № 3: пишите чистые функции
Определение
Чистая функция — это функция, результат которой зависит только от переданных параметров. На алгоритм чистой функции и возвращаемый ею результат не влияют события извне, и сама она не влияет ни на что за пределами себя. Чистая функция не использует ввод-вывод (включая вывод сообщений и выполнение запросов).
Чем полезна:
- Работа чистой функции всегда предсказуема;
- Код функции можно легко и быстро понять, поскольку не нужно обращаться к другим модулям и структуре конфигурации за дополнительной информацией;
- Чистая функция тестопригодна, автотесты на чистые функции дорабатываются крайне редко или вообще остаются неизменными.
Пример
Необходимо получить ближайшую будущую дату, выпадающую на определенный день недели. Вот пример плохо написанной функции:
Функция БлижайшаяДатаДняНедели(ДеньНедели) РасчетнаяДата = КонецДня(ТекущаяДатаСеанса()); Сутки = 86400; Пока ДеньНедели(РасчетнаяДата) <> ДеньНедели Цикл РасчетнаяДата = РасчетнаяДата + Сутки; КонецЦикла; Возврат РасчетнаяДата; КонецФункции
Плохо в ней то, что текущая дата (исходная точка расчета) получается как текущая дата сеанса, то есть зависит от состояния внешней системы. Такая функция не является чистой. Перенесем текущую дату в параметр, чтобы исправить ситуацию:
Функция БлижайшаяДатаДняНедели(ДатаАктуальности, ДеньНедели) РасчетнаяДата = КонецДня(ДатаАктуальности); Сутки = 86400; Пока ДеньНедели(РасчетнаяДата) <> ДеньНедели Цикл РасчетнаяДата = РасчетнаяДата + Сутки; КонецЦикла; Возврат РасчетнаяДата; КонецФункции
Правило № 4: не называйте переменные зарезервированными в языке терминами
Проблема
Понятно, что у вас не получится назвать переменную Цикл. Но ряд других стандартных терминов и предопределенных имен допустимо использовать еще и в качестве переменных.
Такой код вполне допустим в 1С:
Колонки = ТаблицаПравил.НайтиКолонки(Отбор);
Но когда вы позже захотите с помощью глобального поиска определить, где эта переменная используется, результат выдаст множество таких конструкций:
ДеревоВерсий = Новый ДеревоЗначений; ДеревоВерсий.Колонки.Добавить("НомерВерсии"); ДеревоВерсий.Колонки.Добавить("ПредставлениеНомераВерсии"); ДеревоВерсий.Колонки.Добавить("Отклонена", Новый ОписаниеТипов("Булево"));
Работать с результатом поиска будет крайне неудобно, это займет неоправданно много времени.
Как исправить
Не используйте в качестве имени переменной стандартные имена объектов, языковых конструкций и подчиненных объектов. Исключение может составлять локальная переменная в небольшой функции из пяти строк. Но даже в этом случае есть риск, что функция разрастется до более внушительных размеров.
По этой же причине мы советуем не использовать один идентификатор для переменной и названия функции, хотя это не возбраняется стандартами и часто встречается в типовых конфигурациях.
Правило № 5: вложенность условий — не больше двух
Проблема:
Об этой ошибке написано в стандартах, ее не любит Sonar, но все равно она встречается с завидной регулярностью и увеличивает цикломатическую сложность кода. Код с большой вложенностью условий и циклов крайне неудобно сопровождать и дорабатывать, он очень сложен для понимания, тем более, если это чужой код. И особенно, если на каждом уровне условия код не умещается на одном экране.
Как исправить
Два простых приема сделают ваш код на порядок лучше:
Инверсия условий.
Пример № 1
Вы можете не оставлять весь код в одном условии, при котором он должен выполняться:
Для Каждого СтрокаТаблицы Из ТоварыПоставщиков Цикл Если НЕ СтрокаТаблицы.Обработана Тогда // // исполняемый код // КонецЕсли; КонецЦикла;
А отсечь выполнение текущей итерации с помощью обратного условия:
Для Каждого СтрокаТаблицы Из ТоварыПоставщиков Цикл Если СтрокаТаблицы.Обработана Тогда Продолжить; КонецЕсли; // // исполняемый код // КонецЦикла;
Другая модификация инверсии условий применяется в случае, когда есть исполняемый код на обе ветки условия, но один из них содержит значительно меньше кода (код 1 в примере ниже).
Пример № 2
Цикломатически сложный код:
Для Каждого СтрокаТаблицы Из ТоварыПоставщиков Цикл Если СтрокаТаблицы.Обработана Тогда // Исполняемый код 1 Иначе // Исполняемый код 2 КонецЕсли; КонецЦикла;
Результат упрощения:
Для Каждого СтрокаТаблицы Из ТоварыПоставщиков Цикл Если СтрокаТаблицы.Обработана Тогда // Исполняемый код 1 Продолжить; КонецЕсли; // Исполняемый код 2 КонецЦикла;
Также код 1 можно вынести в отдельную функцию.
Вынесение вложенного кода в отдельные функции.
Если вы видите больше двух ступеней вложенности, вынесите вложенный код в отдельную функцию.
Правило № 6: понятная булева логика
Проблема
Ниже приведены сразу несколько простых приемов, которые помогут улучшить читабельность кода с булевыми выражениями. Иногда встречаются выражения, над которыми нужно изрядно поразмыслить, чтобы понять, какой результат ожидается при определенных условия. Например, может присутствовать сложное условие, которое никогда не выполняется, но на первый взгляд этого не определить. А код внутри условия состоит из нескольких сотен строк.
Как исправить
Совет № 1. Тернарный оператор
Вокруг использования тернарного оператора ходит много споров. Но если дело касается простого условия, целесообразно использовать оператор «?». Это как минимум уменьшит цикломатическую сложность кода.
Плохо:
Если А = Б Тогда Результат = Выражение1(); Иначе Результат = Выражение2(); КонецЕсли;
Лучше:
Результат = ?(А = Б, Выражение1(), Выражение2()); Если каждое из выражений достаточно длинное, можно расположить их на отдельных строках: Результат = ?(А = Б, Выражение1(), Выражение2());
Совет № 2. Логическое вычисление вместо условий
Плохо:
Если А = Б Тогда Результат = Истина; КонецЕсли;
Лучше:
Результат = (А = Б);
Совет № 3. Операция НЕ ухудшает читаемость
Плохо:
Если НЕ Результат = Неопределено Тогда КонецЕсли;
Лучше:
Если Результат <> Неопределено Тогда КонецЕсли;
Совет № 4. Разбиваем сложные условия на части
Для этого можно использовать отдельные логические переменные (которые к тому же улучшают самодокументируемость кода) или функции-обертки.
Плохо:
Если (ТекущийДоговор.Тип = Перечисления.ТипыДоговоров.СПоставщиком ИЛИ ТекущийДоговор.Тип = Перечисления.ТипыДоговоров.СКомитентом) И (ТекущееСальдо = 0 ИЛИ ДатаОстатков > ГраницаПросроченнойЗадолженности) Тогда // ... КонецЕсли;
Улучшено с логическими переменными:
ЭтоПоставщик = (ТекущийДоговор.Тип = Перечисления.ТипыДоговоров.СПоставщиком ИЛИ ТекущийДоговор.Тип = Перечисления.ТипыДоговоров.СКомитентом); БезТекущихДолгов = (ТекущееСальдо = 0 ИЛИ ДатаОстатков > ГраницаПросроченнойЗадолженности); Если ЭтоПоставщик И БезТекущихДолгов Тогда // ... КонецЕсли;
Плохо:
Если ТипЗнч(Парам1) = Тип("Число") И ТипЗнч(Парам2) = Тип("Число") Тогда // ... КонецЕсли;
Улучшено с функцией-оберткой:
Если ЭтоЧисло(Парам1) И ЭтоЧисло(Парам2) Тогда // ... КонецЕсли; // ... Функция ЭтоЧисло(Парам) Возврат ТипЗнч(Парам) = Тип("Число"); КонецФункции
Правило № 7: не смешивайте операторы с вызовом функций
Это правило не высечено в камне и следовать ему не обязательно. Но иногда оно поможет сэкономить ценные минуты при отладке кода. Его суть в том, чтобы выносить на отдельные строки вызовы функций, включаемых в выражения, или каскадный вызов функций, последовательно возвращающих результат.
Пример № 1
Было:
ВысотаТаблицы = ВысотаШапки + ТаблицаТелаТаблицы.Высота() + 3;
Стало:
ВысотаТела = ТаблицаТелаТаблицы.Высота(); ВысотаТаблицы = ВысотаШапки + ВысотаТела + 3;
Пример № 2
Было:
Результат = РаботасXML.ПрочитатьДанныеИзXML(РаботаСФайлами.ПолучитьИзФайла(ПолноеИмяФайла));
Стало:
ДвоичныеДанныеФайла = РаботаСФайлами.ПолучитьИзФайла(ПолноеИмяФайла); Результат = РаботасXML.ПрочитатьДанныеИзXML(ДвоичныеДанныеФайла);
Такой подход позволяет решить некоторые затруднения при отладке сложного кода, когда нужно следить за содержанием промежуточных переменных по ходу выполнения алгоритма. Кроме того, этим решается проблема слишком длинных строк, что может стать прямым нарушением стандарта, принятого в вашей команде.
Остались вопросы?
Проконсультируйтесь с нашими специалистами