Implementing your IoC container

Published on September 10, 2018

Implementing your IoC container

image

Introduction


Every novice developer should be familiar with the concept of Inversion of Control.

Almost every new project now begins with the choice of the framework, through which the principle of dependency injection will be implemented.

Inversion of Control (Inversion of Control, IoC) is an important principle of object-oriented programming used to reduce connectivity in computer programs and is one of the five most important principles of SOLID.

Today there are several basic frameworks on this topic:

1. Dagger
2. Google Guice
3. Spring Framework I

still use Spring and are partially pleased with its functionality, but it's time to try something and something of my own, isn't it?

About myself


My name is Nikita, I am 24 years old, and I have been doing java (backend) for 3 years. He studied only on practical examples, in parallel trying to understand the specs of classes. Currently working (freelance) - writing a CMS for a commercial project, where I use Spring Boot. I recently thought: “Why not write your IoC (DI) Container according to your vision and desire?”. Roughly speaking - “I wanted it with a blackjack ...”. This will be discussed today. Well, please under the cat. Link to project sources .

Features


- the main feature of the project - Dependency Injection.
3 main dependency injection methods are supported:
  1. Class fields
  2. Class constructor
  3. Class functions (standard setter on one parameter)

* Note:
- when scanning a class, if you use all three injection methods at once - the injection method through the class constructor marked with the @IoCDependency annotation will take priority. Those. Only one injection method works.

- lazy initialization of components (on demand);

- built-in loader configuration files (formats: ini, xml, properties);

- command line argument handler;

- processing modules by creating factories;

- built-in events and listeners;

- built-in informants (Sensibles) for “informing” a component, factory, listener, processor (ComponentProcessor) that certain information should be loaded into an object depending on the informer;

- a module for managing / creating a thread pool, declaring functions as executable tasks for some time and initializing them in the pool factory, as well as starting with the parameters of SimpleTask.

How packages are scanned:
Used by third-party Reflections API with a standard scanner.

//{@see IocStarter#initializeContext}
 private AppContext initializeContext(Class<?>[] mainClasses, String... args) throws Exception {
        final AppContext context = new AppContext();
        for (Class<?> mainSource : mainClasses) {
            final List<String> modulePackages = getModulePaths(mainSource);
            final String[] packages = modulePackages.toArray(new String[0]);
            final Reflections reflections = ReflectionUtils.configureScanner(packages, mainSource);
            final ModuleInfo info = getModuleInfo(reflections);
            initializeModule(context, info, args);
        }
        Runtime.getRuntime().addShutdownHook(new ShutdownHook(context));
        context.getDispatcherFactory().fireEvent(new OnContextIsInitializedEvent(context));
        return context;
    }

We get a collection of classes using annotation filters, types.
In this case, it is @IoCComponent, @Property and the Prophet Analyzer <R, T>

Context initialization order:
1) The first is the initialization of configuration types.
//{@see AppContext#initEnvironment(Set)}
 public void initEnvironment(Set<Class<?>> properties) {
        for (Class<?> type : properties) {
            final Property property = type.getAnnotation(Property.class);
            if (property.ignore()) {
                continue;
            }
            final Path path = Paths.get(property.path());
            try {
                final Object o = type.newInstance();
                PropertiesLoader.parse(o, path.toFile());
                dependencyInitiator.instantiatePropertyMethods(o);
                dependencyInitiator.addInstalledConfiguration(o);
            } catch (Exception e) {
                throw new Error("Failed to Load " + path + " Config File", e);
            }
        }
    }

* Пояснения:
Аннотация @Property имеет обязательный строковый параметр — path (путь к файлу конфигурации). Именно по нему ведется поиск файла для парсинга конфигурации.
Класс PropertiesLoader — класс-утилита для инициализирования полей класса соответствующих полям файла конфигурации.
Функция DependencyFactory#addInstalledConfiguration(Object) — загружает объект конфигурации в фабрику как SINGLETON (иначе смысл перезагружать конфиг не по требованию).

