Реализация и тестирование Qt C++ клиента сетевого сервиса с асинхронным интерфейсом на примере Yandex Dictionary Api

в 13:30, , рубрики: c++, c++11, qt, qt5, Yandex API, асинхронное программирование, Яндекс API

Есть у меня один проект долгострой, в котором использую Yandex Dictionary Api. В процессе разработки решил поделиться опытом создания асинхронного интерфейса к интернет-сервису.

Если у вас есть интерес, как реализовать такой клиент с помощью Qt C++, то этот пост для вас.

Я не стал заострять внимания на тех моментах Qt, которые и так хорошо описаны. В статье я попытался раскрыть, как создавать асинхронные классы в Qt на базе конкретного примера.

Пример программы

Программа посылает на сервер Yandex Dictioanary Api запросы на перевод слов и затем, по мере поступления ответов от сервиса, выводит их на экран.

#include "Precompiled.h"
#include <QtYandexApi/QtYandexApi.h>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QtYandexDictionary yandexDictionary(QtYandexApi::getYandexKeyFromFile("dictKey"));
    QObject::connect(&yandexDictionary, &QtYandexDictionary::translated,
                     [](const QtYaWordTranslation& wordTranslation)
    {
        if (wordTranslation.isError())
            qDebug() << wordTranslation.errorString();
        else {
            QtYaWord wordForTranslation = wordTranslation.wordForTranslation();
            QtYaTranslatedWord translatedWord = wordTranslation.translatedWord();
            qDebug() << "n***************";
            qDebug() << "Word: " << wordForTranslation.wordName();
            qDebug() << "Direction: " << wordForTranslation.fromLanguage() << "-" << wordForTranslation.toLanguage();
            qDebug() << "Main translation: " << translatedWord.mainTranslation();
            qDebug() << "Synonyms: " << translatedWord.synonyms();
            qDebug() << "Examples: ";
            for (const auto& example : translatedWord.examples()) {
                qDebug() << example.first << "-" << example.second;
            }
        }
    });

    QStringList russianWords, englishWords;
    russianWords << "дом" << "время" << "легенда" << "ключ" << "клавиатура" << "монитор" << "случай" << "один" << "два" << "три" << "четыре" << "пять" << "шесть";
    englishWords << "home" << "time" << "legend" << "key" << "keyboard" << "monitor" << "infection" << "one" << "two" << "three" << "four" << "five" << "success";

    for (const QString& word : russianWords) {
        yandexDictionary.translate(QtYaWord(word, "ru", "en"));
    }
    for (const QString& word : englishWords) {
        yandexDictionary.translate(QtYaWord(word, "en", "ru"));
    }

    return a.exec();
}


Результат работы программы:

***************
Word: «время»
Direction: «ru» — «en»
Main translation: «time»
Synonyms: («while»)
Examples:
«тромбопластиновое время» — «thromboplastin time»
«принять промежуток времени» — «take a while»

***************
Word: «клавиатура»
Direction: «ru» — «en»
Main translation: «keyboard»
Synonyms: («keypad», «pad»)
Examples:
«экранная клавиатура» — «onscreen keyboard»
«ЖКИ-клавиатура» — «LCD keypad»
«цифровая клавиатура» — «numeric pad»

***************
Word: «ключ»
Direction: «ru» — «en»
Main translation: «key»
Synonyms: («turnkey», «clue», «wrench», «dongle»)
Examples:
«деблокировочный ключ» — «unblocking key»
«решение под ключ» — «turnkey solution»
«иметь никакой ключ» — «have no clue»
«шестигранный ключ» — «hexagonal wrench»

***************
Word: «монитор»
Direction: «ru» — «en»
Main translation: «monitor»
Synonyms: ()
Examples:
«ЭЛТ-монитор» — «CRT monitor»

***************
Word: «четыре»
Direction: «ru» — «en»
Main translation: «four»
Synonyms: ()
Examples:
«двадцать четыре часа» — «twenty four hours»

***************
Word: «один»
Direction: «ru» — «en»
Main translation: «one»
Synonyms: ()
Examples:
«одна иота» — «one jot»

***************
Word: «два»
Direction: «ru» — «en»
Main translation: «two»
Synonyms: ()
Examples:
«две несмешивающиеся жидкости» — «two immiscible liquids»

***************
Word: «легенда»
Direction: «ru» — «en»
Main translation: «legend»
Synonyms: («myth»)
Examples:
«древнегреческая легенда» — «ancient Greek legend»
«городская легенда» — «urban myth»

