De QtWidgets à QtQuick, la transition d’une application, partie 4 – Compléter la transition de l’architecture logicielle

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 QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import com.gildedrose
ApplicationWindow {
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 :

  1. 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)
  2. 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
  3. les modifier pour qu’ils fonctionnent directement sur PageProxy plutô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 :

  1. Écrire des tests simples à large couverture qui serviront de tests de non-régression pendant la transition
  2. Faire apparaître les sous-classes QAbstractItemModel manquantes, se débarrasser des QListWidget, QTableWidget et QTreeWidget au profit des vues équivalentes
  3. Introduire des classes proxy basées sur QObject pour vos objets de domaine
  4. Introduire des classes proxy basées sur QObject pour recevoir les règles métier contenues dans vos sous-classes QWidget
  5. Déplacer toute la logique métier restante dans les proxys
  6. Développer la nouvelle interface graphique basée sur QtQuick à partir des proxys introduits
  7. 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.


Publié

dans

par

En savoir plus sur enioka

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Poursuivre la lecture