2) Инициализация анализаторов
3) Инициализация найденных компонентов (Классы помеченные аннотацией @IoCComponent)
//{@see AppContext#scanClass(Class)}
private void scanClass(Class<?> component) {
        final ClassAnalyzer classAnalyzer = getAnalyzer(ClassAnalyzer.class);
        if (!classAnalyzer.supportFor(component)) {
            throw new IoCInstantiateException("It is impossible to test, check the class for type match!");
        }
        final ClassAnalyzeResult result = classAnalyzer.analyze(component);
        dependencyFactory.instantiate(component, result);
    }

* Пояснения:
Класс ClassAnalyzer — определяет метод инъекции зависимостей, так же если имеются ошибки неверной расстановки аннотаций, объявлений конструктора, параметров в методе — возвращает ошибку. Функция Analyzer<R, T>#analyze(T) — возвращает результат выполнения анализа . Функция Analyzer<R, T>#supportFor(Т) — возвращает булевый параметр в зависимости от прописанных условий.
Функция DependencyFactory#instantiate(Class, R) — инсталлирует тип в фабрику методом, определенном ClassAnalyzer или выбрасывает исключение если имееются ошибки либо анализа либо самого процесса инициализации объекта.

3) Методы сканирования
— метод инъекции параметров в конструктор класса
    private <O> O instantiateConstructorType(Class<O> type) {
        final Constructor<O> oConstructor = findConstructor(type);
        if (oConstructor != null) {
            final Parameter[] constructorParameters = oConstructor.getParameters();
            final List<Object> argumentList = Arrays.stream(constructorParameters)
                    .map(param -> mapConstType(param, type))
                    .collect(Collectors.toList());
            try {
                final O instance = oConstructor.newInstance(argumentList.toArray());
                addInstantiable(type);
                final String typeName = getComponentName(type);
                if (isSingleton(type)) {
                    singletons.put(typeName, instance);
                } else if (isPrototype(type)) {
                    prototypes.put(typeName, instance);
                }
                return instance;
            } catch (Exception e) {
                throw new IoCInstantiateException("IoCError - Unavailable create instance of type [" + type + "].", e);
            }
        }
        return null;
    }
    

— метод инъекции параметров в поля класса
   private <O> O instantiateFieldsType(Class<O> type) {
        final List<Field> fieldList = findFieldsFromType(type);
        final List<Object> argumentList = fieldList.stream()
                .map(field -> mapFieldType(field, type))
                .collect(Collectors.toList());
        try {
            final O instance = ReflectionUtils.instantiate(type);
            addInstantiable(type);
            for (Field field : fieldList) {
                final Object toInstantiate = argumentList
                        .stream()
                        .filter(f -> f.getClass().getSimpleName().equals(field.getType().getSimpleName()))
                        .findFirst()
                        .get();
                final boolean access = field.isAccessible();
                field.setAccessible(true);
                field.set(instance, toInstantiate);
                field.setAccessible(access);
            }
            final String typeName = getComponentName(type);
            if (isSingleton(type)) {
                singletons.put(typeName, instance);
            } else if (isPrototype(type)) {
                prototypes.put(typeName, instance);
            }
            return instance;
        } catch (Exception e) {
            throw new IoCInstantiateException("IoCError - Unavailable create instance of type [" + type + "].", e);
        }
    }
   

— метод инъекции параметров через функции класса
    private <O> O instantiateMethodsType(Class<O> type) {
        final List<Method> methodList = findMethodsFromType(type);
        final List<Object> argumentList = methodList.stream()
                .map(method -> mapMethodType(method, type))
                .collect(Collectors.toList());
        try {
            final O instance = ReflectionUtils.instantiate(type);
            addInstantiable(type);
            for (Method method : methodList) {
                final Object toInstantiate = argumentList
                        .stream()
                        .filter(m -> m.getClass().getSimpleName().equals(method.getParameterTypes()[0].getSimpleName()))
                        .findFirst()
                        .get();
                method.invoke(instance, toInstantiate);
            }
            final String typeName = getComponentName(type);
            if (isSingleton(type)) {
                singletons.put(typeName, instance);
            } else if (isPrototype(type)) {
                prototypes.put(typeName, instance);
            }
            return instance;
        } catch (Exception e) {
            throw new IoCInstantiateException("IoCError - Unavailable create instance of type [" + type + "].", e);
        }
    }
   



