[Из песочницы] ООП в 1С своими руками. Как имитировать свои классы и объекты, и зачем это нужно

image

Хочу рассказать о том, как я использую объектно-ориентированное программирование в 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-м году — примерно в это же время данный метод стал использовать и я. Поскольку в указанной статье есть существенные отличия (автор предлагает унифицировать вызов методов и доступ к свойствам класса через функции-обертки, что как бы более технологично, но на мой взгляд менее наглядно), то я решил опубликовать и собственный вариант технологии.

© Habrahabr.ru