***************
Word: «три»
Direction: «ru» — «en»
Main translation: «three»
Synonyms: ()
Examples:
«три мушкетера» — «three musketeers»

***************
Word: «случай»
Direction: «ru» — «en»
Main translation: «case»
Synonyms: («event», «occasion», «chance», «occurrence»)
Examples:
«невзвешенный случай» — «unweighted case»
«несчастный случай» — «unfortunate event»
«тот редкий случай» — «that rare occasion»
«слепой случай» — «blind chance»
«редкий случай» — «rare occurrence»

***************
Word: «пять»
Direction: «ru» — «en»
Main translation: «five»
Synonyms: ()
Examples:
«семьдесят пять лет» — «seventy five year»

***************
Word: «home»
Direction: «en» — «ru»
Main translation: «дом»
Synonyms: ()
Examples:
«ancestral home» — «отчий дом»

***************
Word: «шесть»
Direction: «ru» — «en»
Main translation: «six»
Synonyms: ()
Examples:
«шесть сигм» — «six Sigma»

***************
Word: «time»
Direction: «en» — «ru»
Main translation: «время»
Synonyms: («раз», «тайм»)
Examples:
«thromboplastin time» — «тромбопластиновое время»
«umpteenth time» — «энный раз»
«second time» — «второй тайм»

***************
Word: «key»
Direction: «en» — «ru»
Main translation: «ключевой»
Synonyms: ()
Examples:
«key informants» — «ключевые информанты»

***************
Word: «monitor»
Direction: «en» — «ru»
Main translation: «монитор»
Synonyms: («видеомонитор»)
Examples:
«LCD monitor» — «жидкокристаллический монитор»

***************
Word: «infection»
Direction: «en» — «ru»
Main translation: «инфекция»
Synonyms: («зараза»)
Examples:
«cholangiogenic infection» — «холангиогенная инфекция»
«this infection» — «эта зараза»

***************
Word: «one»
Direction: «en» — «ru»
Main translation: «один»
Synonyms: ()
Examples:
«one jot» — «одна иота»

***************
Word: «keyboard»
Direction: «en» — «ru»
Main translation: «клавиатура»
Synonyms: ()
Examples:
«QWERTY keyboard» — «клавиатура QWERTY»

***************
Word: «three»
Direction: «en» — «ru»
Main translation: «три»
Synonyms: ()
Examples:
«three musketeers» — «три мушкетера»

***************
Word: «four»
Direction: «en» — «ru»
Main translation: «четыре»
Synonyms: ()
Examples:
«twenty-four» — «двадцать четыре»

***************
Word: «two»
Direction: «en» — «ru»
Main translation: «два»
Synonyms: ()
Examples:
«two immiscible liquids» — «две несмешивающиеся жидкости»

***************
Word: «five»
Direction: «en» — «ru»
Main translation: «пять»
Synonyms: ()
Examples:
«ninety-five» — «девяносто пять»

***************
Word: «legend»
Direction: «en» — «ru»
Main translation: «легенда»
Synonyms: («предание», «сказание», «поверье»)
Examples:
«ancient Greek legend» — «древнегреческая легенда»
«ancient legend» — «древнее предание»
«epic legends» — «эпические сказания»
«folk legend» — «народное поверье»

***************
Word: «success»
Direction: «en» — «ru»
Main translation: «успех»
Synonyms: («успешность», «удача»)
Examples:
«resounding success» — «оглушительный успех»
«financial success» — «финансовая успешность»
«bring success» — «приносить удачу»

***************
Word: «дом»
Direction: «ru» — «en»
Main translation: «house»
Synonyms: ()
Examples:
«дом Стенбока» — «Stenbock house»

Код этого примера, а так же реализация самого клиента — bitbucket.org/milovidov/qtyandexapi.

Реализация клиента

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

#ifndef QTYANDEXDICTIONARY_H
#define QTYANDEXDICTIONARY_H

#include <QObject>

#include "qtyandexapi_global.h"

#include "QtYaWord.h"
#include "QtYaWordTranslation.h"
#include "QtYaLanguages.h"

class QNetworkAccessManager;

class QTYANDEXAPISHARED_EXPORT QtYandexDictionary : public QObject
{
    Q_OBJECT
public:
    explicit QtYandexDictionary(const QString& yandexApiKey, QObject *pParent = 0);

