Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Типичная задача на создание велосипеда.
Проблема code reuse в ORM при совместной разработке.
Как аннотации Java позволяют создавать собственные стандарты за чашечкой кофе.
Стандарты на разработку подсистем\компонент в ORM.
Типичная задача на создание велосипеда
При работе с ORM (object relation mapping) все время преследует ощущение постоянного создания монолитного приложения – один раз привязался к какой-либо сущности (например, справочник Контрагенты) и весь код заполнен ссылками на эту конкретную реализацию.
А разве с обычными библиотеками кода не так? Библиотеки Java (package) в отличии от ORM сущностей имеют только зависимости, когда одна библиотека зависит от другой, но это успешно решается через maven. Да ссылка в коде на конкретную библиотеку также создает трудности при переходе на подобную даже в простых случаях ( пример , Java Logging: история кошмара / Хабр (habr.com) ), но это можно решить классами обертками, если предполагается сменить библиотеку на другую реализацию. В ORM все сложнее поскольку оно отражает явные (по foreign key \ constraints ) или неявные по дизайну СУБД связи между таблицами, и такие зависимости maven-подобным подходом не решишь Maven – Introduction (apache.org).
Можно ли независимо разрабатывать в ORM совместимые между собой справочник «Контрагентов» и использующий его документ «Платежное поручение», но при этом избежать жестких зависимостей? Спасут ли нас микросервисы? Быть или не быть - вот в чем вопрос?
Возьмем практическую задачу на 1С. ORM 1С это хороший пример визуального Low code ORM (object relation mapping) с возможностями слияния метаданных.
О организации ORM 1С хорошо написано тут Как мы в 1С: Предприятии работаем с моделями данных (или «Почему мы не работаем с таблицами?») | 1С:Зазеркалье (1c.ru) .
За мою практику мне часто приходилось автоматизировать на 1С учет нерезидентов, учет которых ведется без плана счетов РФ.
Для этой задачи типовая 1С Бухгалтерия избыточна поскольку:
· План счетов для управленческого учета у каждого нерезидента свой, но соответствует стандартам IFRS\GAAP
· Типовые документы (Платежные поручения, накладные и т.д.) не подходят, поскольку реализованы для РФ.
· Нужен не только финансовый (бухгалтерский) , но и управленческий учет с тем же планом счетов.
· Необходимость англоязычного перевода метаданных
«1С:Бухгалтерия КОРП МСФО» — самая популярная программа для бухгалтерии, ведения учета по МСФО для отдельных компаний! (1c.ru) , для этой задачи не очень подходит поскольку подразумевает перекладку учета РФ в МСФО (IFRS) . В случае учета нерезидента речь идет о учете по МСФО, без перекладок \ мэппинга.
В идеале хотелось бы собрать подобную систему из
· 1С Библиотека стандартных подсистем 1С:Библиотека стандартных подсистем (1c.ru) в полном виде (далее 1С БСП)
· Журнал бухгалтеского учета «хозрасчетный» с поддержкой управленческого и финансового учета + стандартные отчеты (оборотно-сальдовые ведомости, главные книги , анализ счета и т.д.)
· Операционный блок уже будет написан свой – поскольку это уже алгоритмы формирования проводок пишутся под нужный план счетов МСФО,а он к сожалению нестандартный.
Почему западные решения (SAP, Sun Account и т.д.) менее эффективны – это тема отдельной статьи, и поверьте соотношение цена\время\результат не в их пользу, и их устриц с ними я уже наелся. Например, вопрос эмуляции двойной записи (для полноценной проводки) - в западных системах это постоянная боль.
Но в 1С тоже не все гладко – тот же 1С БСП как продукт, поставляется разобранным т.е. его нужно еще собрать с вставками кода (обсуждалось тут Конфигурация на БСП с минимальными усилиями - Форум.Инфостарт (infostart.ru) ).
А собранный он только в типовой 1С Бухгалтерии и других конфигурациях,и в итоге приходится брать 1С Бухгалтерию – игнорировать ненужное, дорабатывать нужное. При таком подходе, есть свои очевидные недостатки, которые проявляются при попытках обновлять БСП или делится отдельными подсистемами.
Мечта собирать решение ORM из отдельных компонент все не дает покоя… . Ведь неоперационный блок (ОС, НМА, РБП, материалы) тоже как правило имеет единые алгоритмы, но разные печатные формы. Любой более менее крупный проект, требует большей независимости в разработке, а не ожидания пока один сделает справочник «Контрагентов», а другой уже будет его прикручивать к «Платежному поручению»
Проблема code reuse в ORM при совместной разработке.
Возьмем еще более простой пример для любой ORM. Вася хочет сделать самый лучший справочник контрагентов, а Василиса самое лучшее платежное поручение, которое использует справочник контрагентов. Если Вы думаете, что справочник контрагентов это просто – Вы не правы. Минимальный функционал для РФ
· Сам справочник с реквизитами (ИНН\КПП\ОГРН ) возможностями поиска
· Различная связанная контактная информация
· Проверка и ведение адресов по ФИАС (классификатор адресов)
· Банковские реквизиты контрагента.
А для международной версии тоже самое, но с учетом специфики региона (IBAN , SWIFT т.д.). В реальной жизни по контрагентам идет учет договоров, они бывают клиентами и постащиками или госорганами, физлицами, индивидуальными предпринимателями и как следствие функционал растет почти до уровня CRM и в терминах 1С это подсистема! И каждый раз изобретать велосипед не хочется. А еще любой хороший код должен соответствовать best practice, содержать обработку ошибок, быть оптимальным и прочее ... То что Вася и Василиса справляться при совместной разработке в 1С это очевидно, но какая связь будет между этими метаданными в терминах 1С?
В типовой конфигурации вот такая
У Васи проблем нет – на его справочник(и) ссылаются, а у Василисы проблема – ее Платежное поручение полностью зависит от реализации Васи (наименования реквизитов\справочников, типы и т.д.) . Поскольку в 1С по описанию метаданных может сразу сгенерировать форму (преимущество RAD), то эти ссылки проникнут и туда. В коде тоже будет достаточно отсылок к конкретной реализации справочника Контрагенты. Получаем два следствия
· Разработка Василисы возможна, когда Вася набросает хотябы структуру метаданных, т.е. о независимой разработке уже речи нет.
· Если кому-то больше нравится справочник Контрагентов Коли («КонтрагентыКоли»)_ и платежное поручение Василисы, то оторваться от справочника Контрагентов Васи можно только перекодированием решения.
· Для распространения своего решения «Платежного поручения» Василисе придется тянуть и конкретную реализацию справочника контрагентов
Таким образом справочник Контрагентов Васи будет иметь гарантированный круг поклонников\иц, а Платежное поручение Василисы будут использовать только любители покодировать. Не получается у Васи и Василисы синергии. А если с годами (как это часто бывает) Вася перестанет совершенствовать свое решение или игнорировать требования Василисы в пользу других, лучшего справочника Контрагентов и лучшего Платежного поручения не получится.
Как аннотации Java позволяют создавать собственные стандарты за чашечкой кофе.
Кто то скажет , а пусть Вася и Василиса оформят свои решения в виде Микросервисов, используя SOAP, JSON и подобные технологии. Они могут даже не встречаться, а когда встретятся Василисе достаточно сделать перекладку данных с преобразованиями типов в рамках своих API. Но тут опять Василиса будет опять в невыгодном положении, поскольку основную работу по преобразованиям (в строго-типизированном Java это кошмар) придется сделать ей. А если кто-то захочет сделать отчет по платежам в разрезе контрагентов нерезидентов (признак у контрагента) – просто и без нарушений концепции микросервиса это сделать нельзя, даже если данные находятся в одном Instance СУБД.
Поэтому - возвращаемся обратно к ORM и сделаем это на Java с аннотациями (Можно на JPA ,но как увидите роли это не играет)
Прежде чем что-то писать Васе и Василисе нужно договорится о стандартах . В данном примере на Java, но наверняка в других языках есть похожие механизмы.
1) Определится какой ORM используется напр JPA или расширенные реализации Spring DATA, Hibernate. Это будет важно, когда придется делать различные отчеты в разрезе платежных поручений и реквизитов контрагентов (соединение). Иначе проблемы будут как у микросервисов (см выше)
2) Договорится о универсальном идентификакторе (ключе) записей в таблицах (далее УИД). В 1С для этого используется GUID который неявно генерируется ORM платформы . Это позволит из платежного поручения легко ссылаться на записи, разных реализаций справочника контрагентов
3) Способ предоставления данных. В нашем примере будут использоваться процедуры get и set для каждого реквизита объекта. Оба разработчика могут называть их как угодно (даже одинаковые по смыслу) главное чтобы для чтения использовался get* , а для установки значения set* . Это распространенная практика в стандартах Java, например, в JSF для связи метода представляющего значение и места на странице HTML , желающие могут почитать Геттеры и сеттеры в Java | Getters and Setters (javarush.com).
4) Формат аннотации, который будут использоваться для мэппинга get и set
Итак создаем Package Standards
Определим класс FieldValue он нужен для того чтобы передавать между методами поле любого типа. В Java жесткая типизация, кроме того разработчики пишут независимо и Вася может сделать ИНН типа Integer, а Василиса типа String
package Standards;
import java.math.*;
import java.util.*;
//Позволяет возвращать значение поля таблицы\объекта независимо от его типа
public class FieldValue {
private BigInteger IDValue;
private Long LongValue;
private String StringValue;
private Date DateValue;
private String FieldType;
//Конструкторы для каждого типа данных, для упрощения идентификации типа фиксируем его
public FieldValue(BigInteger IDValue) {
this.IDValue = IDValue;
this.FieldType = "BigInteger";
}
;
public FieldValue(String StringValue) {
this.StringValue = StringValue;
this.FieldType = "String";
}
;
public FieldValue(Date DateValue) {
this.DateValue = DateValue;
this.FieldType = "Date";
}
;
public FieldValue(Long LongValue) {
this.LongValue = LongValue;
this.FieldType = "Long";
}
;
public String toString() {
switch (FieldType) {
case "BigInteger":
return this.IDValue.toString();
case "String":
return this.StringValue;
case "Date":
return this.DateValue.toString();
case "Long":
return this.LongValue.toString();
default:
return null;
}
}
;
}
Определяем нашу аннотацию, это по сути определение интерфейса специального вида с префиксом @ . Там мы определим свойства аннотации которые будем использовать для мэппинга get* и set* методов. Что такое аннотация? Lesson: Annotations (The Java™ Tutorials > Learning the Java Language) (oracle.com)
“Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.
Annotations have a number of uses, among them:
· Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings.
· Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.
· Runtime processing — Some annotations are available to be examined at runtime.”
Стало понятнее? Я думаю нет, потому что важно не что это , а как они обрабатываются. Это будет видно на конкретном примере.
Мы определили аннотацию, которая доступна при исполнении (@Retention(RetentionPolicy.RUNTIME), применяется к любому элементу класса @Target(ElementType.TYPE), может применятся к одному классу несколько раз (@Repeatable(LinkSubsystemS.class))
package Standards;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(LinkSubsystemS.class)
public @interface LinkSubsystem {
String LinkedClass();
String LinkedGetter();
String TargetSetter();
}
Поскольку эта аннотация может повторятся для нескольких мэппингов, нам нужно создать еще один класс LinkSubsystemS
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface LinkSubsystemS {
LinkSubsystem[] value();
}
Аннотация сама по себе работать не будет, если для нее нет процедуры обработки. Напишем собственный парсер аннотации. Именно этот класс дает возможность использовать аннотации. В Java package java.lang.reflect.* позволяет работать с метаданными класса (т.е. из программы смотреть для любого класса его методы, свойства) . Вот тут творится та самая магия аннотаций
import java.lang.reflect.*;
import java.math.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LinkSubsystemParser {
//Исполняем метод линкованого объекта для получения информации. Для Setter нужно передавать устанавливаемые значения это сложнее но тоже возможно стандартизировать
//Достаточно передавать только определение метаданных класса Class<?> (в данном случае платежное поручение) ,
//LinkedObject - объект который используем в платежном поручении (конкретный экземпляр справочника контрагентов), универсальный LikedObjectID, и название set* метода чтобы по нему вычислить get* метод
//Возвращаем универсальный для нас объект FieldValue
public FieldValue ExecuteGetMethodByMap(Class<?> ClassConsumer, Object LinkedObject, BigInteger LikedObjectID, String SetMethodName) {
try {
//Внимание!!! переменные я назвал с *Counterpart* только для понимания, на самом деле метод может мэппить геттеры и сеттеры любых классов
//Получаем аннотации которые указали у класса потребителя (платежное поручение, которое использует контрагентов). Аннотации получаются из описания метаданных
LinkSubsystem LinkedCounterpartGetters[] = ClassConsumer.getAnnotation(LinkSubsystemS.class).value();
//Вот тут вычисляем метод get справочника контрагентов, соотвествующий методу set платежного поручения
//Обходим привязанную аннотацию, она может состоять из нескольких привязок
for (LinkSubsystem LinkedCounterpartGetterItem : LinkedCounterpartGetters) {
//Если в соответствие нужный нам метод set то по нему можно получить метод get справочника контрагентов
//Вот так работать не будет LinkedCounterpartGetterItem.TargetSetter()==SetMethodName потомучто String это объект в отличии от string
if (LinkedCounterpartGetterItem.TargetSetter().equals(SetMethodName)) {
Method CounterpartGetter = LinkedObject.getClass().getMethod(LinkedCounterpartGetterItem.LinkedGetter(), BigInteger.class);
Object GetterResult = CounterpartGetter.invoke(LinkedObject, LikedObjectID);
return (FieldValue) GetterResult;
};
}
return null;
} catch (Throwable ex) {
System.out.println(ex);
return null;
}
}
;
}
Теперь Вася может создавать класс Supplier даже не уведомляя Василису о наименованиях, но следуя оговоренным стандартам. Здесь представлены классы обертки без JPA чтобы сократить демо код
package BestCounterpartDir;
import java.math.BigInteger;
import java.util.HashMap;
//Класс соотвествует записи постащика в СУБД с несколькими телефонами
public class SupplierData {
public BigInteger ID;
public String FullName;
public Long INN;
public String Telephone;//Основной телефон
public HashMap<BigInteger, String> Telephones;
public SupplierData(BigInteger ID, String FullName, Long INN, String Telephone, HashMap<BigInteger, String> Phones) {
//Простая инициализация
this.ID = ID;
this.FullName = FullName;
this.INN = INN;
this.Telephone = Telephone;
this.Telephones = null;
}
;
}
//Класс соотвествует справочнику поставщиков в СУБД . Реквизиты с префиксом Selected отражают текущего выбранного поставщика
//Выбор поставщика осуществляется методом selectByName
public class Suppliers {
private BigInteger IDCounter;
private BigInteger SelectedID;
private String SelectedFullName;
private Long SelectedINN;
private String SelectedTelephone;
//Заглушка Здесь должна быть релазиция таблицы поставщиков, можно все сделать через JPA, но кода будет много
//Для примера делаем просто HashMap
private HashMap<String, SupplierData> SuppliersTable;
//Для простоты примера, заполняем начальные данные прямо в конструкторе
public Suppliers() {
//Заглушка. Чтобы не загромождать демо код инициализацию класса данными делаем сразу в конструкторе
SuppliersTable = new HashMap<String, SupplierData>();
if (IDCounter == null) {
IDCounter = BigInteger.valueOf(5000000);
};
IDCounter = this.IDCounter.add(BigInteger.valueOf(1));
SuppliersTable.put("ООО 1С", new SupplierData(IDCounter, "ООО 1С", 7709860400L, "+7 495 688 90 02", null));
IDCounter = this.IDCounter.add(BigInteger.valueOf(1));
SuppliersTable.put("ООО SAP", new SupplierData(IDCounter, "ООО SAP", 7705058323L, "8 800 200 01 28", null));
IDCounter = this.IDCounter.add(BigInteger.valueOf(1));
SuppliersTable.put("ООО Такском", new SupplierData(IDCounter, "ООО Такском", 7704211201L, " 8 495 730 73 45", null));
} ;
//Поик и выбор поставщика если он еще не выбран
public BigInteger selectByName(String NameMask) {
SupplierData SelectedSupplier = SuppliersTable.get(NameMask);
if (SelectedSupplier == null) {
return null;
}
//Устанавливаем выбранного из справочника поставщика, своего рода кэш
this.SelectedID = SelectedSupplier.ID;
this.SelectedFullName = SelectedSupplier.FullName;
this.SelectedINN = SelectedSupplier.INN;
this.SelectedTelephone = SelectedSupplier.Telephone;
return SelectedSupplier.ID;
} ;
//Интерфейсный метод геттеры get* значение поля. Если поставщик не выбран он ищется по ID
//
public FieldValue getFullName(BigInteger ID) {
//Если контрагент выбран возвращем значение, в перспективе можно реализовать автопоиск по ID
return (this.SelectedID != null & this.SelectedID.equals(ID) ? new FieldValue(this.SelectedFullName) : null);
}
;
//Если контрагент выбран возвращем значение, в перспективе можно реализовать автопоиск по ID
public FieldValue getPhone(BigInteger ID) {
return (this.SelectedID != null & this.SelectedID.equals(ID) ? new FieldValue(this.SelectedTelephone) : null);
} ;
public FieldValue getINN(BigInteger ID) {
return (this.SelectedID != null & this.SelectedID.equals(ID) ? new FieldValue(this.SelectedINN.longValue()) : null);
};
}
Василиса может создать свое платежное поручение, используя отладочную версию справочника контрагентов, где только нужные ей get* методы. В методе setCounterPart она использует парсер аннотаций который сделан ранее в Package Standards
package BestPaymentOrder;
import java.math.*;
import java.util.*;
import Standards.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
public class PaymentOrder {
private Date PaymentDate;
private Integer PaymentAmount;
//По соглашению ID типа BigInteger
private BigInteger CounterPartID;
private String CounterPartName;
private String TAXNumber;
private String Telephone;
//Конструкторы нужно оставлять по умолчанию - они не наследуются public PaymentOrder(){};
//Устанавливаем дату время , контрагента и его ревизиты используя данные аннотаций. По сути тут идет обработка аннотаций, в которых содержится мэппинг getters, setters
public void setCounterPart(BigInteger SelectedCounterpartID, Object CounterpartObj) throws Throwable {
this.CounterPartID = SelectedCounterpartID;
// Используем наш Parser аннотаций, используем мэппинг в методах аннотаций чтобы исполнить соответствующий get* контрагента для соответствующего set* платежного поручения
LinkSubsystemParser AnnotationParser = new LinkSubsystemParser();
FieldValue GetterResult = AnnotationParser.ExecuteGetMethodByMap(this.getClass(), CounterpartObj, SelectedCounterpartID, "setTelephone");
this.Telephone = GetterResult.toString();
GetterResult = AnnotationParser.ExecuteGetMethodByMap(this.getClass(), CounterpartObj, SelectedCounterpartID, "setTAXNumber");
//Заметьте у контрагента ИНН это число, а у платежного поручения это строка. Можно делать и более сложные преобразования если за основу взять XML
this.TAXNumber = GetterResult.toString();
GetterResult = AnnotationParser.ExecuteGetMethodByMap(this.getClass(), CounterpartObj, SelectedCounterpartID, "setCounterPartName");
this.CounterPartName = GetterResult.toString();
} ;
public void setPayment(Date PaymentDate, Integer PaymentAmount) {
this.PaymentDate = PaymentDate;
this.PaymentAmount = PaymentAmount;
} ;
public void PrintPaymentOrder() {
System.out.println("Дата " + this.PaymentDate);
System.out.println("Сумма " + this.PaymentAmount);
System.out.println("Получатель " + this.CounterPartName);
System.out.println("ИНН Получателя " + this.TAXNumber);
System.out.println("Телефон " + this.Telephone);
} ;
public void setTAXNumber(FieldValue FldTAXNumber) {/*код с преобразованием типов*/
} ;
public void setTelephone(FieldValue FldTelephone) {/*код с преобразованием типов*/
} ;
public void setCounterPartName(FieldValue FldCounterPartName) {/*код с преобразованием типов*/
};
}
И наконец вишенка на торте – как Василисе подключить справочник контрагентов Василия. Она просто в аннотациях устанавливает мэппинг между get* методами Васи и своими set* методами. Если захочется подключить справочник контрагентов Коли достаточно сделать тоже самое и это будет работать
Если кому то захочется использовать платежное поручение Василисы со справочником Пети, достаточно унаследовать класс class MyPaymentOrder extends PaymentOrder {}; и аннотировать его.
package codereuse;
import BestCounterpartDir.*;
import BestPaymentOrder.*;
import Standards.*;
import java.math.*;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
@LinkSubsystem(LinkedClass = "Suppliers", LinkedGetter = "getFullName", TargetSetter = "setCounterPartName")
@LinkSubsystem(LinkedClass = "Suppliers", LinkedGetter = "getPhone", TargetSetter = "setTelephone")
@LinkSubsystem(LinkedClass = "Suppliers", LinkedGetter = "getINN", TargetSetter = "setTAXNumber")
class MyPaymentOrder extends PaymentOrder {};
public class CodeReuse {
public static void main(String[] args) {
// TODO code application logic here
BigInteger SelectedCounterPartID;
Suppliers CounterPart = new Suppliers();
SelectedCounterPartID = CounterPart.selectByName("ООО 1С");
MyPaymentOrder PO = new MyPaymentOrder();
try {
Calendar SysDate = new GregorianCalendar(2023, 03, 07);
PO.setPayment(SysDate.getTime(), 55000);
PO.setCounterPart(SelectedCounterPartID, CounterPart);
PO.PrintPaymentOrder();
} catch (Throwable e) {
return;
}
}
}
И результат
Дата Fri Apr 07 00:00:00 MSK 2023
Сумма 55000
Получатель ООО 1С
ИНН Получателя 7709860400
Телефон +7 495 688 90 02
Стандарты на разработку подсистем\компонент в ORM
С помощью механизма аннотаций можно разрабатывать свои стандарты. Мы можем не только устанавливать соответствия между методами get* и set* для свойств класса, но и для наборов записей getRows* setRows* . Получится что-то аналогичное DAO библиотекам.
Вопросы которые возникают на этом пути
Есть ли подобные решения на базе которых может развиться стандарт (не обязательно Java) ?
Меня он очень интересует, и возможно в комментариях подскажут.
Как сделать реализацию стандарта оптимальной по производительности?
Проблемы современных реализаций ORM написаны тут, но они решаемы
Концепция ORM как двигатель прогресса — выдержит ли ее ваша СУБД? / Хабр (habr.com)
Концепция ORM как двигатель прогресса – выявит слабое место Вашей СУБД / Хабр (habr.com)
Delayed durability поможет вашему ORM увеличить производительность на 50% и более, если Вы только будете использовать … / Хабр (habr.com)
Готовность ORM к горизонтальному маштабированию?
Без полноценной реализации этой темы, невозможно решать вопросы с производительностью
Язык мой – враг мой. Архитектору о будущем 1С | 1CUnlimited | Дзен (dzen.ru)
Если выражаться языком ООП мы пытаемся достичь слабой связанности между классом PaymentOrder и Supplier см цитату
«Two or more classes are said to have loose coupling when they do not rely on each other to operate. If one class cannot be created without creating another class first, for example, their coupling is said to be tight.»
Очевидно, что внедрение такого стандарта возможно только при спонсировании заинтересованным производителем софта, ведь рынок компонент \ подсистем не возникает на пустом месте. 1С достигла успехов в партнерских решениях со статусом «1С Совместимо» Справочник «Внедренные решения» (1c.ru) . Но решения там являются либо дополнительными подсистемами для типовых конфигураций 1С. Либо самостоятельные решения, но с обменами для типовых конфигураций 1С т.е. все привязано тем или иным способом к типовым решениям 1С. По сравнению со «свободным» ПО , это конечно плюс в направлении совместимости, но ограничения четко видны.
В теории таже 1С могла бы разработать БСП и платформу 1С Предприятие с полноценной разработкой в ORM относительно независимых подсистем. Но визуально в стиле RAD (Rapid application development) это требует усилий, а не добавление очередной «фичи». Если делать это в стиле Java мы получим огромное количество кода и система 1С перестанет быть RAD . Вы же не хотите этого или все-таки хотите? Я думаю, что в прикладных Frameworks появление подобных стандартов более реально, чем в стандартах Java. Ведь Java и его конкуренты, ориентированы на другие цели, нежели просто прикладная разработка. Подписывайтесь на следующие серии «1C без ограничений» на нашем канале t.me/Chat1CUnlimited . Код могу выложить на популярный, но полностью доступный в РФ аналог github .