English Русский 中文 Español Deutsch Português
preview
MetaTrader 5用のMQTTクライアントの開発:TDDアプローチ(第3部)

MetaTrader 5用のMQTTクライアントの開発:TDDアプローチ(第3部)

MetaTrader 5統合 | 29 1月 2024, 11:19
118 0
Jocimar Lopes
Jocimar Lopes

「自分のコードがすべて機能していることを知らないで、どうしてプロフェッショナルだと言えるのか。変更を加えるたびにテストしなければ、すべてのコードが機能することをどうやって知ることができるだろうか。非常に高いカバレッジの自動単体テストがなければ、変更を加えるたびにテストすることはできない。TDDを実践せずに、カバレッジの非常に高い自動単体テストを得るにはどうすればいいのか。」(Robert 「Uncle Bob」Martin、The Clean Coder、 2011)

はじめに

これまで、本連載の第1部と第2部では、MQTTプロトコルの非操作部分のごく一部を扱ってきました。プロトコルの定義、列挙、そしてクラス間で共有されるいくつかの共通関数のすべてを、2つの別々のヘッダーファイルに整理しました。また、オブジェクト階層のルートとして待機するインターフェイスを記述し、適合するMQTT CONNECTパケットを構築することだけを目的として、1つのクラスに実装しました。その一方で、パケットのビルドに関わる各関数の単体テストを書いてきました。生成したパケットをローカルのMQTTブローカーに送り、それが整形式のMQTTパケットとして認識されるかどうかを確認しましたが、技術的にはこの手順は必要ありませんでした。フィクスチャーのデータを使って関数のパラメータを供給していたのだから、それを単独でテストしていることも、状態に依存しない方法でテストしていることもわかっていました。それは良いことで、私たちはテストを、ひいては関数を、そのように書き続けるよう努力します。これによってコードがより柔軟になり、同じ関数シグネチャを持つ限り、テストコードを変更することなく関数の実装を変更できるようになります。

ここからは、MQTTプロトコルの操作部分を扱います。驚くことではありませんが、OASIS規格ではOperational Behavior(操作時の動作)と命名されています。つまり、これからはサーバーから送られてくるパケットを処理する必要があるということです。クライアントは、サーバーのパケットタイプとそのセマンティクスを識別できなければなりません。そして、与えられたコンテキストにおける適切な動作、クライアントの可能な各状態における適切な動作を選択できなければなりません。

このタスクに対処するためには、応答の最初のバイトでサーバーのパケットタイプを特定しなければなりません。それがCONNACKパケットであれば、その接続理由コードを読み、それに応じて対応しなければなりません。


(CONNECT)クライアントConnect Flagsの設定

クライアントがサーバーとの接続を要求するとき、サーバーに次のことを通知しなければなりません。

  • ブローカーが望む能力
  • ユーザー名とパスワードによる認証が必要かどうか
  • この接続が新しいセッションなのか、それとも以前に開いたしたセッションを再開するものなのか

これは、変数ヘッダーの先頭、プロトコル名とプロトコルバージョンの直後に、いくつかのビットフラグを設定することでおこなわれます。CONNECTパケット上のこれらのビットフラグはConnect Flagsと呼ばれます。

ビットフラグはブーリアン値であることを忘れないでください。これらの値には異なる名前や表現が与えられることがありますが、ブーリアン値には2つの値しかなく、通常はtrueかfalseです。

図01:ブール値の表現によく使われる用語

図01:ブール値の表現によく使われる用語

OASIS規格では、一貫して1と0を使用しています。ここではほとんどの場合trueとfalseを使い、最終的にはsetとunsetを使うことになります。これでテキストが読みやすくなるはずです。さらに、公開APIでは、これらの値の設定にtrueとfalseを一貫して使用しています。これらの用語を使用することで、ライブラリの開発を追っている読者にとって、この記事がより理解しやすくなるはずです。

図02:OASIS Connectフラグビット

図02:OASIS Connectフラグビット

上の画像にあるOASISの表を見ればわかるように、最初のビット(bit_0)は予約されているので、そのままにしておかなければなりません(ゼロ、チェックなし、ブーリアンfalse、未設定)。設定すると、Malformed Packetが発生します。


Clean Start (bit_1)

最初に設定できるビットは2番目のビットです。これは、Clean Startフラグの設定に使用されます。trueの場合、サーバーはClean Startを実行し、クライアントIDに関連付けられた既存のセッションを破棄します。サーバーは新しいセッションを開始します。設定されていない場合、サーバーは以前の会話があれば再開し、クライアントIDに関連する既存のセッションがなければ新規セッションを開始します。

