В данной статье будет рассмотрен мой личный опыт постановки на поток документирования и анализа PHP проекта, который был разработан порядка 10-ти лет назад и не претерпевал с тех пор никаких существенных изменений.

Статья может быть полезна специалистам, которые занимаются управлением проектами или решают схожие задачи представленные в данной статье.

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

Введение

Для начала хотелось бы рассказать о том, что это за проект, который потребовалось задокументировать и проанализировать, какова цель, какие вообще есть задачи и почему это потребовалось сделать сейчас, а не в далёком будущем.

Проект, который попал в область моих задач называется "Selena". Точно не знаю с чем связано данное название, однако по умолчанию будем считать, что это связано с древнегреческой мифологией.

Определение слова "Селена", позаимствованное из Википедии

Селе́на (др.-греч. Σελήνη; от др.-греч. σελήνη — «луна», далее от σέλας — «свет, сияние») — одна из богинь в древнегреческой мифологии, известная также под именем Ме́не (др.-греч. Μήνη, от др.-греч. μήνη — «луна»). Богиня Луны, в поздней античности отождествлявшаяся с Артемидой и Гекатой.

Проект представляет собой веб-приложение уровня предприятия. Используется оно сотрудниками предприятия и работает во внутренней его сети, тем самым ограничивая доступ "не посвящённым".

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

Само по себе веб-приложение представляет собой монолитное приложение написанное на PHP 5.4, без каких-либо популярных на сегодняшний день архитектурных подходов, с игнорированием SOLID принципов, без пакетного менеджера (инъекция зависимостей происходит напрямую) и со множеством проблем в проектировании базы данных (одной из которых является отсутствие явных связей между таблицами через foreign key, т.к. используемые в проекте технологии и подходы не обновлялись с начала разработки проекта) . В общем, есть причины отнести данный проект к типу legacy.

Основная цель анализа и документирования проекта заключается в том, чтобы сформировать базу технической документации проекта, расширить его возможности в области командного образования и как таковой его дальнейшей поддержки. До того, как моя команда взялась за решение задач, связанное с достижением поставленных целей, никакой технической документации проект не имел. За 10 лет разработки сменилось множество разработчиков, которые не оставили после себя документы, которые бы подробно описывали что там в системе происходит, какой функционал в ней есть, какие бизнес-процессы и бизнес-функции присутствуют, и так далее. Как только моя команда приступила к анализу проекта, выяснилось, что в нём присутствуют лишь комментарии к PHP-коду, которые не давали никакой ясности в том, что происходит. Пришлось проводить ещё и документирование PHP-кода, что позволило нам также создать пространство для тестирования такого популярного на сегодняшний день инструмента, как ChatGPT в области анализа программного кода.

Теперь рассмотрим задачи, которые были передо мной и моей командой поставлены:

  1. Задокументировать исходный код проекта в виде развёрнутых комментариев к PHP-коду, с ведением учёта задокументированных файлов. Под документированием в данном пункте понимается комментирование исходного кода.

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

  3. Описать все API веб-приложения и вынести их описание в отдельную информационную базу.

Теперь приступим к последовательному рассмотрению процесса решения поставленных задач.

Немного о семантическом версировании

Как только мы взялись за проект, сразу же встал вопрос - а какую версию веб-приложения мы будем анализировать и документировать?

Ситуация осложнялась ещё и тем, что помимо нашего участия в проекте присутствовал разработчик, который активно вносил изменения в проект и переписывал систему на более новую версию языка - PHP 7.

Спойлер

Разработчик до сих пор занимается переписыванием проекта на PHP 7 с PHP 5.4, однако нам удалось с ним наладить контакт и синхронизироваться в процессе работы, а потому моя команда поработала с разными версиями PHP, что благоприятно повлияло на их опыт в код-ревью.