User API
1. ComponentProcessor — утилита позволяющая изменять компонент по собственному желанию как до его инициализации в контексте так и после.
public interface ComponentProcessor {
    Object afterComponentInitialization(String componentName, Object component);
    Object beforeComponentInitialization(String componentName, Object component);
}


*Пояснения:
Функция #afterComponentInitialization(String, Object) — позволяет проводить манипуляции с компонентом после инициализации его в контексте, входящие параметры — (закрепленной название компонента, инстанциированный объект компонента).
Функция #beforeComponentInitialization(String, Object) — позволяет проводить манипуляции с компонентом перед инициализацией его в контексте, входящие параметры — (закрепленной название компонента, инстанциированный объект компонента).

2. CommandLineArgumentResolver
public interface CommandLineArgumentResolver {
    void resolve(String... args);
}


*Пояснения:
Функция #resolve(String...) — интерфейс-обработчик различных команд переданных через cmd при запуске приложения, входящий параметр — неограниченный массив строк (параметров) командной строки.
3. Информаторы (Sensibles) — указывает, что дочернему классу информатора нужно будет встроить опр. функционал в зависимости от типа информатора (ContextSensible, EnvironmentSensible, ThreadFactorySensible, etc.)

4. Слушатели (Listener's)
Реализован функционал слушателей, гарантировано multi-threading выполнение с настройкой рекомендуемого количества дескрипторов для оптимизированной работы событий.
@org.di.context.annotations.listeners.Listener // обязательный аннотация-маркер
@IoCComponent // обязательный аннотация, в случае ее отсутствия реализация информеров (Sensibles) не будет интегрирована.
public class TestListener implements Listener {
    private final Logger log = LoggerFactory.getLogger(TestListener.class);
    @Override
    public boolean dispatch(Event event) {
        if (OnContextStartedEvent.class.isAssignableFrom(event.getClass())) {
            log.info("ListenerInform - Context is started! [{}]", event.getSource());
        } else if (OnContextIsInitializedEvent.class.isAssignableFrom(event.getClass())) {
            log.info("ListenerInform - Context is initialized! [{}]", event.getSource());
        } else if (OnComponentInitEvent.class.isAssignableFrom(event.getClass())) {
            final OnComponentInitEvent ev = (OnComponentInitEvent) event;
            log.info("ListenerInform - Component [{}] in instance [{}] is initialized!", ev.getComponentName(), ev.getSource());
        }
        return true;
    }
}

** Пояснения:
Функция dispatch(Event) — главная функция обработчик событий системы.
— Присутствуют стандартные реализации слушателей с проверкой на типы событий а так же со встраиваемыми пользовательскими фильтрами {@link Filter}. Стандартные фильтры входящие в пакет: AndFilter, ExcludeFilter, NotFilter, OrFilter, InstanceFilter (custom). Стандартные реализации слушателей: FilteredListener и TypedListener. Первый задействует в себе фильтр для проверки входящего объекта события. Второй осуществляет проверку событийного объекта либо же любого другого на принадлежность к определенному инстансу.



Modules
1) Модуль для работы с потоковыми задачами в Вашем приложении

— подключаем зависимости
<repositories>
        <repository>
            <id>di_container-mvn-repo</id>
            <url>https://raw.github.com/GenCloud/di_container/threading/</url>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
            </snapshots>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>org.genfork</groupId>
            <artifactId>threads-factory</artifactId>
            <version>1.0.0.RELEASE</version>
        </dependency>
    </dependencies>


— маркер-аннотация для включения модуля в контекст (@ThreadingModule)
@ThreadingModule
@ScanPackage(packages = {"org.di.test"})
public class MainTest {
    public static void main(String... args){
      IoCStarter.start(MainTest.class, args);
    }
}