このフラグを設定/解除する関数は、現在このようになっています。

void CPktConnect::SetCleanStart(const bool cleanStart)
  {
   cleanStart ? m_connect_flags |= CLEAN_START : m_connect_flags &= ~CLEAN_START;
   ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags);
  }

ビット演算で値を切り替えます。コードをよりコンパクトにするために、三項演算子を使ってブーリアンと複合代入を切り替えています。そして、その結果をm_connect_flagsのprivateクラスメンバーに格納します。最後に、組み込み関数ArrayFillを呼び出して、CONNECTパケットを表すバイト配列を新しい値で更新します(この後半の手順である配列への記入は、実装の詳細であり、おそらく後で変更することになります)。

テストの1つで、この行がどのように呼び出されるかを示しています。

   CPktConnect *cut = new CPktConnect(buf);
//--- Act
   cut.SetCleanStart(true);


ビット 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


予約


X X X X X X 1 0

表01:CleanStart (bit_1)ビットフラグをtrueに設定 - MQTTv5.0

三項演算子や複合代入を使ったビット演算など、ブーリアンフラグの切り替えにはこのパターンを多用します。

Will Xと名付けられた以下の3つのフラグは、サーバーに何らかの能力があることを示すものです。私たちはサーバーに、次のことができるように望んでいます。

  1. Will Messageを保存し、それらをクライアントのセッションに関連付ける(これについては後で詳しく説明します)
  2. 特定のQoSレベルを提供する(通常は、何も設定されていない場合のデフォルトであるQoS0より上)
  3. Will Messageがtrueに設定されている場合、 Will Message(複数可)を保持し、「保持」(下記参照)として公開する


Will Flag (bit_2)

3番目のビットはWill Flagの設定に使用されます。trueに設定されていれば、クライアントは「ネットワーク接続が正常に終了しない場合」に公開されるWill Messageを提供しなければなりません。Will Messageは、サブスクライバーと向き合って死んでいくクライアントの「最後の言葉」のようなものだと考えることもできます。

void CPktConnect::SetWillFlag(const bool willFlag)
  {
   willFlag ? m_connect_flags |= WILL_FLAG : m_connect_flags &= ~WILL_FLAG;
   ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags);
  }

前の関数と同じように呼び出されます。

//--- Act
   CPktConnect *cut = new CPktConnect(buf);
   cut.SetWillFlag(true);


ビット 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


予約


X X X X X 1 X 0

表02:Will Flag (bit_2)ビットフラグをtrueに設定 - MQTTv5.0

Will QoS(bit_3、bit_4) 

前の2つのフラグとは異なり、この機能では、クライアントがQoSレベル2を要求している場合、4番目と5番目のビットの2つのビットを設定る必要があります。QoSはQuality of Serviceの略で、3つのうちの1つです。

図03:OASIS QoS定義

図03:OASIS QoS定義

最も信頼性の低い配送システムから最も信頼性の高い配信システムまで

QoS0

QoS0は最大1回の配信を設定します。一種の「火をつけて忘れる」です。送信者は一度だけ試行します。メッセージは失われるかもしれません。サーバーからの確認はありません。つまり、ビット3と4に何も設定されていない場合、クライアントが要求しているQoSレベルはQoS0です。

QoS 1 

QoS 1は最低1回の配信を設定します。配信を確認するPUBACKが付いています。

関数定義パターンは同じです。

void CPktConnect::SetWillQoS_1(const bool willQoS_1)
  {
   willQoS_1 ? m_connect_flags |= WILL_QOS_1 : m_connect_flags &= ~WILL_QOS_1;
   ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags);
  }

関数呼び出しパターンも同じです。

//--- Act
   CPktConnect *cut = new CPktConnect(buf);
   cut.SetWillQoS_1(true);

ビット 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


予約


X X X X 1 X X 0

表03:WillQoS 1 (bit_3)ビットフラグをtrueに設定 - MQTTv5.0

QoS 2 

QoS 2は一度だけ配信するように設定されます。このQoSでは、損失や重複がないことが要求されます。送信者はPUBRECでメッセージを確認し、PUBRELで配信を確認します。

このレベルは、書留小包を送るようなものだと考えることができます。郵便システムは、小包を相手の手に渡すと、当面は正しい住所に届ける責任があることを示す受領書を渡します。そして、小包が配達されると、受取人から小包の配達を認める署名入りの領収書が送られてきます。

関数定義パターンは同じです。