Проект разрабатывается порядка 10-ти лет, а репозиторий для него на GitHub был создан 3 года назад, и, что естественно, ни о каких соглашениях о коммитах и семантическом версировании речи и не шло, т.к. это бы повлияло на создание документации, которой не было. Я принял решение закрепиться за конкретной версией по временному принципу - задокументированные исходники кода отстают по своей "актуальности" от боевого проекта всего на 5 недель (в долгосрочной перспективе это не такой большой промежуток).

Как только моя команда создала копию проекта в GitHub, мы сразу же начали использовать соглашение о коммитах, которое посчитали полезным, не смотря на то, что проект уже очень давно разрабатывался без каких-либо соглашений (никогда не поздно начать).

Нет комментариев? Проставим!

После того, как мы скопировали исходники проекта и выделили для его документирования отдельный репозиторий, моя команда незамедлительно приступила к комментированию исходного кода проекта.

Файлов в проекте оказалось очень много (что не удивительно), и нужно было каким-то образом вести учёт уже задокументированных и описанных файлов. Здесь сделаю небольшой спойлер (без этого никак) и скажу, что для ведения технической документации мы выбрали инструмент Wiki.js, а потому для начала комментировали PHP-код, изучали бизнес-процессы, строили схемы по архитектуре приложения, а затем переносили это всё в отдельные страницы Wiki.js. К слову, получилось лучше чем я предполагал.

Для учёта файлов, которые уже были задокументированы было принято решение их отмечать определёнными PHP комментариями в самом начале файла. Выглядело это следующим образом:

<?php
// В многострочном комментарии находится "метка" задокументированного файла
/* --------------------- */
/* Documented in Wiki.js */
/* --------------------- */

// Дальше идёт PHP код файла

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

Скрипт выглядел следующим образом (он достаточно прост для понимания):

import os
import pandas as pd

# Получение всех файлов в определённой директории
def getfiles(dir = os.getcwd()):
    filelist = []
    # Перебор всех файлов и добавление наиболее интересных
    # в результирующий массив (интересны только PHP и JS файлы)
    for dirpath, dirnames, filenames in os.walk(dir):
        for filename in filenames:
            if filename.count('.php') or filename.count('.js'):
                filelist.append(os.path.join(dirpath, filename))
    return filelist

# Функция парсинга файлов
def parcer():
    # Получение пути к директории с файлами проекта
    way_to_file = input(str('Введите путь до файла(если путь окажется некорректным программа вернёт пустой файл): \n'))
    if way_to_file == '':
        # По умолчанию директорией с исходными файлами считается та, где данный скрипт запущен
        mass = getfiles()
    else:
        mass = getfiles(way_to_file)

    # Массив ещё не задокументированных файлов
    output = []

    # Поиск во всех файлах исходного кода "метки"
    for i in mass:
        cheq = ''

        # Чтение данных из файла
        with open(i, "r", encoding="latin-1") as file:
            cheq = file.read()

        # Проверка содержания "метки" в файле
        # Если метка есть - то файл задокументирован и в выходной массив он не добавляется
        # Если метки нет - то файл ещё не задокументирован и он добавляется в выходной массив
        if cheq.count('/* --------------------- */\n'
                '/* Documented in Wiki.js */\n'
                '/* --------------------- */\n') == 0:
            output.append(i)
    return output

# Получение результата работы функции "парсинг файлов"
data = parcer()
while True:
    # Выбор типа сохранения файлов
    save_as = input(str("Сохранить как:\n"
                        "1. excel фaйл\n"
                        "2. .txt файл\n"))
    
    # Сохранение результата работы утилиты в Excel-таблицу
    if save_as == '1':
        df = pd.DataFrame(data=data)
        df.to_excel('./result.xlsx')
        break
    elif save_as == '2':
        # Сохранение результата работы утилиты в текстовый файл
        with open('result.txt', 'w') as file:
            for i in data:
                file.write(i + '\n')
        break
    else:
        print("Введите 1 или 2")

Если рассматривать общий поток комментирования, то он представлен на рисунке 1 в виде схемы.

Рисунок 1 - Поток выборки и документирования файлов исходного кода
Рисунок 1 - Поток выборки и документирования файлов исходного кода

