バインダブル・プロパティ

バインダブル・プロパティを使用することで、C++コードを簡素化できることを説明します。

この例では、互いに依存する異なるオブジェクト間の関係を表現するための2つのアプローチ(シグナル/スロット接続ベースとバインダブル・プロパティ・ベース)を示します。この目的のために、サブスクリプションのコストを計算するサブスクリプション・サービス・モデルを考えます。

シグナル/スロットアプローチによるサブスクリプションシステムのモデリング

まず、通常のQt 6以前の実装を考えてみましょう。サブスクリプションサービスをモデル化するために、Subscription クラスが使用されます:

class Subscription : public QObject
{
    Q_OBJECT
public:
    enum Duration { Monthly = 1, Quarterly = 3, Yearly = 12 };

    Subscription(User *user);

    void calculatePrice();
    int price() const { return m_price; }

    Duration duration() const { return m_duration; }
    void setDuration(Duration newDuration);

    bool isValid() const { return m_isValid; }
    void updateValidity();

signals:
    void priceChanged();
    void durationChanged();
    void isValidChanged();

private:
    double calculateDiscount() const;
    int basePrice() const;

    QPointer<User> m_user;
    Duration m_duration = Monthly;
    int m_price = 0;
    bool m_isValid = false;
};

これは、サブスクリプションに関する情報を格納し、対応するゲッター、セッター、およびサブスクリプション情報の変更をリスナーに通知するためのノティファイアシグナルを提供します。また、User クラスのインスタンスへのポインタを保持します。

サブスクリプションの価格はサブスクリプションの期間に基づいて計算される:

double Subscription::calculateDiscount() const
{
    switch (m_duration) {
    case Monthly:
        return 1;
    case Quarterly:
        return 0.9;
    case Yearly:
        return 0.6;
    }
    Q_ASSERT(false);
    return -1;
}

そしてユーザーの場所:

int Subscription::basePrice() const
{
    if (m_user->country() == User::Country::AnyTerritory)
        return 0;

    return (m_user->country() == User::Country::Norway) ? 100 : 80;
}

価格が変更されると、priceChanged() シグナルが発信され、リスナーに変更を通知します:

void Subscription::calculatePrice()
{
    const auto oldPrice = m_price;

    m_price = qRound(calculateDiscount() * int(m_duration) * basePrice());
    if (m_price != oldPrice)
        emit priceChanged();
}

同様に、購読期間が変更されると、durationChanged() シグナルが発信されます。

void Subscription::setDuration(Duration newDuration)
{
    if (newDuration != m_duration) {
        m_duration = newDuration;
        calculatePrice();
        emit durationChanged();
    }
}

注: どちらのメソッドも、データが実際に変更されたかどうかをチェックし、それからシグナルを発信する必要があります。setDuration() は、期間が変更されたときに価格を再計算する必要もあります。

Subscription は、ユーザーが有効な国と年齢を持っていない限り有効ではないので、有効性は次の方法で更新される:

void Subscription::updateValidity()
{
    bool isValid = m_isValid;
    m_isValid = m_user->country() != User::Country::AnyTerritory && m_user->age() > 12;

    if (m_isValid != isValid)
        emit isValidChanged();
}

User クラスはシンプルで、ユーザーの国と年齢を保存し、対応するゲッター、セッター、ノーティファイア・シグナルを提供します:

class User : public QObject
{
    Q_OBJECT

public:
    using Country = QLocale::Territory;

public:
    Country country() const { return m_country; }
    void setCountry(Country country);

    int age() const { return m_age; }
    void setAge(int age);

signals:
    void countryChanged();
    void ageChanged();

private:
    Country m_country { QLocale::AnyTerritory };
    int m_age { 0 };
};

void User::setCountry(Country country)
{
    if (m_country != country) {
        m_country = country;
        emit countryChanged();
    }
}

void User::setAge(int age)
{
    if (m_age != age) {
        m_age = age;
        emit ageChanged();
    }
}

main() 関数では、UserSubscription のインスタンスを初期化する:

    User user;
    Subscription subscription(&user);

そして、UI要素が変更されたときに、usersubscription のデータを更新するために、適切なシグナル・スロット接続を行います。これは簡単なので、この部分は省略します。

次に、価格が変更されたときにUIの価格を更新するためにSubscription::priceChanged()

    QObject::connect(&subscription, &Subscription::priceChanged, priceDisplay, [&] {
        QLocale lc{QLocale::AnyLanguage, user.country()};
        priceDisplay->setText(lc.toCurrencyString(subscription.price() / subscription.duration()));
    });

また、Subscription::isValidChanged() に接続して、購読が有効でない場合に価格の表示を無効にします。

    QObject::connect(&subscription, &Subscription::isValidChanged, priceDisplay, [&] {
        priceDisplay->setEnabled(subscription.isValid());
    });

購読価格と有効期限はユーザーの国と年齢にも依存するため、User::countryChanged()User::ageChanged() のシグナルにも接続し、subscription を適宜更新する必要があります。

    QObject::connect(&user, &User::countryChanged, &subscription, [&] {
        subscription.calculatePrice();
        subscription.updateValidity();
    });

    QObject::connect(&user, &User::ageChanged, &subscription, [&] {
        subscription.updateValidity();
    });