void CPktConnect::SetWillQoS_2(const bool willQoS_2)
  {
   willQoS_2 ? m_connect_flags |= WILL_QOS_2 : m_connect_flags &= ~WILL_QOS_2;
   ArrayFill(ByteArray, ArraySize(ByteArray) - 1, 1, m_connect_flags);
  }

関数呼び出しパターンも同じです。

//--- Act
   CPktConnect *cut = new CPktConnect(buf);
   cut.SetWillQoS_2(true);
ビット 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


予約


X X X 1 X X X 0

表04:WillQoS 2 (bit_4)ビットフラグをtrueに設定 - MQTTv5.0

サーバーは、CONNACKReasonCodesとCONNACKPropertiesで、受け入れられている最大QoSレベルを教えてくれます。クライアントは要求できますが、サーバーの能力は必須です。最大QoSのCONNACKを受信した場合、このサーバーの制限に従わなければならず、それ以上のQoSのPUBLISHを送信してはなりません。そうでない場合、サーバーはDISCONNECTします。

QoS 2は、MQTTv5.0で利用可能な最も高いQoSレベルであり、配信プロトコルが対称的であるため、これに関連するかなりのオーバーヘッドがあります。つまり、 この件に関してはいずれかの側(サーバーとクライアント)が送信者または受信者の両方として機能できることになります。

:QoSは、ユーザーの視点に立ったプロトコルの核心であると言えます。アプリケーションプロファイルを定義し、プロトコルの他の多くの側面に影響を与えます。そこで、PUBLISHパケットの実装の文脈で、QoSレベルとその設定について深く掘り下げてみます。

注目すべきは、QoS 1とQoS 2はクライアント実装のオプションであることです。OASISは非規範的なコメントで次を述べています。

「クライアントはQoS 1またはQoS 2のPUBLISHパケットをサポートする必要はありません。この場合、クライアントは、それが送るすべてのSUBSCRIBEコマンドの最大QoSフィールドを、サポートできる値に制限するだけです。


Will RETAIN (bit_5)

6バイト目には、Will Retainフラグを設定します。このフラグは上記のWill Flagとタイアップしています。 

  • もしWill Flagが設定されていなければ、Will Retainも設定されていなければなりません。
  • もしWill Flagが設定され、Will Retainが設定されていない場合、サーバーはWill Messageを非保持メッセージとして発行します。
  • 両方が設定されている場合、サーバーはWill Messageを保持されたメッセージとして公開します。

関数定義と関数呼び出しパターンは、前のフラグと厳密に同じであるため、このフラグと次の2つのフラグでは、簡潔にするためにコードを省略しています。詳細とテストについては、添付ファイルをご覧ください。

ビット 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


予約


X X 1 X X X X 0

表05:Will Retain (bit_5)ビットフラグをtrueに設定 - MQTTv5.0

PUBLISHパケットの送信を開始する前に、CONNACKパケットがこのフラグを確認するのを待たなければなりません。サーバーがWill Retainを1に設定したPUBLISHパケットを受信し、それが保持されたメッセージをサポートしていない場合、サーバーはDISCONNECTします。疑問に思されているかもしれません。CONNACKパケットを受信する前でもパブリッシュを開始することは可能でしょうか。はい、可能です。OASIS規格はこの動作を認めていますが、次も言っています。

「CONNACKを受信する前にMQTT Control Packetを送信するクライアントは、サーバーの制約に気づかない」

したがって、Will Retainを1に設定したPUBLISHパケットを送信する前に、CONNACKパケットでこのフラグを確認しなければなりません。 


Password flag (bit_6)

7バイト目では、PayloadにPasswordを送るかどうかをサーバーに通知します。このフラグが設定されている場合、Payloadにパスワードフィールドが存在しなければなりません。未設定の場合、パスワードフィールドはPayloadに存在してはなりません。

「このバージョンのプロトコルでは、MQTT v3.1.1にはなかった、User NameなしのPasswordの送信が可能です。これは、パスワード以外の資格情報に対するPasswordの一般的な使用を反映しています(OASIS規格、3.1.2.9)。」

ビット 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


予約


X 1 X X X X X 0

表06:Password Flag (bit_6)ビットフラグをtrueに設定 - MQTTv5.0


User Name Flag (bit_7)

そして最後に、8ビットで、PayloadにUser Nameを送るかどうかをサーバーに通知します。上記のPassword Flagと同様に、このフラグが設定されている場合、User NameフィールドがPayloadに存在しなければなりません。そうでない場合、User NameフィールドはPayloadに存在してはなりません。

ビット 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


予約


1 X X X X X X 0

表07:User Name (bit_7)ビットフラグをtrueに設定 - MQTTv5.0