Таким образом моя команда смогла задокументировать большое число файлов исходного кода проекта.

Куда складывать техническую документацию? Wiki.js!

После первых успехов в понимании работы веб-приложения, наступил момент определения дальнейшего вектора развития наших действий, ведь до этого момента мы ещё не определились с единой точкой, в которую бы складывали все результаты нашего анализа системы и технической документации.

Конечно, можно вести техническую документацию и в обычном Word или даже текстовом документе с соответствующими соглашениями об оформлении и структуре. Однако читать линейный документ не доставляет большого удовольствия. Тем более когда страниц в нём порядка 500 или даже 1000. Мне хотелось найти инструмент, который бы предоставлял гибкие возможности в плане структурирования контента и даже в некотором смысле позволял бы имитировать файловую структуру исходного кода (грубо говоря).

Были мысли использовать docuWiki, мы даже начали её использовать в первую неделю работы над проектом, однако быстро поняли, что его функциональных возможностей недостаточно для удовлетворения всех наших требований.

Нам нужен был инструмент, с помощью которого мы могли бы организованно хранить разные страницы, причём изолированно друг от друга. Инструмент docuWiki не предоставлял такие возможности. Как и имел не очень хороший дизайн (кто работал с docuWiki поймёт о чём я).

Полный список требований к инструменту для содержания технической документации по проекту был следующий:

  1. Инструмент должен иметь хороший дизайн, чтобы было приятно им пользоваться и не было желания разбить экран монитора.

  2. Инструмент должен предоставлять широкие возможности по организации контента

  3. Инструмент должен предоставлять возможности сделать бэкап всех ресурсов и контента

  4. Инструмент должен предоставлять различные способы редактирования контента и поддерживать язык разметки Markdown

  5. Желательно, чтобы инструмент можно было развернуть в контейнере с простым сценарием развёртывания

Всем требованиям, которые были предъявлены к искомому инструменту, удовлетворяла платформа Wiki.js.

Данный инструмент имеет хороший дизайн (см. рис. 2), предоставляет широкие возможности по организации контента, позволяет сделать бэкап (с возможностью синхронизацией с GitHub), а также поддерживает разметку Markdown и разворачивается через простые инструкции в Docker Compose.

Рисунок 2 - Главная страница документации Wiki.js
Рисунок 2 - Главная страница документации Wiki.js

Предприятие, для которого работает веб-приложение, предоставило нам два шлюза:

  1. Для развёртывания своих сервисов и платформы Wiki.js (серверный шлюз)

  2. Для работы с развёрнутыми сервисами и платформой Wiki.js (клиентский шлюз, на котором установлена ОС Windows 10 и с помощью которой мы взаимодействовали с тестовой версией проекта и развёрнутой Wiki.js)

На серверном шлюзе было развёрнуто приложение Wiki.js, по достаточно простому сценарию развёртывания, который включает в себя использование инструмента Docker.

Следующие шаблонные инструкции были использованы нами для развёртывания Wiki.js с помощью Docker Compose:

version: "3"
services:

  db:
    image: postgres:11-alpine
    environment:
      POSTGRES_DB: wiki
      POSTGRES_PASSWORD: wikijsrocks
      POSTGRES_USER: wikijs
    logging:
      driver: "none"
    restart: unless-stopped
    volumes:
      - db-data:/var/lib/postgresql/data

  wiki:
    image: ghcr.io/requarks/wiki:2
    depends_on:
      - db
    environment:
      DB_TYPE: postgres
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: wikijs
      DB_PASS: wikijsrocks
      DB_NAME: wiki
    restart: unless-stopped
    ports:
      - "80:3000"

volumes:
  db-data:

В шаблонном коде всё достаточно просто. Есть сервис db, который представляет собой базу данных (в данном случае - postgresql), и также есть сервис wiki, который, собственно, и является нашим приложением Wiki.js (мы используем вторую версию данного приложения). После запуска данных инструкций происходит развёртывание контейнера с базой данных и приложением Wiki.js (конечно, все переменные окружения должны быть добавлены в данные шаблонные инструкции).

