2026年03月27日
コメント
本記事はゲスト執筆者 Kevin Ottens による 「Qt Widgets to Qt Quick, An Application Journey Part 4」を翻訳したものです。
ソフトウェアアーキテクチャの移行を完了
このシリーズの前回では、サンプルとなるQtウィジェットベースのアプリケーションをMVPアーキテクチャへ移行する作業を開始しました。コードの大部分は、ウィジェットから、役割が明確に定義された2つのプロキシ(1つはドメインアイテムをラップし、もう1つはビジネスロジックをラップすることを目的としています)へと移行されました。
本シリーズの第4回、最終回となる今回は、アーキテクチャの移行を完了させ、この段階で新しいGUIの改良がいかに容易になるかを確認していきます。
このアプリケーションの場合、ビジネスロジックはすべて、1つのスロットと2つのメソッド、すなわちonUpdateCurrentItemQuality()およびupdateQuality()によって駆動されています。これらをPageProxy [コミット 5eea88d]に移動するだけで済みます。
その結果、Windowクラスの実装は非常に簡潔になります:
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();
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);
}
このクラスは、基本的には関係性を設定する単一のコンストラクタだけになりました。これ以上に宣言的な感覚を与えることはできません。また、下位レベルの依存関係(Repositoryなど)をプロキシに移したため、計画通りカプセル化と関心の分離が改善されました。
もちろん、基本的にはコードの配置を変更するだけで済んだため、PageProxyのコード量はほぼ同程度増えました。
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);
}
現時点では、テストのおかげで後退を引き起こすことなくこれらすべての変更を実施できたため、updateQuality()内のロジックを簡素化することさえ検討できるでしょう。しかし、それはまた別の機会に検討すべき課題です。
あと少しです!アプリケーション全体をMVPパターンに移行し、全体的にずいぶん使いやすくなりました。次は、Qt Quick GUIを追加する段階です [コミット 7e4e72a] および [コミット 66accfb]。
このため、Window.qmlファイルを用意し、これを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
}
}
}
}
簡潔にするため、一部のコードは省略されています。お気づきかもしれませんが、このQt QuickベースのウィンドウとPageProxyとの連携は、Qt WidgetsベースのWindowとの連携と非常に似ているように見えます。
次に、メイン関数内で、以前のWindowインスタンスを作成していたコードを、以下のコードに置き換えます:
QQmlApplicationEngine engine;
engine.loadFromModule("com.gildedrose", "Window");
そして、新しいQt Quick ベースの GUIに移行しました。

ついにMVPに向けたアーキテクチャの移行を完了し、さらにその上に新しいQtQuick GUIを実装しました。現時点では、QtWidgetsとQtQuickの両方のGUIが完全に機能しています。両方を並行して動作させることも、条件付きコンパイルを利用してどちらか一方のみを使用することも可能です。これにより、ユーザーにとって最適なペースで移行を進めるための柔軟性が確保されています。
さて、このテストについて何か手を加える必要があるかもしれません。選択肢は3つあります:
ひとまず完全に廃止し、今後価値がなくなれば(例えば、時間の経過とともに適切なユニットテストが蓄積された場合など)、改めて検討するようにします。
現状のままにしておくという選択肢もありますが、Qt Widgets GUIが完全に廃止された場合、中長期的には現実的ではないかもしれません。
GUIを経由するのではなく、PageProxy上で直接動作するように修正してください(その横に、スタブ化されたプロキシを使った、よりシンプルなGUIのみのテストを用意するのも一案です)。
この3つの選択肢のうち、GUIの遷移への影響が最も少ないため、最後の選択肢が間違いなく私たちの希望です。もちろん、将来的に不要になった場合には、それを廃止することも可能です。
このシリーズを通じて、私たちのアプローチには一定の規律が必要ではありますが、アプリケーションをQt WidgetsからQt Quickへ移行するための有効な方法であるということを、皆様にご理解いただけたことを願っております。
私たちのアプローチの重要なポイントを整理してみましょう:
移行期間中に非回帰テストとして機能する、シンプルで広範囲をカバーするテストを作成してください。
不足しているQAbstractItemModelのサブクラスを追加し、QListWidget、QTableWidget、およびQTreeWidgetを削除して、それぞれのビュー版を優先するようにします。
ドメインオブジェクト用に、QObject ベースのプロキシクラスを導入してください。
QObjectをベースにしたプロキシクラスを導入し、QWidgetのサブクラスに閉じ込められたビジネスルールを受け取ります。
プロキシに残っているビジネスロジックをすべて移動してください。
導入されたプロキシを基盤として、新しいQt Quick ベースの GUIを開発します。
Qt Widgets ベースの GUI を削除してください。
最後に、Qt Widgetsを使い続ける予定であっても、MVPパターンへの移行には価値があるという点にも触れておきます。上記のステップ6と7を実行しなければならないという決まりはありません。たとえ実行しなくても、他のステップを踏むことで、全体としてより優れたアーキテクチャが実現され、関心の分離が図られ、アプリケーション全体のテストしやすさが向上するでしょう。
enioka Haute Couture について
あらゆる業種において、ITシステムへの依存度は極めて高まっています。しかし、多くの企業が、この重要な資産の管理を適切に行えていないのが現状です。enioka Haute Coutureは、プロジェクトの分野、組織体制、あるいはスケジュールといった要因にかかわらず、複雑なソフトウェア開発を確実に遂行することに特化しています。
enioka Haute Coutureは、お客様のチームと緊密に連携し、特定のニーズに合わせたソフトウェアを構築いたします。
また、開発にあたっては、お客様がシステムを確実に管理できるよう、細心の注意を払います。eniokaは、お客様のチームが最適な組織体制、技術、ツールを導入できるよう支援いたします。さらに、適切な場合には、フリー・オープンソース・ソフトウェア(FOSS)プロジェクトの立ち上げや、既存のプロジェクトとの連携についてもサポートいたします。enioka Haute Coutureなら、お客様はシステムを完全に管理しつつ、開発ニーズを実現できる信頼できるパートナーを得ることができます。ベンダーやサービスプロバイダーへの依存から解放されます。
最新リリースはこちらからダウンロードできます。 www.qt.io/download
Qt 6.10 がリリースされました!アプリケーション開発者やデバイス開発者向けに、多くの新機能と改善が追加されています。
現在、さまざまなポジションで採用を行っています。募集職種はこちら をご覧ください。また、Instagram をフォローして #QtPeople の働き方もぜひチェックしてください。