つまり、Connect Flagsバイトは次のようなビット列になります。

ビット 7 6 5 4 3 2 1 0

User Name Flag
Password Flag
Will Retain
Will  QoS 2
Will QoS 1

Will Flag

Clean Start


予約


X X 1 1 X 1 1 0

表08:Clean Start、Will Flag、Will QoS 2、Will Retaiに設定されたビットフラグ - MQTTv5.0

...というように訳すことができます。QoSレベル2で新しいセッションの接続を開き、私のWill Messageを保存し、それを保持されたメッセージとして公開する準備をします。ところでサーバーさん、ユーザー名とパスワードによる認証は必要ありません。

サーバーは私たちの意志を満たすことができれば、親切に答えてくれます。それを完全に満たすことも、部分的に満たすことも、まったく満たさないこともあります。そして、サーバーはCONNACKパケットにConnect Reason Codesという形でその答えを送ります。


(CONNACK) Connect Flagsに関連するReason Codesの取得

MQTT v5.0には44のReason Codesがあります。それらをDefines.mqhヘッダーに集めました。CONNACK(および他のパケットタイプ)は、変数ヘッダーの一部として単一の理由コードを持ちます。これらはConnect Reason Codesと名付けられます。

16進数 Reason Code名 詳細
0 0x00 Success Connectionが受け入れられた
128 0x80 Unspecified error サーバーが失敗の理由を明らかにしたくないか、他のReasonCodesのどれにも当てはまらない
129 0x81 Malformed Packet CONNECTパケット内のデータが正しく解析されなかった 
130 0x82 Protocol Error CONNECTパケットのデータがこの仕様に準拠していない
131 0x83 実装固有のエラー The CONNECT is valid but is not accepted by this Server.
132 0x84 Unsupported Protocol Version サーバーが、クライアントが要求したMQTTプロトコルのバージョンをサポートしていない
133 0x85 Client Identifier not valid クライアント識別子は有効な文字列であるが、サーバーが許可していない
134 0x86 Bad User Name or Password クライアントが指定したUser NameまたはPasswordがサーバーに受け入れられない
135 0x87 Not authorized クライアントは接続を許可されていない
136 0x88 Server unavailable MQTTサーバーは使用不可
137 0x89 Server busy サーバーがビジー状態です。後ほどもう一度お試しください。
138 0x8A Banned このクライアントは行政処分により禁止されています。サーバー管理者に連絡してください。
140 0x8C Bad authentication method 認証方法がサポートされていないか、現在使用されている認証方法と一致しない
144 0x90 Topic Name invalid Will Topic Nameは不正ではないが、このサーバーでは受け入れられない
149 0x95 Packet too large CONNECTパケットが許容最大サイズを超えている
151 0x97 Quota exceeded 実施上または管理上の制限を超えている
153 0x99 Payload format invalid Will Payloadが指定されたPayload Format Indicatorと一致しない
154 0x9A Retain not supported サーバーは保持メッセージをサポートしておらず、Will Retainが1に設定されている
155 0x9B QoS not supported サーバーはWill QoSで設定されたQoSをサポートしていない
156 0x9C Use another server クライアントは一時的に別のサーバーを使用する必要がある
157 0x9D Server moved クライアントは恒久的に別のサーバーを使用する必要がある
159 0x9F Connection rate exceeded 接続レートの制限を超えている

表08:Connect Reason Codeの値

規格では、サーバーがCONNACKでConnect Reason Codesを送信する必要があることを明示されています。

「CONNACKパケットを送信するサーバーは、Connect Reason Code値のいずれかを必ず使用しなければならない[MQTT-3.2.2-8]」

Connect Reason Codesは、この時点で私たちにとって特別な関心事です。通信を進める前に確認する必要があるからです。利用可能なQoSレベルや保持されるメッセージの可用性など、サーバーの機能と制限についての情報が提供されます。また、上の表の名前と説明を見ればわかるように、CONNECTの試みが成功したかどうかを知らせてくれます。

Reason Codesを取得するには、まず、パケットの種類を特定する必要があります。なぜなら、CONNACK パケットのみに興味があるからです。

パケットタイプを取得するために非常に単純な関数が必要であるという事実を利用して、テスト駆動開発をどのように使っているかを説明し、このテクニックについて少し推論し、いくつかの短い例を提供します。詳細は添付ファイルをご覧ください。


(CONNACK)サーバーパケットタイプの識別

MQTT Control Packetの最初のバイトが、パケットのタイプをエンコードしていることは確かです。そこで、この最初のバイトをできるだけ早く読み込んで、サーバーパケットのタイプを取得します。