— внедрение фабрики модуля в инсталлируемый компонент приложения
@IoCComponent
public class ComponentThreads implements ThreadFactorySensible<DefaultThreadingFactory> {
    private final Logger log = LoggerFactory.getLogger(AbstractTask.class);
    private DefaultThreadingFactory defaultThreadingFactory;
    private final AtomicInteger atomicInteger = new AtomicInteger(0);
    @PostConstruct
    public void init() {
        defaultThreadingFactory.async(new AbstractTask<Void>() {
            @Override
            public Void call() {
                log.info("Start test thread!");
                return null;
            }
        });
    }
    @Override
    public void threadFactoryInform(DefaultThreadingFactory defaultThreadingFactory) throws IoCException {
        this.defaultThreadingFactory = defaultThreadingFactory;
    }
    @SimpleTask(startingDelay = 1, fixedInterval = 5)
    public void schedule() {
        log.info("I'm Big Daddy, scheduling and incrementing param - [{}]", atomicInteger.incrementAndGet());
    }
}

*Пояснения:
ThreadFactorySensible — один из дочерних классов-информаторов для внедрения в инстанциируемый компонент опр. информации (конфигурации, контекста, модуля, etc.).
DefaultThreadingFactory — фабрика модуля threading-factory.

Аннотация @SimpleTask — параметризируемый маркер-аннотация для выявления у компонента реализации задач в функциях. (запускает поток с заданными параметрами аннотацией и добавляет его в фабрику, откуда его можно будет достать и, к примеру, отключить выполнение).

— стандартные функции шедулинга задач
     // Выполняет асинхронные задачи. Задачи, запланированные здесь, перейдут в пул разделяемых потоков по умолчанию.
    <T> AsyncFuture<T> async(Task<T>)
    // Выполняет асинхронные задачи в запланированное время.
    <T> AsyncFuture<T> async(long, TimeUnit, Task<T>)
    // Выполняет асинхронные задачи в фиксированное время.
    ScheduledAsyncFuture async(long, TimeUnit, long, Runnable)


***Обратите внимание, что ресурсы в пуле запланированных потоков ограничены, и задачи должны выполняться быстро.

— дефолтная конфигурация пула
# Threading
threads.poolName=shared
threads.availableProcessors=4
threads.threadTimeout=0
threads.threadAllowCoreTimeOut=true
threads.threadPoolPriority=NORMAL




Starting point or how it all works


We connect project dependencies:

   <repositories>
        <repository>
            <id>di_container-mvn-repo</id>
            <url>https://raw.github.com/GenCloud/di_container/context/</url>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
            </snapshots>
        </repository>
    </repositories>
...
    <dependencies>
        <dependency>
            <groupId>org.genfork</groupId>
            <artifactId>context</artifactId>
            <version>1.0.0.RELEASE</version>
        </dependency>
    </dependencies>

Test class application.

@ScanPackage(packages = {"org.di.test"})
public class MainTest {
    public static void main(String... args) {
        IoCStarter.start(MainTest.class, args);
    }
}

** Explanation: The
@ScanPackage annotation indicates to the context which packages should be scanned to identify components (classes) for their injection. If the package is not specified, the package of the class marked with this annotation will be scanned.

IoCStarter # start (Object, String ...) - entry point and initialization of the application context.

Additionally, we will create several classes of components for direct verification of the functional.

ComponentA
@IoCComponent
@LoadOpt(PROTOTYPE)
public class ComponentA {
    @Override
    public String toString() {
        return "ComponentA{" + Integer.toHexString(hashCode()) + "}";
    }
}


ComponentB
@IoCComponent
public class ComponentB {
    @IoCDependency
    private ComponentA componentA;
    @IoCDependency
    private ExampleEnvironment exampleEnvironment;
    @Override
    public String toString() {
        return "ComponentB{hash: " + Integer.toHexString(hashCode()) + ", componentA=" + componentA +
                ", exampleEnvironment=" + exampleEnvironment +
                '}';
    }
}


ComponentC
@IoCComponent
public class ComponentC {
    private final ComponentB componentB;
    private final ComponentA componentA;
    @IoCDependency
    public ComponentC(ComponentB componentB, ComponentA componentA) {
        this.componentB = componentB;
        this.componentA = componentA;
    }
    @Override
    public String toString() {
        return "ComponentC{hash: " + Integer.toHexString(hashCode()) + ", componentB=" + componentB +
                ", componentA=" + componentA +
                '}';
    }
}