これは動作しますが、いくつかの問題があります:

  • usersubscription の両方の変更を適切に追跡するために、シグナルスロット接続のための定型的なコードがたくさんあります。価格の依存関係のいずれかが変更された場合、対応するノーティファイアシグナルを発信し、価格を再計算し、UIで更新することを忘れないようにする必要があります。
  • 将来、価格計算の依存関係がさらに追加された場合、シグナルスロットの接続をさらに追加し、依存関係のいずれかが変更されるたびに、すべての依存関係が適切に更新されるようにする必要があります。全体的な複雑さが増し、コードのメンテナンスが難しくなるだろう。
  • SubscriptionUser クラスは、シグナル/スロットのメカニズムを使用できるようにメタオブジェクトシステムに依存しています。

もっといい方法はないだろうか?

バインド可能なプロパティによるサブスクリプションシステムのモデリング

それでは、Qtのバインダブル・プロパティが同じ問題を解決するのに役立つ方法を見てみましょう。まず、BindableSubscription クラスを見てみましょう。Subscription クラスと似ていますが、バインダブル・プロパティを使用して実装されています:

class BindableSubscription
{
public:
    enum Duration { Monthly = 1, Quarterly = 3, Yearly = 12 };

    BindableSubscription(BindableUser *user);
    BindableSubscription(const BindableSubscription &) = delete;

    int price() const { return m_price; }
    QBindable<int> bindablePrice() { return &m_price; }

    Duration duration() const { return m_duration; }
    void setDuration(Duration newDuration);
    QBindable<Duration> bindableDuration() { return &m_duration; }

    bool isValid() const { return m_isValid; }
    QBindable<bool> bindableIsValid() { return &m_isValid; }

private:
    double calculateDiscount() const;
    int basePrice() const;

    BindableUser *m_user;
    QProperty<Duration> m_duration { Monthly };
    QProperty<int> m_price { 0 };
    QProperty<bool> m_isValid { false };
};

最初の違いは、データ・フィールドがQProperty クラスにラップされ、ノーティファイア・シグナル(結果としてメタオブジェクト・システムからの依存性)がなくなり、代わりにQProperty ごとにQBindable を返す新しいメソッドが追加されていることです。また、calculatePrice()updateValidty() のメソッドも削除されました。これらが不要になった理由は後述する。

BindableUser クラスはUser クラスと同じような違いがあります:

class BindableUser
{
public:
    using Country = QLocale::Territory;

public:
    BindableUser() = default;
    BindableUser(const BindableUser &) = delete;

    Country country() const { return m_country; }
    void setCountry(Country country);
    QBindable<Country> bindableCountry() { return &m_country; }

    int age() const { return m_age; }
    void setAge(int age);
    QBindable<int> bindableAge() { return &m_age; }

private:
    QProperty<Country> m_country { QLocale::AnyTerritory };
    QProperty<int> m_age { 0 };
};

つ目の違いは、これらのクラスの実装です。まず、subscriptionuser の間の依存関係は、バインディング式によって追跡されるようになりました:

BindableSubscription::BindableSubscription(BindableUser *user) : m_user(user)
{
    Q_ASSERT(user);

    m_price.setBinding(
            [this] { return qRound(calculateDiscount() * int(m_duration) * basePrice()); });

    m_isValid.setBinding([this] {
        return m_user->country() != BindableUser::Country::AnyCountry && m_user->age() > 12;
    });
}

舞台裏では、バインディング可能なプロパティが依存関係の変更を追跡し、変更が検出されるたびにプロパティの値を更新します。そのため、例えばユーザーの国や年齢が変更された場合、サブスクリプションの価格と有効期限が自動的に更新されます。

もう1つの違いは、セッターが些細なものになったことです:

void BindableSubscription::setDuration(Duration newDuration)
{
    m_duration = newDuration;
}

void BindableUser::setCountry(Country country)
{
    m_country = country;
}

void BindableUser::setAge(int age)
{
    m_age = age;
}

プロパティの値が実際に変更されたかどうかをセッターの内部でチェックする必要はありません。QProperty が既にそれを行います。値が実際に変更された場合にのみ、従属プロパティに変更が通知されます。

UIで価格に関する情報を更新するコードも単純化されます:

    auto priceChangeHandler = subscription.bindablePrice().subscribe([&] {
        QLocale lc{QLocale::AnyLanguage, user.country()};
        priceDisplay->setText(lc.toCurrencyString(subscription.price() / subscription.duration()));
    });

    auto priceValidHandler = subscription.bindableIsValid().subscribe([&] {
        priceDisplay->setEnabled(subscription.isValid());
    });

bindablePrice()bindableIsValid() を介して変更を購読し、これらのプロパティのいずれかが値を変更したときに、それに応じて価格表示を更新します。サブスクリプションは、対応するハンドラが生きている限り、生き続けます。

また、BindableSubscriptionBindableUser の両方のコピーコンストラクタが無効になっていることに注意してください。コピー時にバインディングがどうなるかが定義されていないからです。

見ての通り、コードはずっとシンプルになり、上記の問題も解決した:

  • シグナルスロット接続のための定型的なコードが削除され、依存関係が自動的に追跡されるようになりました。
  • コードのメンテナンスが容易になった。将来的に依存関係を追加するには、対応するバインド可能なプロパティを追加し、お互いの関係を反映するバインド式を設定するだけです。
  • SubscriptionUser クラスは、メタオブジェクトシステムに依存しなくなりました。もちろん、必要であればメタオブジェクトシステムに公開してQ_PROPERTYを追加し、C++QML のコードの両方でバインド可能プロパティの利点を利用することはできます。そのためにはQObjectBindableProperty

プロジェクト例 @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。