uchar pkt_type = server_response_buffer[0];

ここで止まり、完了したので、次の問題に移るべきだとお考えでしょうか。

まあ、確かにそうです。コードは明快で、変数の名前も適切で、パフォーマンスもよく、軽量です。

でも、待ってください。私たちのライブラリを使用するコードは、このステートメントをどのように呼び出すのでしょうか。パケットタイプはpublic関数の呼び出しによって返されるのでしょうか。それとも、この情報を実装の詳細としてprivateメンバーの後ろに隠すことができるのでしょうか。関数呼び出しによって返される場合、その関数はどこでホストされるべきでしょうか。CPktConnectクラス、それとも、さまざまなクラスで使用されるため、ヘッダーファイルのどれかにホストされるべきでしょうか。privateメンバーに格納されている場合、どのクラスに格納されるべきでしょうか。

(TMTOWTDI*)「There is more than one way to do it」(やり方はひとつではありません)は、とてもポピュラーになった頭字語です。TDDもまた、さまざまな理由から非常によく使われるようになった頭字語です。かつてそうであったように、どちらの略語も乱用され、誇張され、TDDのように「ファッション」にさえなったものもあります。

「母さん、TDDしてるよ。大丈夫だよ。」

しかし、これらを考案したグループは、開発者の生産性を向上させながら、よりパフォーマンスが高く、慣用的で堅牢なコードを作成するにはどうすればよいかという同じ基本的な質問に何年も取り組んだ末に、このようなコードを作成しました。どうすれば開発者を、できるかもしれないことにについてうろうろするのではなく、やらなければならないことに集中させることができるのでしょうか。一人一人が一度に1つの仕事、しかもたった1つの仕事に集中し続けるにはどうすればいいのでしょうか。彼らがやっていることが、リグレッションバグを引き起こし、システムを壊してしまわないことをどうやって確認するのでしょうか。 

一言で言えば、これらの頭字語、頭字語に込められた考え方、頭字語に推奨されるテクニックは、ソフトウェア開発の専門知識を持つ何百人という全く異なる個人の長年の実践を集約したものです。TDDは理論ではなく、実践です。TDDは、問題を構成要素に分解してスコープを閉じることで、問題を解決する手法だと言えます。私たちは、一歩先に進むためのたったひとつの仕事を明確にしなければなりません。たった一歩です。しばしば、極小さな一歩です。

では、何が問題なのでしょうか。サーバーの応答がCONNACKパケットかどうかを識別する必要があります。それだけです。なぜなら、仕様によれば、次に何をすべきかを決めるためにCONNACK応答コードを読む必要があるからです。つまり、サーバーから応答として受け取ったパケットのタイプを特定することが、接続状態から公開状態に進むために必要なのです。

サーバーの応答がCONNACKパケットであるかどうかを識別するにはどうすればよいか?まあ、簡単なことです。これは、MQTT.mqhヘッダーでEnumerationとしてコード化された特定のタイプ、すなわちENUM_PKT_TYPEを持っています。

