組み込みデザインパターン

状態が一つ増えるたびにswitch文が伸び、どこを直せば安全なのか誰も言い切れなくなります。議論がこの段階に入ると、話は個人の書き方の好みへ流れがちです。そこへ名前の付いたパターン(型)を持ち込むことで、議論の焦点を設計の目的に戻せます。組み込みで実際に使われる定番パターンを、使いどころと使わないほうがよい場所の両面から確認します。

組み込みでデザインパターンを使う意義

デザインパターンは新しい機能を生むものではありません。すでに誰かが解いた設計課題に、共有できる名前を与える枠組みです。組み込みで使う意義は、名前が付くとレビューでの指摘も設計判断の記録も短く済む点にあります。

定型的な設計課題を名前付きの解として共有できる

「ここはStateパターンで書き直そう」の一言で意図が伝わります。この短さがパターンの実利です。1994年に刊行された設計パターンのカタログは、設計者が解を自分で再発見しなくても、より柔軟で再利用しやすい設計を作れるようにすることを目的としています。名前付きの解を共有できると、レビューの議論は書き方の好みから設計判断へ移ります。

定番のパターン集は組み込みでも応用できるものが多い

同書は23のパターンを収め、生成、構造、振る舞いの三つに分類しました。このうち、振る舞いに属するState、Observer、Command、Strategyや、生成に属するFactory Method、Singletonは、組み込みのC言語やC++でも有効です。関数ポインタを用いてインタフェースと実装を分離すれば、C言語でも同様の設計を表現できます。多くのOS実装では、ファイル操作を関数ポインタ群(操作テーブル)として抽象化する設計が見られます。

State・Command・Observerを組み込みで扱う

組み込みで扱う3つのデザインパターンの図。Stateは状態を型にして遷移を明示する。待機、受信中、解析中の遷移を例に、分岐はswitch文ではなく状態そのものが持つことを示す。Commandは操作を1件ずつの依頼にして積み、まとめて実行する。積む、取り消す、やり直すを同じ形で扱える。Observerは1つの変化を表示、記録、送信の複数へ配る。通知元は宛先を1件ずつ知らなくてよい。

組み込みでは、状態を扱うState、操作を扱うCommand、通知を扱うObserverといったパターンが活用できます。担当する関心事が重ならないので、同じ製品に併存しても衝突しません。

Stateパターンで状態遷移をswitch文から解放する

巨大なswitch文の問題は、行数ではなく変更時の読み替えコストにあります。状態を一つ増やすと、すべての分岐を見直すことになるからです。Stateパターンでは、状態ごとの処理を分離することにより、状態追加時の変更範囲を局所化しやすくなります。状態数が少なく遷移も単純なうちはswitch文でも十分に対応できますが、増える見込みが立った時点が切り換えどきです。目安としては、状態追加が四半期に一度以上発生している、状態ごとの処理が20行を超え始めた、遷移条件が複数入力に依存し始めた、のいずれかに当てはまれば切り換えを検討します。

Commandパターンで操作をオブジェクト化してキュー化する

Commandパターンは、実行したい操作をオブジェクトとしてカプセル化する発想です。操作をオブジェクト化して構造体へ包むと、キューへ積む、後で実行するといった扱いが自然にできます。リアルタイムOSのタスク間通信とは相性が良く、送る側と実行する側を時間的に切り離せます。

Observerパターンで1対多の通知を疎結合に実装する

センサ値が変わったとき、表示にもログにも通信にも伝えたいと考えるはずです。呼び出す側が宛先を全部知っている実装では、購読者が増えるたびに送信側を直すことになります。Observerパターンは購読者のリストを関数ポインタで持ち、送信側から宛先の知識を取り上げて1対多の通知を疎結合にします。C言語では、コールバック関数の配列などで管理する形で実装できます。

Publish-Subscribeとイベント駆動アーキテクチャ