После внедрения инструмента Wiki.js в наш рабочий поток несколько изменился. Теперь после того, как были задокументированы исходные файлы проекта, составлены необходимые схемы и концептуальные модели нужно было всё вносить в Wiki.js.

Более того, предоставленные шлюзы ограничивали нам доступ к GitHub (политика безопасности предприятия), а потому не было возможности напрямую синхронизироваться с репозиториями. Однако, была возможность подключиться к серверному шлюзу по SFTP, и загружать туда под правами root файлы бэкапа (Wiki.js поддерживает множество функций для бэкапа, SFTP и Git это лишь одни из них, полный их список можно найти тут).

Для периодического бэкапа была придумана схема, по которой после внесения каких-либо результатов в Wiki.js необходимо было выгружать бэкап сначала локально (на серверный шлюз), а затем с помощью FileZilla загружать этот бэкап на свою локальную машину и переносить всё в репозиторий на GitHub.

Полная схема бэкапа представлена на рисунке 3.

Рисунок 3 - Схема загрузки бэкапа Wiki.js на репозиторий в GitHub
Рисунок 3 - Схема загрузки бэкапа Wiki.js на репозиторий в GitHub

В общем-то, такой способ сохранения информации о содержимом Wiki.js используется нами до сих пор. Он достаточно прост и эффективен, а также позволяет гибко распоряжаться содержимым бэкапа (например, можно его загружать не на GitHub, а на любую другую платформу). Моя команда осуществляет данную процедуру раз в день и этого достаточно чтобы сохранить результаты работы и в любой момент времени перенести их на любое другое устройство где можно развернуть Wiki.js (для этого есть механизм импорта бэкапа).

А в поток работ по документированию и анализу кода проекта добавляется новые узлы - добавление результатов анализа и документирования в Wiki.js, а также создание бэкапа Wiki.js (см. рис. 4).

Рисунок 4 - Новая схема потока работ (добавление данных в Wiki.js)
Рисунок 4 - Новая схема потока работ (добавление данных в Wiki.js)

На рисунке 5 представлено веб-приложение с технической документацией целевого проекта (результаты плодотворной работы моей команды).

Рисунок 5 - Техническая документация целевого проекта
Рисунок 5 - Техническая документация целевого проекта

Куда пропали контроллеры для API? Они есть везде!

Так уж сложилось, что ни один из популярных подходов при проектировании рассматриваемого веб-приложения не был использован. Ни MVC, ни CSR, ни даже SOLID принципы не посчитали нужным уделить внимание данному проекту.

К сожалению, многообразие обработчиков POST и GET запросов в почти каждом файле исходного кода (даже там, где это не уместно) может вызвать смущение даже у начинающего архитектора. В данном проекте огромное пространство для изучения ошибок проектирования, чем я и занимался какое-то время.

Сейчас уже очень популяризированы такие подходы как MVC, CSR, MVVM, DI и многие другие. На момент разработки данного проекта они были не настолько популярны, да и не из каждого угла можно было услышать "пиши MVC, делай по SOLID, а архитектурный стиль используй микросервисный". Так сложилось, ничего не поделаешь. Однако, в наши задачи входит исключительно анализ, а значит меньше критики, больше дела.

Такая подводка была не случайна. Дело в том, что когда документируют API каких-либо сервисов часто взаимодействуют с каким-то одним слоем системы.

Например, есть архитектурный подход Controller Service Repository, где приложение делится на разные слои: контроллеры, сервисы и репозитории соответственно. Слой контроллеров обрабатывает запросы пользователя, слой сервисов реализует бизнес-логику, а слой репозиториев - работает с базой данных.

Так вот, описание API серверных приложений осуществляется на слое контроллеров, это можно взять как правило, даже если вы не используете именно CSR всё равно в хорошей архитектуре (а я говорю про хорошую архитектуру) слой контроллеров (адаптеры связи с внешним миром, если угодно) выделяется отдельно и именно с ним предстоит проделать работу при документировании API.