ComponentD
@IoCComponent
public class ComponentD {
    @IoCDependency
    private ComponentB componentB;
    @IoCDependency
    private ComponentA componentA;
    @IoCDependency
    private ComponentC componentC;
    @Override
    public String toString() {
        return "ComponentD{hash: " + Integer.toHexString(hashCode()) + ", ComponentB=" + componentB +
                ", ComponentA=" + componentA +
                ", ComponentC=" + componentC +
                '}';
    }
}


* Notes:
- cyclic dependencies are not provided, there is a stub in the form of an analyzer, which, in turn, checks the resulting classes from the scanned packets and throws an exception if there is a cyclic.
** Explanations:
Annotation @IoCComponent - shows the context that it is a component and must be analyzed to identify dependencies (mandatory annotation).

The @IoCDependency annotation shows the analyzer that this is a component dependency and it must be instantiated into the component.

The @LoadOpt annotation tells the context what type of component loading to use. Currently, 2 types are supported - SINGLETON and PROTOTYPE (single and multiple).

Expand the implementation of the main class:

Maintest
@ScanPackage(packages = {"org.di.test", "org.di"})
public class MainTest extends Assert {
    private static final Logger log = LoggerFactory.getLogger(MainTest.class);
    private AppContext appContext;
    @Before
    public void initializeContext() {
        BasicConfigurator.configure();
        appContext = IoCStarter.start(MainTest.class, (String) null);
    }
    @Test
    public void printStatistic() {
        DependencyFactory dependencyFactory = appContext.getDependencyFactory();
        log.info("Initializing singleton types - {}", dependencyFactory.getSingletons().size());
        log.info("Initializing proto types - {}", dependencyFactory.getPrototypes().size());
        log.info("For Each singleton types");
        for (Object o : dependencyFactory.getSingletons().values()) {
            log.info("------- {}", o.getClass().getSimpleName());
        }
        log.info("For Each proto types");
        for (Object o : dependencyFactory.getPrototypes().values()) {
            log.info("------- {}", o.getClass().getSimpleName());
        }
    }
    @Test
    public void testInstantiatedComponents() {
        log.info("Getting ExampleEnvironment from context");
        final ExampleEnvironment exampleEnvironment = appContext.getType(ExampleEnvironment.class);
        assertNotNull(exampleEnvironment);
        log.info(exampleEnvironment.toString());
        log.info("Getting ComponentB from context");
        final ComponentB componentB = appContext.getType(ComponentB.class);
        assertNotNull(componentB);
        log.info(componentB.toString());
        log.info("Getting ComponentC from context");
        final ComponentC componentC = appContext.getType(ComponentC.class);
        assertNotNull(componentC);
        log.info(componentC.toString());
        log.info("Getting ComponentD from context");
        final ComponentD componentD = appContext.getType(ComponentD.class);
        assertNotNull(componentD);
        log.info(componentD.toString());
    }
    @Test
    public void testProto() {
        log.info("Getting ComponentA from context (first call)");
        final ComponentA componentAFirst = appContext.getType(ComponentA.class);
        log.info("Getting ComponentA from context (second call)");
        final ComponentA componentASecond = appContext.getType(ComponentA.class);
        assertNotSame(componentAFirst, componentASecond);
        log.info(componentAFirst.toString());
        log.info(componentASecond.toString());
    }
    @Test
    public void testInterfacesAndAbstracts() {
        log.info("Getting MyInterface from context");
        final InterfaceComponent myInterface = appContext.getType(MyInterface.class);
        log.info(myInterface.toString());
        log.info("Getting TestAbstractComponent from context");
        final AbstractComponent testAbstractComponent = appContext.getType(TestAbstractComponent.class);
        log.info(testAbstractComponent.toString());
    }
}


We start by means of your IDE or the command line project.