//+------------------------------------------------------------------+
//|              MQTT - CONTROL PACKET - TYPES                       |
//+------------------------------------------------------------------+
/*
Position: byte 1, bits 7-4.
Represented as a 4-bit unsigned value, the values are shown below.
*/
enum ENUM_PKT_TYPE
  {
   CONNECT     =  0x01, // Connection request
   CONNACK     =  0x02, // Connection Acknowledgment
   PUBLISH     =  0x03, // Publish message
...

そこで、MQTTブローカーから送られてくるネットワークバイト配列を渡すと、パケットのタイプを返す関数から始めることができるかもしれません。

それはいいと思います。この関数のテストを書いてみましょう。

bool TEST_GetPktType_FAIL()
  {
   Print(__FUNCTION__);
//--- Arrange
   uchar expected[] = {(uchar)CONNACK};
   uchar result[1] = {};
   uchar wrong_first_byte[] = {'X'};
//--- Act
   CSrvResponse *cut = new CSrvResponse();
   ENUM_PKT_TYPE pkt_type = cut.GetPktType(wrong_first_byte);
   ArrayFill(result,0,1,(uchar)pkt_type);
//--- Assert
   bool isTrue = AssertNotEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }

すべてのテストでこのパターンを使うことになるので、テスト関数を理解しましょう。

コメント行では、パターンの各手順を説明してます。 

Arrange

まず、関数が返すと予想されるバイト値を1つの要素とする配列を初期化します。

次に、関数呼び出しの結果を受け取るために、空のcharバッファをサイズ1で初期化します。

テストする関数に渡すフィクスチャーを初期化します。これは、パケットタイプを識別するために、サーバー応答固定ヘッダーから読み取られる最初の1バイトを表します。この場合、wrong_first_byte があるので、変数名をそれに合わせて明示します。

Act

テスト対象のクラス(Class Under Test 、cut)をインスタンス化し、関数を呼び出します。

Assert

MQL5のArrayCompare関数を使用して、期待される配列と結果の配列の内容およびサイズが不等であることを確認します(添付のテストファイルを参照)。

Clean-Up

最後に、cutインスタンスを削除し、resultバッファをZeroMemoryに渡すことで、リソースをクリーンアップします。これにより、メモリーリークやテストの汚染を避けることができます。

図04:TEST_CSrvResponse-FAIL-宣言されていない識別子

図04:TEST_CSrvResponse-FAIL-宣言されていない識別子

関数がまだ存在しないため、コンパイル時に失敗します。書く必要があります。しかし、どこにホストするべきなのでしょうか。

応答パケットのタイプを常に特定する必要があることは、すでに分かっています。パケットをブローカーに送ると、ブローカーは「応答のもの」を送ってきます。そしてこの「もの」は、MQTT Control Packetの一種です。ある「もの」の一種である以上、その類似した「もの」のグループの下に独自のクラスを持つのは自然なことだと思われます。コントロールパケットグループの下に、すべてのサーバー応答を表すクラスがあるとしましょう。

例えば、IControlPacketインターフェイスを実装したCSrvResponseクラスがあるとします。

すでに存在するCPktConnectクラスの別の関数にしたくなるかもしれません。しかし、オブジェクト指向プログラミングの重要な原則である単一責任の原則(SRP)に違反することになります。

「異なる理由で変化するものは分け、同じ理由で変化するものはまとめるべきです。」(R. Martin、The Clean Coder, 2011)。

一方では、CONNECTパケットの構築方法を変更するたびにCPktConnectクラスが変更され、もう一方では、CONNACK、PUBACK、SUBACK、その他のサーバー応答の読み取り方法を変更するたびに(存在しない)CSrvResponseクラスが変更されます。両者には明確な責任の違いがあります。この場合、それを確認するのは非常に簡単です。しかし、モデル化している始域の実体を適切なクラスで宣言すべきかどうか、判断に迷うことがあります。SRPを適用することで、これらの「物事」を判断するための客観的な指針を得ることができます。

では、テストに合格する程度に書いてみましょう。

ENUM_PKT_TYPE CSrvResponse::GetPktType(uchar &resp_buf[])
  {
   return (ENUM_PKT_TYPE)resp_buf[0];
  }

テストはコンパイルされますが、予想通り失敗します。失敗させるために「間違った」サーバー応答を渡したからです。

図05:TEST_CSrvResponse-FAIL-間違ったパケット

図05:TEST_CSrvResponse-FAIL-誤ったパケット

「正しい」CONNACKパケットタイプをサーバーの応答として渡しましょう。ここでも、フィクスチャにright_first_byteという名前をつけていることにご注意ください。名前そのものはラベルにすぎません。重要なのは、コードを読む人にとってその意味が明確であることです。半年後、6年後の自分たちも含めてです。

bool TEST_GetPktType()
  {
   Print(__FUNCTION__);
//--- Arrange
   uchar expected[] = {(uchar)CONNACK};
   uchar result[1] = {};
   uchar right_first_byte[] = {2};
//--- Act
   CSrvResponse *cut = new CSrvResponse();
   ENUM_PKT_TYPE pkt_type = cut.GetPktType(right_first_byte);
   ArrayFill(result,0,1,(uchar)pkt_type);
//--- Assert
   bool isTrue = AssertEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }

図06:TEST_CSrvResponse-成功

図06:TEST_CSrvResponse-成功

さて、これでテストは合格となり、少なくともこの2つの引数(1つは間違い、もう1つは正解)については、それぞれ失敗と成功であることがわかりました。必要であれば、後でもっと広範囲にテストすることもあります。

これらのシンプルな手順には、R. Martinが要約したTDDの3つの基本的な「法則」が組み込まれています。

  1. まず失敗する単体テストを書くまでは、本番用のコードを書くことは許されません。
  2. 失敗するのに十分な以上の単体テストを書くことは許されないし、コンパイルしないことは失敗することです。
  3. 現在不合格になっている単体テストに合格するのに十分な量以上のプロダクションコードを書くことは許されません。

さて、TDDについてはこれで終わりです。手元の作業に戻り、サーバーから到着したCONNACKパケットのConnect Reason Codesを読んでみましょう。


(Connect Reason Codes)サーバーで利用できない機能をどうするか?

Connect Reason Codesのうち、この時点で注目すべきものが2つあります。

  1. QoS not supported
  2. Retain not supported

これらはエラーを示すのではなく、サーバーの制限を示すため、やや特殊です。CONNACKにこれらのConnect Reason Codesのいずれかがある場合、サーバーは、ネットワーク接続が成功し、CONNECTが整形式パケットであり、サーバーがオンラインで動作しているが、こちらの要求を満たすことができない、と言っています。私たちは行動を起こす必要があります。次に何をすべきかを選択する必要があります。

WillQoS 2でCONNECTを送信し、サーバーがQoSMaximum1で返信してきた場合、どうすればよいでしょうか。QoSフラグをダウングレードしてCONNECTを再送すべきか、それとも、ダウングレードする前にDISCONNECTするべきか。もしRETAIN機能がそうだとしたら、関係ないものとして無視して出版を始めてもいいのでしょうか。それとも、公開前にフラグをダウングレードしてCONNECTを再送すべきでしょうか。

CONNACKが成功した、つまりサーバーが接続を受け入れ、こちらが要求したすべての機能を持っていることを受け取った後、どうすればいいのでしょうか。すぐにPUBLISHパケットの送信を開始しなければならないのでしょうか。それとも、メッセージを発表する準備が整うまで、PINGREパケットを連続して送信することで、接続をオープンにしておくこともできるのでしょうか。ところで、トピックを公開する前に、そのトピックに登録しなければならないのでしょうか。

これらの質問のほとんどは、規格が答えてくれます。MQTTv5.0準拠のクライアントを持つためには、現状で実装する必要があります。多くの選択肢がアプリケーション開発者に委ねられています。今のところ、私たちは必要なことだけを扱い、できるだけ早く適合したクライアントを持てるようにします。

規格によると、クライアントはWill Flagも1に設定されている場合のみ、QoSレベル>0を要求できます。つまり、CONNECTパケットでWill Messageも送信している場合のみ、QoSレベル>0を要求することが許可されます。しかし、私たちは今すぐWill Messageを扱いたいとは思わないし、むしろ扱う必要はありません。つまり、私たちの決断は、今必要なことだけを理解することと、規格の複雑な部分をすべて理解しようとすることの折衷案であり、結局は後で役に立たないかもしれないコードを書くことになります。 

要求されたQoSレベルやRetainがサーバーで利用できない場合、クライアントがどうするのかだけ知っていれば大丈夫です。そして、新しいCONNACKが到着したらすぐにそれを知る必要があります。そこで、CSrvResponseのコンストラクタで確認しています。応答がCONNACKの場合、コンストラクタはprotected GetConnectReasonCodeメソッドを呼び出します。

CSrvResponse::CSrvResponse(uchar &resp_buf[])
  {
   if(GetPktType(resp_buf) == CONNACK
      && GetConnectReasonCode(resp_buf)
      == (MQTT_REASON_CODE_QOS_NOT_SUPPORTED || MQTT_REASON_CODE_RETAIN_NOT_SUPPORTED))
     {
      CSrvProfile *serverProfile = new CSrvProfile();
      serverProfile.Update("000.000.00.00", resp_buf);
     }
  }

Connect Reason CodeがMQTT_REASON_CODE_QOS_NOT_SUPPORTEDまたはMQTT_REASON_CODE_RETAIN_NOT_SUPPORTEDのいずれかである場合、この情報をServerProfileに保存します。今のところ、サーバーに関するこの情報だけを保存し、DISCONNECTを待つことにします。後で、この同じサーバーに新しい接続を要求するときに、この値を使うことになります。この「後で」というのは、最初の接続試行から数ミリ秒後かもしれないが、数週間遅れるかもしれないことにご注意ください。大事なのは、この情報をサーバープロファイルに保存させるということです。


protectedメソッドのテスト方法

protectedメソッドをテストするために、テストスクリプトにクラスを作成しました。このクラスは、テスト対象のクラス(この場合はCSrvResponse)から派生したクラスです。次に、TestProtectedMethodsと名付けた「テスト目的専用」の派生クラスを通して、CSrvResponseのプロテクトメソッドを呼び出します。

class TestProtectedMethods: public CSrvResponse
  {
public:
                     TestProtectedMethods() {};
                    ~TestProtectedMethods() {};
   bool              TEST_GetConnectReasonCode_FAIL();
   bool              TEST_GetConnectReasonCode();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestProtectedMethods::TEST_GetConnectReasonCode_FAIL()
  {
   Print(__FUNCTION__);
//--- Arrange
   uchar expected = MQTT_REASON_CODE_SUCCESS;
   uchar reason_code_banned[4];
   reason_code_banned[0] = B'00100000'; // packet type
   reason_code_banned[1] = 2; // remaining length
   reason_code_banned[2] = 0; // connect acknowledge flags
   reason_code_banned[3] = MQTT_REASON_CODE_BANNED;
//--- Act
   CSrvResponse *cut = new CSrvResponse();
   uchar result = this.GetConnectReasonCode(reason_code_banned);
//--- Assert
   bool isTrue = AssertNotEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool TestProtectedMethods::TEST_GetConnectReasonCode()
  {
   Print(__FUNCTION__);
//--- Arrange
   uchar expected = MQTT_REASON_CODE_SUCCESS;
   uchar reason_code_success[4];
   reason_code_success[0] = B'00100000'; // packet type
   reason_code_success[1] = 2; // remaining length
   reason_code_success[2] = 0; // connect acknowledge flags
   reason_code_success[3] = MQTT_REASON_CODE_SUCCESS;
//--- Act
   CSrvResponse *cut = new CSrvResponse();
   uchar result = this.GetConnectReasonCode(reason_code_success);
//--- Assert
   bool isTrue = AssertEqual(expected, result);
//--- cleanup
   delete cut;
   ZeroMemory(result);
   return  isTrue ? true : false;
  }

サーバープロファイルには何も保存していないことに注意してください。実際、サーバープロファイルはまだ存在しません。サーバープロファイルを更新中であるというメッセージを表示しているだけです。サーバープロファイルがクライアントのセッション間で永続化されるためであり、私たちはまだ永続化を扱っていないからです。後で永続化を実装するときに、このスタブ関数を変更して、例えば、印刷された(またはログに記録された)メッセージを削除することなく、SQLiteデータベースにサーバープロファイルを永続化することができます。ただ、今は実施されていないだけです。上述したように、この時点では、サーバーが要求した能力と一致しない場合にどうすればよいかを知るだけで大丈夫です。


結論

この記事では、MQTTv5.0プロトコルのOperational Behavior(操作時の動作)セクションについて、OASIS規格が要求しているコンフォーマントクライアントをできるだけ早く動作させるために、どのように対処し始めているかを説明しました。CSrvResponseクラスを実装して、サーバー応答タイプとそれに関連するReason Codesを識別する方法を説明しました。また、利用できないサーバー能力に対してクライアントがどのように反応するかも説明しました。

次のステップでは、PUBLISHを実装し、QoSレベルの動作の理解を深め、セッションとその永続性を扱います。

**その他の便利な略語:DRY, KISS, YAGNI。それぞれに実用的な知恵が盛り込まれいます。ただし、YMMVです(効果は個人によって異なります)。 

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/13388

添付されたファイル |
headers.zip (7.16 KB)
tests.zip (3 KB)
MQL5における代替リスクリターン指標 MQL5における代替リスクリターン指標
本稿では、シャープレシオの代替指標とされるいくつかのリスクリターン指標の実装を紹介し、その特徴を分析するために仮想資本曲線を検証します。
エキスパートアドバイザー(EA)に指標を追加するための既製のテンプレート(第1部):オシレーター エキスパートアドバイザー(EA)に指標を追加するための既製のテンプレート(第1部):オシレーター
この記事では、オシレーターカテゴリから標準的な指標を検討します。パラメータの宣言と設定、指標の初期化と初期化解除、EAの指標バッファからのデータとシグナルの受信など、EAですぐに使用できるテンプレートを作成します。
MQL5の圏論(第23回):二重指数移動平均の別の見方 MQL5の圏論(第23回):二重指数移動平均の別の見方
この記事では、前回に引き続き、日常的な取引指標を「新しい」視点で見ていくことをテーマとします。今回は、自然変換の水平合成を取り扱いますが、これに最適な指標は、今回取り上げた内容を拡大したもので、二重指数移動平均(DEMA)です。
パターン検索への総当たり攻撃アプローチ(第VI部):循環最適化 パターン検索への総当たり攻撃アプローチ(第VI部):循環最適化
この記事では、MetaTrader 4および5の取引の自動化チェーン全体を完成するだけでなく、より興味深いことができるようになった改善の最初の部分を示します。今後、このソリューションにより、EAの作成と最適化の両方を完全に自動化し、効果的な取引構成を見つけるための人件費を最小限に抑えることができます。