В данном проекте можно найти части работы контроллеров. Но, только лишь части. Слой контроллеров явно не был выделен, а потому одному члену моей команды пришлось собирать всю работу контроллеров по частям, что было сделано достаточно успешно.

После получения первых результатов в описании API контроллеров появились вопросы о том, какую методику выбрать для их документирования.

Было понятно, что мы будем использовать Swagger для описания API веб-приложения, поскольку он очень удобен для решения данной задачи.

Мне пришла в голову интересная идея - создать шаблон для документирования API сервисов на базе Node.js приложения, в котором реализована возможность генерации документации Swagger по комментариям.

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

Было принято решение создать по шаблону отдельное приложение и начать описывать API веб-приложения в комментариях.

К слову, для описания контроллеров не нужен был даже JavaScript код, т.к. платформа использована Node.js читатель мог подумать, что шаблон требует знаний JavaScript. Однако там достаточно базового понимания работы npm и совсем азов JavaScript - знание как комментировать код.

Пример описания контроллеров API с помощью используемого нами инструментария следующий:

/**
 * Авторизация пользователя
 * @route POST /index.php
 * @group Авторизация (пользователь) - Функции для авторизации пользователя
 * @param {string} username.query.required Входные данные
 * @param {string} password.query.required входные данные
 * @returns {RequestRedirectDto.model} 200 - перенаправление на страницу /
 * @returns {RequestRedirectDto.model} 400 - перенаправление на страницу inc/templates/middle/login/loginfailed.html
 */



/**
 * Запрос списка записей для конкретного материала
 * @route GET /xxx_editmat.php
 * @group Материалы - функции работы с материалами
 * @param {number} matid.query.required идентификатор материала
 * @returns {RequestRedirectDto.model} 200 - перенаправление на страницу admin/editmat/editmat
 * @returns {RequestRedirectDto.model} 400 - перенаправление на страницу admin/editmat/editmat
 */

/**
 * Запрос операции редактирования записи
 * @route PUT /xxx_editmat.php
 * @group Материалы - функции работы с материалами
 * @param {number} matid.query.required идентификатор материала
 * @param {number} edit_mat.query.required идентификатор редактирования записи
 * @param {number} mat_art.query.required артикул записи
 * @param {string} mat_name.query.required название материала
 * @param {number} mat_price.query.required цена материала материала
 * @returns {RequestRedirectDto.model} 200 - перенаправление на страницу admin/editmat/editmat
 * @returns {RequestRedirectDto.model} 400 - перенаправление на страницу admin/editmat/editmat
 */

В комментариях идёт описание API по стандарту Open API, с использованием комментариев в стиле JSDoc.

На серверном шлюзе было также развёрнуто серверное приложение используемого шаблона, что позволяет на клиентском шлюзе видеть описание API через Swagger UI (см. рис. 6).

Рисунок 6 - Документация API в клиентском шлюзе через Swagger UI
Рисунок 6 - Документация API в клиентском шлюзе через Swagger UI

Ну, а с серверным приложением, которое генерирует документацию, отправлять изменения в репозиторий на GitHub ещё проще - не нужно загружать с серверного шлюза какие-либо файлы через FileZilla на локальную машину, все действия с генератором происходят на локальной машине разработчика-исследователя.

Однако, все результаты с локальной машины теперь необходимо загружать на серверный шлюз, чтобы на клиентском шлюзе были продемонстрированы все результаты. Весь этот процесс представлен на рисунке 7.

Рисунок 7 - Загрузка изменений документации API на серверный шлюз
Рисунок 7 - Загрузка изменений документации API на серверный шлюз

Что ж, в наш поток документирования и анализа добавляется последний элемент - документирование API веб-приложения (см. рис. 8).

Рисунок 8 - Окончательная схема потока анализа и документирования веб-приложения
Рисунок 8 - Окончательная схема потока анализа и документирования веб-приложения

Этот код мне непонятен. Нужна нейронка!

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

