Хочу рассказать о том, как я использую объектно-ориентированное программирование в 1С. Вернее его имитацию, т.к. в самом встроенном языке таких возможностей нет. Тем не менее, возможность создавать логически независимые, обособленные, самодостаточные фрагмены кода (да еще с инкапсулированными в них данными), весьма полезна.
Ведь их можно:
— использовать повторно внутри одного и того же проекта;
— легко и просто (не задумываясь) переносить из одного проекта в другой;
— передать кому-то еще, или выложить в Интернете для всеобщего использования как самостоятельное средство решения определенной задачи, которое соответственно также легко может быть кем-то скопировано и вставлено в собственный проект);
— имея класс, можно создать сразу несколько объектов (строить из них массивы, коллекции, списки и т.д.);
— еще какие-то плюсы, о которых я не знаю…
В этой статье будет показаны приемы имитации ООП средствами процедурно-ориентированного языка 1С.
Суть проблемы
Как известно, встроенный язык 1С не поддерживает ООП в полной мере. Есть стандартные встроенные классы платформы со своими полями, свойствами и методами. Можно создавать объекты этих классов:
МойМассив = Новый Массив;
и пользоваться ими так, как это делается в обычных универсальных языках программирования. Однако определять и реализовывать собственные классы во встроенном языке нельзя. В принципе, можно сказать, что платформа 1С не поддерживает объектно-ориентированное программирование.
Более того, от нескольких 1С-специалистов я слышал о том, что такая поддержка совершенно не нужна. Но это были именно «1С-программисты», т.е. люди которые всегда занимались разработкой только для платформы 1С, а не для компьютера, и не знакомы с классической «Computer Science». Те же, кто занимался разработкой на обычных универсальных языках и привык к возможностям ООП, столкнувшись с платформой 1С, часто испытывают сильные неудобства от невозможности строить архитектуру своей программы и вообще выражать свои мысли, свое видение реализации задачи, в виде объектной модели. Взамен ООП, встроенный язык предлагает вернуться к структурному программированию, которое также урезано по сравнению с классическими вариантами (например, языки Pascal и C). Нет возможности создавать несколько модулей общего вида (без форм) в том или ином объекте конфигурации. Это очень существенно, если например, делаешь внешнюю обработку (чтобы не привязываться к конкретной конфигурации, или просто не хочешь вносить в саму типовую конфигурацию изменения и снимать ее с поддержки) и соответственно никакая часть обработки не может быть вынесена в «общие модули», — весь функционал нужно помещать в один единственный модуль обработки. Нет возможности подключать конкретные модули внутри другого модуля (предложения uses или #include).
Uses СотрудникиОрганизаций, ТиповыеОтчеты;
#include Справочники.Банки;
Впрочем, в данной статье речь пойдет не о модулях.
Итак, как же все-таки можно средствами структурного программирования реализовать хотя бы часть основных парадигм ООП?
С полиморфизмом и свойствами типа
property MyVar: Integer read GetMyVar write SetMyVar;
ничего не выйдет (их вообще-то тоже можно сделать, но синтаксически это будет выглядеть слишком наворочено, некрасиво и соответственно неудобно и неочевидно в применении). Про разграничение доступа (private, protected, public) тоже придется забыть. А вот инкапсуляцию и наследование (в режиме доступа public) можно сделать вполне прилично.
Суть решения
Идея в общем не нова, и ее можно почерпнуть, например, в архитектуре такого мощного суперпроекта из мира Unix, как графическая библиотека GTK+. Эта библиотека реализована на языке Си, но тем не менее по факту своей архитектуры является объектно-ориентированной (см. пример инициализации библиотеки GTK+ из вышеуказанной статьи в Википедии – из него станет понятно, что имеется в виду). Возможность реализовать ООП средствами языка, который ООП не поддерживает, кроется в принятии особых соглашений о кодировании исходников.
Ведь что такое класс? Класс – это определение данных и методов их обработки «в одном флаконе». А объект – это, по сути динамически созданный набор данных, который неявно передается в качестве параметра методам класса для того, чтобы методы могли обрабатывать эти самые данные. И если средствами языка, данные и методы запихнуть в «один флакон» не получится, то воспользуемся для этого условной надстройкой над возможностями языка – соглашениями о кодировании (Code Conventions).
Будем в качестве хранилища полей объекта использовать структуры (в Си – это встроенный тип данных, в 1С – встроенный класс – хеш-таблица).
Объект = Новый Структура;
Объект. Вставить(«Поле1», 0);
Объект. Вставить(«Поле2», “”);
…
Объект. Вставить(«ПолеN», Новый Массив);
В качестве методов будем использовать процедуры и функции, используя следующие соглашения об их наименовании:
Процедура ИмяКласса_ИмяМетода(СтруктураОбъект, …);
Функция ИмяКласса_ИмяМетода(СтруктураОбъект, …);
Как видно вместо неявной передачи параметра-указателя на набор полей объекта, он переда-ется явно 1-м параметром. Следом идет обычный набор параметров метода.
Рассмотрим следующий пример:
Пример класса ПочтовоеОтделение
////////////////////////////////////////////////////////////////////////////////
// Реализация класса ПочтовоеОтделение
// Создание полей объекта в виде структуры.
//
// Параметры:
// Нет.
// Возврат:
// Возвращает созданную структуру
//
Функция ПочтовоеОтделение_СоздатьОбъект() Экспорт
// Все это конечно можно делать в конструкторе, но поскольку
// в конструкторах классических языков, поддерживающих ООП,
// это делается неявно, то лучше вынести создание полей в
// отдельную функцию, чтобы не загромождать конструктор,
// т.к. полей у реального (не демонстрационного) объекта
// может быть много.
Отделен = Новый Структура;
// Общие параметры сущности
Отделен.Вставить("Индекс", Неопределено); // Индекс почтового отделения
Отделен.Вставить("Адрес", Адрес_Конструктор()); // Адрес почтового отделения
Возврат Отделен;
КонецФункции
// Имитирует конструктор объекта.
//
// Параметры:
// Индекс - строковое представление почтового индекса отделения
// Адрес - ссылка на объект, содержащий адрес отделения
// Возврат:
// (Структура) - Структура с полями объекта
//
Функция ПочтовоеОтделение_Конструктор(Индекс, Адрес) Экспорт
// Создать объект
Отделен = ПочтовоеОтделение_СоздатьОбъект();
// Выполнить начальную инициализацию
Отделен.Индекс = Индекс;
Адрес_Assign(Отделен.Адрес, Адрес);
Возврат Отделен;
КонецФункции
// Имитирует деструктор объекта - освобождает ресурсы.
//
// Параметры:
// Отделен - ссылка на объект
//
Процедура ПочтовоеОтделение_Деструктор(Отделен) Экспорт
// Во встроенном языке не нужно явно удалять ранее созданные объекты 1С.
// Но здесь можно завершить работу с какими-либо внешними ресурсами.
// Например: закрыть подключение к базе данных.
Адрес_Деструктор(Отделен.Адрес);
КонецПроцедуры
// Возвращает полный почтовый адрес отделения.
//
// Параметры:
// Отделен - ссылка на объект
// Возврат:
// (Строка) - Полный почтовый адрес отделения
//
Функция ПочтовоеОтделение_ПолучитьАдресОтделения(Отделен) Экспорт
АдресОтделения = Отделен.Индекс + ", " + Адрес_ВСтроку(Отделен.Адрес);
Возврат АдресОтделения;
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Реализация класса Адрес
// Создание полей объекта в виде структуры.
//
// Параметры:
// Нет.
// Возврат:
// Возвращает созданную структуру
//
Функция Адрес_СоздатьОбъект() Экспорт
Адрес = Новый Структура;
// Общие параметры сущности
Адрес.Вставить("Регион", Неопределено); // Наименование региона
Адрес.Вставить("Город", Неопределено); // Наименование города
Адрес.Вставить("Улица", Неопределено); // Наименование улицы
Адрес.Вставить("НомерДома", Неопределено); // Номер дома
Возврат Адрес;
КонецФункции
// Имитирует конструктор объекта.
//
// Параметры:
// НЕТ
// Возврат:
// (Структура) - Структура с полями объекта
//
Функция Адрес_Конструктор() Экспорт
// Создать объект
Адрес = Адрес_СоздатьОбъект();
Возврат Адрес;
КонецФункции
// Имитирует деструктор объекта - освобождает русурсы.
//
// Параметры:
// Адрес - ссылка на объект
//
Процедура Адрес_Деструктор(Адрес) Экспорт
КонецПроцедуры
// Устанавливает основные атрибуты адреса.
//
// Параметры:
// Адрес - ссылка на объект
// Регион - название региона
// Город - название города
// Улица - название улица
// НомерДома - номер дома
//
Процедура Адрес_УстАтриб(Адрес, Регион, Город, Улица, НомерДома) Экспорт
Адрес.Регион = Регион;
Адрес.Город = Город;
Адрес.Улица = Улица;
Адрес.НомерДома = НомерДома
КонецПроцедуры
// Возвращает строковое представление адреса.
//
// Параметры:
// Адрес - ссылка на объект
// Возврат:
// (Строка) - Строковое представление адреса
//
Функция Адрес_ВСтроку(Адрес) Экспорт
АдресСтрока = Адрес.Регион + ", " +
"г. " + Адрес.Город + ", " +
"ул. " + Адрес.Улица + ", " +
"д. " + Адрес.НомерДома;
Возврат АдресСтрока;
КонецФункции
// Копирует данные из объекта Адрес2 в объект Адрес1.
// Все предыдущие данные Адрес1 будут утеряны.
//
// Параметры:
// Адрес1 - ссылка на объект назначение
// Адрес2 - ссылка на объект - источник
//
Процедура Адрес_Assign(Адрес1, Адрес2) Экспорт
Адрес1.Регион = Адрес2.Регион;
Адрес1.Город = Адрес2.Город;
Адрес1.Улица = Адрес2.Улица;
Адрес1.НомерДома = Адрес2.НомерДома
КонецПроцедуры
Это был пример инкапсуляции. Наследование выглядит чуть более коряво.
Классы: Линия, Прямая, Окружность
////////////////////////////////////////////////////////////////////////////////
// Реализация класса Линия
// Создание полей объекта в виде структуры.
//
// Параметры:
// Нет.
// Возврат:
// Возвращает созданную структуру
//
Функция Линия_СоздатьОбъект() Экспорт
Линия = Новый Структура;
// Общие параметры сущности
Линия.Вставить("X1", 0); // X-координата центра
Линия.Вставить("Y1", 0); // Y-координата центра
Возврат Линия;
КонецФункции
// Имитирует конструктор объекта.
//
// Параметры:
// X1, Y1 - координаты начальной (центральной) точки линии
// Возврат:
// (Структура) - Структура с полями объекта
//
Функция Линия_Конструктор(X1, Y1) Экспорт
// Создать объект
Линия = Линия_СоздатьОбъект();
// Выполнить начальную инициализацию
Линия.X1 = X1;
Линия.Y1 = Y1;
Возврат Линия;
КонецФункции
// Имитирует деструктор объекта - освобождает ресурсы.
//
// Параметры:
// Линия - ссылка на объект
//
Процедура Линия_Деструктор(Линия) Экспорт
КонецПроцедуры
// Устанавливает атрибуты линии.
//
// Параметры:
// Линия - ссылка на объект
// X1, Y1 - координаты начальной (центральной) точки линии
//
Процедура Линия_УстАтриб(Линия, X1, Y1) Экспорт
Линия.X1 = X1;
Линия.Y1 = Y1;
КонецПроцедуры
// Возвращает длину линии.
//
// Параметры:
// Линия - ссылка на объект
// Возврат:
// (Число) - Длина линии
//
Функция Линия_Длина(Линия) Экспорт
Возврат 0; // Заглушка - данная функция должна быть переопределена в потомках
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Реализация класса Прямая - потомок класса Линия.
// Создание полей объекта в виде структуры.
//
// Параметры:
// Нет.
// Возврат:
// Возвращает созданную структуру
//
Функция Прямая_СоздатьОбъект() Экспорт
Прямая = Линия_СоздатьОбъект();
// Общие параметры сущности
Прямая.Вставить("X2", 0); // X-координата конечной точки
Прямая.Вставить("Y2", 0); // Y-координата конечной точки
Возврат Прямая;
КонецФункции
// Имитирует конструктор объекта.
//
// Параметры:
// X1, Y1 - координаты начальной точки прямой
// X2, Y2 - координаты конечной точки прямой
// Возврат:
// (Структура) - Структура с полями объекта
//
Функция Прямая_Конструктор(X1, Y1, X2, Y2) Экспорт
// Создать объект
Прямая = Прямая_СоздатьОбъект();
// Выполнить начальную инициализацию
Прямая_УстАтриб(Прямая, X1, Y1, X2, Y2);
Возврат Прямая;
КонецФункции
// Имитирует деструктор объекта - освобождает ресурсы.
//
// Параметры:
// Прямая - ссылка на объект
//
Процедура Прямая_Деструктор(Прямая) Экспорт
КонецПроцедуры
// Устанавливает атрибуты объекта.
//
// Параметры:
// Прямая - ссылка на объект
// X1, Y1 - координаты начальной точки прямой
// X2, Y2 - координаты конечной точки прямой
//
Процедура Прямая_УстАтриб(Прямая, X1, Y1, X2, Y2) Экспорт
Линия_УстАтриб(Прямая, X1, Y1);
Прямая.X2 = X2;
Прямая.Y2 = Y2;
КонецПроцедуры
// Возвращает длину линии.
//
// Параметры:
// Прямая - ссылка на объект
// Возврат:
// (Число) - Длина линии
//
Функция Прямая_Длина(Прямая) Экспорт
// Найти длину проекции линии на ось X
ДлинаX = Прямая.X2 - Прямая.X1;
// Найти длину проекции линии на ось Y
ДлинаY = Прямая.Y2 - Прямая.Y1;
// Найти гипотенузу по теореме Пифагора
ДлинаЛин = Sqrt(ДлинаX*ДлинаX + ДлинаY*ДлинаY);
Возврат ДлинаЛин;
КонецФункции
////////////////////////////////////////////////////////////////////////////////
// Реализация класса Окружность (Окружн) - потомок класса Линия.
// Создание полей объекта в виде структуры.
//
// Параметры:
// Нет.
// Возврат:
// Возвращает созданную структуру
//
Функция Окружн_СоздатьОбъект() Экспорт
Окружн = Линия_СоздатьОбъект();
// Общие параметры сущности
Окружн.Вставить("R", 0); // радиус окружности
Возврат Окружн;
КонецФункции
// Имитирует конструктор объекта.
//
// Параметры:
// X, Y - координаты центра окружности
// R - радиус окружности
// Возврат:
// (Структура) - Структура с полями объекта
//
Функция Окружн_Конструктор(X, Y, R) Экспорт
// Создать объект
Окружн = Окружн_СоздатьОбъект();
// Выполнить начальную инициализацию
Окружн_УстАтриб(Окружн, X, Y, R);
Возврат Окружн;
КонецФункции
// Имитирует деструктор объекта - освобождает ресурсы.
//
// Параметры:
// Окружн - ссылка на объект
//
Процедура Окружн_Деструктор(Окружн) Экспорт
КонецПроцедуры
// Устанавливает атрибуты объекта.
//
// Параметры:
// Окружн - ссылка на объект
// X, Y - координаты центра окружности
// R - радиус окружности
//
Процедура Окружн_УстАтриб(Окружн, X, Y, R) Экспорт
Линия_УстАтриб(Окружн, X, Y);
Окружн.R = R;
КонецПроцедуры
// Возвращает длину линии.
//
// Параметры:
// Окружн - ссылка на объект
// Возврат:
// (Число) - Длина линии
//
Функция Окружн_Длина(Окружн) Экспорт
ДлинаЛин = 2*3.14*Окружн.R;
Возврат ДлинаЛин;
КонецФункции
Естественно никаких виртуальных методов и полиморфизма здесь нет. Только переопределение одноименных методов в потомках. Одноименными они опять же являются с точки зрения логики разработки.
Резюме.
Как мы видим, описанный прием оформления исходников на языке 1С, расширяет возможности написанных таким образом фрагментов кода в плане наглядности, универсальности и переносимости.
В дальнейших публикациях я планирую рассказать о некоторых полезных штуках (можно сказать, инструментах), которые можно сразу взять и использовать в своих 1С-программах. Их код будет оформлен как раз в виде таких вот классов.
Скачать тексты рассмотренных в статье примеров (обработка для 1С 8.2) можно здесь.
Всем удачи. До встречи.
P.S.: В процессе подготовки этой статьи наткнулся на описание аналогичного подхода на портале «Инфостарт». Написано, кажется в 2012-м году — примерно в это же время данный метод стал использовать и я. Поскольку в указанной статье есть существенные отличия (автор предлагает унифицировать вызов методов и доступ к свойствам класса через функции-обертки, что как бы более технологично, но на мой взгляд менее наглядно), то я решил опубликовать и собственный вариант технологии.