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

Комментарии (10)


  1. ComodoHacker
    26.11.2015 11:33
    +3

    Знаете, статья получилась практически ни о чем. Как имитировать ООП в процедурном языке, думаю, все и так знают.
    А когда это оправдано и имеет смысл именно в 1С, осталось нераскрытым. Для каких задач подходит и какие преимущества дает по сравнению с процедурным стилем, предлагаемым платформой. А иначе правы те, кто говорит «такая поддержка совершенно не нужна».


    1. ComodoHacker
      26.11.2015 11:36
      +3

      Функция Прямая_Длина(Прямая)

      Длина прямой бесконечна. А то, что описали вы, называется отрезок.

      Не в ту ветку, прошу прощения.


  1. mwizard
    26.11.2015 11:41
    +1

    А еще можно было просто сделать транспайлер из расширенной грамматики с классами в базовую без оных.


  1. ComodoHacker
    26.11.2015 11:41

    никакая часть обработки не может быть вынесена в «общие модули», — весь функционал нужно помещать в один единственный модуль обработки.

    Это неверно. В качестве модулей можно использовать (и используются) другие внешние обработки.

    Нет возможности подключать конкретные модули внутри другого модуля (предложения uses или #include)

    Все общие модули конфигурации доступны для использования без дополнительных действий.


    1. ComodoHacker
      26.11.2015 11:43

      Кроме того, в последних версиях платформы появились т.н. «расширения».


  1. PQR
    26.11.2015 12:11
    +3

    ООП уже не модно.
    Расскажите, лучше, как применить функциональное программирование в 1С.
    Хочу, как минимум, filter.map.reduce для моих документов и неизменяемые типы данных!

    Вполне подойдёт вариант с трансляцией из другого языка, например Clojure1C — кто займётся?


  1. MasMaX
    26.11.2015 13:27
    +12

    Мои глаза!


  1. iliabvf
    26.11.2015 14:30
    +8

    иммитация оргазма, резиновая баба, безалкагольное пиво, ООП в 1С…


  1. Crank
    26.11.2015 17:58

    Казалось бы причем тут тег с битрикс?


  1. alkov
    27.11.2015 06:36

    Представил типовую бухгалтерию 3.0, переписанную на такое вот ООП, и испугался