Мы решили поэкспериментировать и использовать популярную на сегодняшний день нейронную сеть ChatGPT для решения задач анализа исходного кода 10-ти летней давности и были приятно удивлены её результатами.

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

Например, я выдал задачу ChatGPT прокомментировать определённый участок кода с подробными комментариями. Вот часть кода, который нужно было прокомментировать:

globalize($_REQUEST, array('url' => STR, 'userid' => INT));

  if ($userid != $bbuserinfo['userid'])
  {
    eval(print_standard_error('logout_missing_userid'));
  }

  $prefix_length = strlen(COOKIE_PREFIX);
  foreach ($_COOKIE AS $key => $val)
  {
    $index = strpos($key, COOKIE_PREFIX);
    if ($index == 0 AND $index !== false)
    {
      $key = substr($key, $prefix_length);
      if (trim($key) == '')
      {
        continue;
      }
      vbsetcookie($key, '', 1);
    }
  }

А вот результат работы ChatGPT:

"Данный код начинается с вызова функции globalize(), которая принимает два аргумента: массив данных из запроса ($_REQUEST) и массив, содержащий пары ключ-значение, где каждый ключ указывает на переменную, которую нужно объявить глобальной, а значения определяют тип данных для каждой переменной.
Далее происходит проверка условия, где $userid (полученный из глобального массива $_REQUEST) должен равняться значению $bbuserinfo['userid']. Если они не равны, вызывается функция print_standard_error(), в которую передается строка-код ошибки 'logout_missing_userid', которая должна отобразиться пользователю.
Затем происходит очистка всех cookies, начинающихся с префикса COOKIE_PREFIX. Для этого в цикле проходится массив $_COOKIE и для каждого элемента проверяется начинается ли его имя с COOKIE_PREFIX. Если да, то происходит удаление этой cookie с помощью функции vbsetcookie().
Общий смысл кода заключается в том, чтобы разлогинить пользователя, очистить все cookies и произвести некоторые другие действия, связанные с очисткой сессии текущего пользователя."

И действительно, если более детально рассмотреть код, то ChatGPT будет прав - это все инструкции используемые для осуществления выхода пользователя из системы:

// Код ...
if ($_REQUEST['do'] == 'logout')
{

	globalize($_REQUEST, array('url' => STR, 'userid' => INT));

	if ($userid != $bbuserinfo['userid'])
	{
		eval(print_standard_error('logout_missing_userid'));
	}

	// clear all cookies beginning with COOKIE_PREFIX
	$prefix_length = strlen(COOKIE_PREFIX);
	foreach ($_COOKIE AS $key => $val)
	{
		$index = strpos($key, COOKIE_PREFIX);
		if ($index == 0 AND $index !== false)
		{
			$key = substr($key, $prefix_length);
			if (trim($key) == '')
			{
				continue;
			}
			vbsetcookie($key, '', 1);
		}
	}

// Код ...

И это лишь один из множества примеров его использования.

ChatGPT мы использовали для ускорения работы по задачам анализа и документирования кода. С его помощью нам удалось в сравнительно короткие сроки для такой задачи всё проанализировать и задокументировать.

Вполне возможно, что в скором времени ChatGPT и другие продвинутые нейронные сети будут обладать возможностями для работы с разработчиками в командах, и будут формироваться целые коллективы разработчиков, где смешаны люди и нейронные сети. Однако, уже сейчас нейронные сети используются повсеместно как инструменты для облегчения рутинного труда людей.

Заключение

В статье был рассмотрен мой опыт формирования потока для документирования и анализа проекта 10-ти летней давности, который систематизировал работу моей команды, а внедрение ChatGPT в рабочие процессы позволил им ускорить выполнение задач анализа.

Был пройден путь от комментирования исходного кода до документирования API веб-приложения, которые располагаются по приложению где только можно, нарушая принципы SOLID и озадачивая разработчика-исследователя тем, как собрать по частям работу контроллеров.

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

Были решены все поставленные задачи и достигнуты цели. Для формирования единой точки сбора технической документации был использован инструмент Wiki.js, а для документирования API - Swagger UI.

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

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