Dans le précédent article de cette série, nous avons commencé à migrer notre exemple d’application basée sur QtWidgets vers une architecture MVP. La majeure partie du code a été déplacée d’un widget vers deux proxys aux rôles bien définis (l’un encapsulant les objets du domaine, l’autre encapsulant la logique métier).
Dans ce quatrième et dernier article de notre série, nous allons terminer la transition vers la nouvelle architecture et voir à quel point il est à présent facile d’itérer vers une nouvelle interface graphique.
Cet article est déjà apparu en anglais sur le Qt blog.
Transférer la logique métier restante dans le proxy correspondant
Dans le cas de notre application, la logique métier est entièrement pilotée par un slot et une méthode : onUpdateCurrentItemQuality() et updateQuality(). Il suffit de les déplacer vers PageProxy (commit 5eea88d) .
L’implémentation résultante pour la classe Window devient très légère :
Window::Window(QWidget *parent) : QWidget(parent) , ui(new Ui::Window) // Note: the dependency on Repository is gone , m_pageProxy(new PageProxy(this)){ ui->setupUi(this); connect(ui->currentItemCombo, &QComboBox::currentIndexChanged, [=] { const auto id = ui->currentItemCombo->currentData().value<quint64>(); m_pageProxy->item()->setItemId(id); }); connect(m_pageProxy->item(), &ItemProxy::sellInChanged, ui->sellInSpinBox, &QSpinBox::setValue); connect(ui->sellInSpinBox, &QSpinBox::valueChanged, m_pageProxy->item(), &ItemProxy::setSellIn); connect(m_pageProxy->item(), &ItemProxy::qualityChanged, ui->qualitySpinBox, &QSpinBox::setValue); connect(ui->qualitySpinBox, &QSpinBox::valueChanged, m_pageProxy->item(), &ItemProxy::setQuality); // This connect changed to trigger the slot on the PageProxy connect(ui->updateButton, &QPushButton::clicked, m_pageProxy, &PageProxy::updateItemQuality); ui->currentItemCombo->setModel(m_pageProxy->model()); ui->tableView->setModel(m_pageProxy->model()); ui->tableView->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);}
La classe se résume désormais essentiellement à un unique constructeur qui établit les relations. On ne peut pas faire plus déclaratif que cela. De plus, nous avons déplacé les dépendances de niveau inférieur (comme Repository) vers le proxy, ce qui nous a permis d’améliorer l’encapsulation et la séparation des responsabilités comme prévu.
Bien sûr, il s’agissait principalement de déplacer du code, donc PageProxy a augmenté d’environ la même quantité de code.
void PageProxy::updateItemQuality() { Item item = m_itemProxy->item(); updateQuality(item); m_itemProxy->reloadData();}void PageProxy::updateQuality(Item &item) { // Long and complex logic to update item quality field and decrease sellIn by one // Followed by: m_repository->save(item);}
À ce stade, puisque nous avons effectué toutes ces modifications sans introduire de régressions grâce à notre test, nous pourrions même envisager de simplifier la logique dans updateQuality(). Mais c’est une bataille pour un autre jour.
Mise en place de l’interface graphique QtQuick
Nous y sommes presque ! Nous avons migré l’ensemble de notre application vers le modèle MVP, ce qui améliore considérablement son fonctionnement général. Il est maintenant temps d’ajouter l’interface graphique QtQuick (commit 7e4e72a et commit 66accfb).
Pour cela, nous introduisons un fichier Window.qml que nous déclarons dans le module com.gildedrose :
import QtQuickimport QtQuick.Controlsimport QtQuick.Layoutsimport com.gildedroseApplicationWindow { visible: true title: "Gilded Rose (QtQuick)" PageProxy { id: pageProxy currentItem.itemId: currentItemCombo.currentValue } ColumnLayout { anchors.fill: parent GridLayout { Layout.fillWidth: true columns: 2 Label { text: "Current item" } ComboBox { id: currentItemCombo model: pageProxy.model textRole: "display" valueRole: "user" } Label { text: "Quality" } SpinBox { id: qualitySpinBox value: pageProxy.currentItem.quality // Simulating bidirectional bindings Binding { pageProxy.currentItem.quality: qualitySpinBox.value } } Label { text: "Remaining days" } SpinBox { id: sellInSpinBox value: pageProxy.currentItem.sellIn // Simulating bidirectional bindings Binding { pageProxy.currentItem.sellIn: sellInSpinBox.value } } } Button { id: updateButton Layout.fillWidth: true text: "Update item quality" onClicked: pageProxy.updateItemQuality() } Rectangle { Layout.fillWidth: true height: 1 color: "lightGray" } HorizontalHeaderView { ... } TableView { id: tableView Layout.fillWidth: true Layout.fillHeight: true columnSpacing: 1 rowSpacing: 1 model: pageProxy.model delegate: TableViewDelegate { padding: 5 } } }}
Une partie du code a été omise pour des raisons de concision. Vous remarquerez peut-être que l’intégration entre cette fenêtre basée sur QtQuick et PageProxy ressemble beaucoup à celle de notre Window basée sur QtWidgets.
Ensuite, dans la fonction principale, nous remplaçons le code créant notre ancienne instance Window par le code suivant :
QQmlApplicationEngine engine;engine.loadFromModule("com.gildedrose", "Window");
Nous sommes alors passés à la nouvelle interface graphique basée sur QtQuick.

Et maintenant ?
Nous avons enfin achevé notre transition architecturale vers MVP et avons même ajouté une nouvelle interface graphique QtQuick. À ce stade, les interfaces graphiques QtWidgets et QtQuick sont toutes deux pleinement fonctionnelles. Nous pourrions les faire fonctionner en parallèle ou utiliser la compilation conditionnelle pour n’utiliser que l’une ou l’autre. Nous disposons ainsi d’une liberté totale pour mener la transition au rythme qui convient à nos utilisateurs.
Nous pourrions maintenant vouloir faire quelque chose pour nos tests. Trois options s’offrent à nous :
- les abandonner complètement, pour les réexaminer plus tard s’ils n’apportent plus de valeur ajoutée (par exemple, parce que nous avons accumulé des tests unitaires appropriés au fil du temps)
- les conserver tels quels, ce qui pourrait ne pas être pratique à moyen ou long terme si l’interface graphique QtWidgets est complètement mise hors service
- les modifier pour qu’ils fonctionnent directement sur
PageProxyplutôt que de passer par l’interface graphique (peut-être avec un test beaucoup plus simple de l’interface graphique à côté, avec un stub proxy)
Conclusion
Nous espérons que cette série vous aura convaincu que notre approche, bien qu’elle exige de la discipline, est valable pour faire passer une application de QtWidgets à QtQuick.
Passons en revue les points importants de notre approche :
- Écrire des tests simples à large couverture qui serviront de tests de non-régression pendant la transition
- Faire apparaître les sous-classes
QAbstractItemModelmanquantes, se débarrasser desQListWidget,QTableWidgetetQTreeWidgetau profit des vues équivalentes - Introduire des classes proxy basées sur
QObjectpour vos objets de domaine - Introduire des classes proxy basées sur
QObjectpour recevoir les règles métier contenues dans vos sous-classesQWidget - Déplacer toute la logique métier restante dans les proxys
- Développer la nouvelle interface graphique basée sur QtQuick à partir des proxys introduits
- Supprimer l’interface graphique basée sur QtWidgets
Pour conclure, nous soulignons également qu’il est intéressant de passer au modèle MVP même si vous prévoyez de conserver QtWidgets. Rien ne vous oblige à exécuter les étapes 6 et 7 ci-dessus. Même si vous ne le faites pas, les autres étapes vous permettront d’obtenir une meilleure architecture globale, avec une séparation des responsabilités améliorée et une meilleure testabilité de l’ensemble de l’application.