Result of performance
Connected to the target VM, address: '127.0.0.1:55511', transport: 'socket'
0 [main] INFO org.di.context.runner.IoCStarter  - Start initialization of context app
87 [main] DEBUG org.reflections.Reflections  - going to scan these urls:
file:/C:/Users/GenCloud/Workspace/di_container/context/target/classes/
file:/C:/Users/GenCloud/Workspace/di_container/context/target/test-classes/
[main] DEBUG org.reflections.Reflections  - could not scan file log4j2.xml in url file:/C:/Users/GenCloud/Workspace/di_container/context/target/test-classes/ with scanner SubTypesScanner
[main] DEBUG org.reflections.Reflections  - could not scan file log4j2.xml in url file:/C:/Users/GenCloud/Workspace/di_container/context/target/test-classes/ with scanner TypeAnnotationsScanner
[main] INFO org.reflections.Reflections  - Reflections took 334 ms to scan 2 urls, producing 21 keys and 62 values 
[main] INFO org.di.context.runner.IoCStarter  - App context started in [0] seconds
[main] INFO org.di.test.MainTest  - Initializing singleton types - 6
[main] INFO org.di.test.MainTest  - Initializing proto types - 1
[main] INFO org.di.test.MainTest  - For Each singleton types
[main] INFO org.di.test.MainTest  - ------- ComponentC
[main] INFO org.di.test.MainTest  - ------- TestAbstractComponent
[main] INFO org.di.test.MainTest  - ------- ComponentD
[main] INFO org.di.test.MainTest  - ------- ComponentB
[main] INFO org.di.test.MainTest  - ------- ExampleEnvironment
[main] INFO org.di.test.MainTest  - ------- MyInterface
[main] INFO org.di.test.MainTest  - For Each proto types
[main] INFO org.di.test.MainTest  - ------- ComponentA
[main] INFO org.di.test.MainTest  - Getting ExampleEnvironment from context
[main] INFO org.di.test.MainTest  - ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]}
[main] INFO org.di.test.MainTest  - Getting ComponentB from context
[main] INFO org.di.test.MainTest  - ComponentB{hash: be64738, componentA=ComponentA{3ba9ad43}, exampleEnvironment=ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]}}
[main] INFO org.di.test.MainTest  - Getting ComponentC from context
[main] INFO org.di.test.MainTest  - ComponentC{hash: 49d904ec, componentB=ComponentB{hash: be64738, componentA=ComponentA{3ba9ad43}, exampleEnvironment=ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]}}, componentA=ComponentA{48e4374}}
[main] INFO org.di.test.MainTest  - Getting ComponentD from context
[main] INFO org.di.test.MainTest  - ComponentD{hash: 3d680b5a, ComponentB=ComponentB{hash: be64738, componentA=ComponentA{3ba9ad43}, exampleEnvironment=ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]}}, ComponentA=ComponentA{4b5d6a01}, ComponentC=ComponentC{hash: 49d904ec, componentB=ComponentB{hash: be64738, componentA=ComponentA{3ba9ad43}, exampleEnvironment=ExampleEnvironment{hash: 6f96c77, nameApp='Di Container (ver. 0.0.0.2)', components=[ComponentD, ComponentC, ComponentB, ComponentA]}}, componentA=ComponentA{48e4374}}}
[main] INFO org.di.test.MainTest  - Getting MyInterface from context
[main] INFO org.di.test.MainTest  - MyInterface{componentA=ComponentA{cd3fee8}}
[main] INFO org.di.test.MainTest  - Getting TestAbstractComponent from context
[main] INFO org.di.test.MainTest  - TestAbstractComponent{componentA=ComponentA{3e2e18f2}, AbstractComponent{}}
[main] INFO org.di.test.MainTest  - Getting ComponentA from context (first call)
[main] INFO org.di.test.MainTest  - ComponentA{10e41621}
[main] INFO org.di.test.MainTest  - Getting ComponentA from context (second call)
[main] INFO org.di.test.MainTest  - ComponentA{353d0772}
Disconnected from the target VM, address: '127.0.0.1:55511', transport: 'socket'
Process finished with exit code 0


+ There is a built-in api parsing of configuration files (ini, xml, properties).
Rolled test is in the repository.

Future


Plans to expand and support the project as much as possible.

What I want to see:

  1. Writing additional modules - network / work with databases / writing solutions for typical tasks.
  2. Replacing Java Reflection API with CGLIB
  3. etc. (I listen to users, if any)

This is followed by the logical end of the article.

Thanks to all. I hope someone my works will be useful.
UPD. Updating the article - 09/15/2018. Release 1.0.0