    void translate(const QtYaWord& word);
    void getLanguages();

signals:
    void translated(const QtYaWordTranslation& translation) const;
    void languagesGot(const QtYaLanguages& langs) const;

private:
    QString m_yandexApiKey;
    QNetworkAccessManager* m_pNetworkAccessManager;
};

#endif // QTYANDEXDICTIONARY_H

Методы translate и getLanguages асинхронные. Они формируют и отправляют соответствующие запросы к Yandex Dictionary Api и возвращают управление потоку из которого были вызваны.

Сигналы translated и languagesGot эмитятся после получения какого либо результата от Yandex Dictionary Api, либо при невозможности подключиться к этому сервису. В аргументах этих сигналов содержится информация полученная от Яндекса или же текст ошибки при невозможности подключиться к нему.

Если посмотреть на код функции translate класса QtYandexDictionary, то она выглядит следующим образом:

void QtYandexDictionary::translate(const QtYaWord &word)
{
    QtYaTranslationGettor* pYandexTranslationGettor = new QtYaTranslationGettor(m_yandexApiKey, word, m_pNetworkAccessManager, this);
    connect(pYandexTranslationGettor, &QtYaTranslationGettor::translated, this, &QtYandexDictionary::translated);
    connect(pYandexTranslationGettor, &QtYaTranslationGettor::translated, pYandexTranslationGettor, &QtYaTranslationGettor::deleteLater);
}

Здесь мы видим, что для каждого слова переданного в функцию translate создается уникальный экземпляр класса QtYaTranslationGettor. Этот класс непосредственно занимается переводом слов через Yandex Dictionary Api и так же эмитит сигнал translated. Этот же сигнал, без каких либо изменений пробрасывается наверх к QtYandexDictionary. После эмита этого сигнала экземпляр класса QtYaTranslationGettor удаляется за счет коннекта к его же слоту deleteLater.

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

Метода getLanguages реализован аналогично:

void QtYandexDictionary::getLanguages()
{
    QtYandexLanguagesGettor* pYandexLanguagesGettor = new QtYandexLanguagesGettor(m_yandexApiKey, m_pNetworkAccessManager, this);
    connect(pYandexLanguagesGettor, &QtYandexLanguagesGettor::languagesGot, this, &QtYandexDictionary::languagesGot);
    connect(pYandexLanguagesGettor, &QtYandexLanguagesGettor::languagesGot, pYandexLanguagesGettor, &QtYandexLanguagesGettor::deleteLater);
}

Реализация классов QtYaTranslationGettor и QtYandexLanguagesGettor содержит в себе стандартную практику работы с классом QNetworkAccessManager и работу с JSON в Qt5. Поэтому не буду заострять внимания на деталях их реализации.

Тестирование клиента

В данном разделе я покажу, как можно тестировать асинхронные Qt классы.

Пример тестирования асинхронной функции translate выглядит так:

void TQtYandexApi::testTranslation()
{
    const QString rusHome = QString("Home");
    QSignalSpy signalSpy(m_pYandexDictionary, SIGNAL(translated(QtYaWordTranslation)));
    QMetaObject::Connection connection = connect(m_pYandexDictionary, &QtYandexDictionary::translated,
                                                 [&](const QtYaWordTranslation& translation)
    {
        QVERIFY2(translation.isError() == false, translation.errorString().toStdString().c_str());
        QVERIFY(translation.wordForTranslation().wordName() == rusHome);
        QVERIFY(translation.wordForTranslation().fromLanguage() == "en");
        QVERIFY(translation.wordForTranslation().toLanguage() == "ru");
        QVERIFY(translation.translatedWord().mainTranslation().toLower() == QString::fromUtf8("дом"));
    });
    m_pYandexDictionary->translate(QtYaWord(rusHome, "en", "ru"));
    END_ASYNC_FUNCTION(signalSpy, connection)
}

В начале создаем слово для перевода с английского на русский — переменная rusHome.

Затем создаем signalSpy — это объект будет ждать сигнала от m_pYandexDictionary.

Далее создается обработчик сигнала translated в виде лямбды выражения. В ней проверяется правильность перевода слова.

После этого отправляется запрос на перевод и вызывается макрос END_ASYNC_FUNCTION.

Код этого макроса:

#define END_ASYNC_FUNCTION(signalSpy, connection) 
    QVERIFY(signalSpy.wait()); 
    QObject::disconnect(connection);

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

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

Выводы

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

Автор: 1vertus1

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js