Observerパターンをシステム全体へ拡張し、ブローカー(イベントバス)を介した形にすると、Publish-Subscribe型のイベント配信として構成できます。Observerは同一プロセス内でのコールバックやリスナ登録など比較的近距離な依存を前提とし、Subjectが購読者リストを保持します。Publish-Subscribeはブローカーを介するため、発行者は購読者を保持せず、疎結合で分散構成にも拡張できます。この点が両者の主な違いです。ここへリアルタイムOSのキューと優先度制御を組み合わせると、組み込みで実用になるイベント駆動アーキテクチャを構成できます。

センサ値・状態変化を購読者に配信する

イベントバスへ投げてしまえば、購読者はそれぞれ独立に受け取れます。ログ機能や外部通信を購読者として構成すると、機能の増減で送信側を触らずに済むわけです。標準化された車載向けの組み込みソフトウェアでも、機能を複数の層に分けて責務を分離する設計が採られています。ただし配信の処理コストは無視できません。Publish-Subscribeでは、一つのイベントを複数の購読者へ配信する必要があるため、購読者が増えるほど配信処理の負荷も増えます。周期の厳しい処理では、すべての購読者へ配信するのではなく、配信対象を絞る判断が必要です。

Active Objectパターンとイベント駆動

Active Objectパターンは、各オブジェクトが専用のイベントキューと実行文脈を持つ設計です。Active Objectパターンに関する技術文献では、非同期でイベント駆動かつノンブロッキングな計算モデルとして説明されることが一般的です。また、この計算モデルを決定的かつリアルタイムに実行する環境を提供するとも述べられています。さらに、イベントは受け取ったら最後まで処理しきるRun-to-Completionの規約に従うのが特徴です。イベント処理を1件ずつ完了まで進めるため、Active Object自身が管理する状態への直接的な競合を減らしやすくなります。

Factory MethodとSingletonの使いどころ

生成に関わるパターンは、組み込みでも役に立ちます。ただし乱用すると、起動時の初期化順序という別の悩みを呼び込むため、使いどころを絞る前提で確認します。

Factory Methodでハードウェア依存の生成を隔離する

センサドライバの生成をFactory Methodの内側へ隠すと、上位層はどのハードウェアでも同じインタフェースで扱えます。分岐が要るのはFactory Methodの中だけになり、条件分岐が製品コード全体へ散らばりません。ハードウェア依存の生成をどこへ隔離するかという問題として捉えると、判断しやすくなります。

Singletonは本当に一つのインスタンスが必要な場合に使う

ロガーやシステム時刻の管理のように、システムとして一つのインスタンスだけを共有する必要がある場合に限って使うのがSingletonの本来の用法です。便利さに引かれて広げると、テストで差し替えにくい依存が増えるなど、グローバルな共有状態と同様の問題を招きます。実質的にグローバル変数と変わらない使い方になっていないか、一度確認することを推奨します。

過剰設計を避ける判断軸

パターンは道具であって目的ではありません。当てはめること自体が目的になると、読みにくさと実行時コストだけが残ります。判断軸は「変更の見込み」と「実行時コスト」の二つです。

本当に将来変更が来る箇所だけにパターンを当てる

変更点を予測して先に抽象化しておく進め方は、予測が外れたときの損が大きいものです。将来の変更を見込んで抽象化する場合は、その変更シナリオが現実的かどうかを評価することが重要です。ソフトウェア設計の一般的な手順でも、まず変更シナリオを洗い出し、そのシナリオに照らして構造の有効性を評価します。想定する変更シナリオが具体的で、その変更に対して構造の面でメリットが確認できる箇所にパターンを適用すると、過剰な抽象化を抑えられます。

抽象化のコストとリアルタイム性を天秤にかける

関数ポインタ経由の呼び出しには、抽象化のオーバーヘッドが発生します。C++の性能を扱ったISO/IECの技術報告書(ISO/IEC TR 18015:2006)は、仮想関数呼び出しの一般的な代替が間接関数呼び出しであると述べ、複数の処理系で実測しています。その結果、両者の差は処理系間のばらつきの範囲内に収まるケースもあることが示されています。リアルタイム性が厳しい経路では直接呼び出しを残し、変更の起きる境界にだけ抽象を置く線引きが現実的です。冒頭の伸び続けるswitch文に話を戻すと、問うべきは、その状態がこれからいくつ増えるかという一点です。

組み込みソフトの世界 トップへ戻る