[2006 年 5 月号] |
[技術講座]
中編では,分析モデルの作成までをご紹介しました.後編では,設計モデルの作成から,実装までをご紹介します.できるだけ多くのコードを取り上げながら,最終的にモデルがどのようにコードに落とし込まれるのかを中心に説明します.
2.1.オブジェクト構造設計・実装
オブジェクト構造設計では,オブジェクト構造分析で作成したモデルを元に,設計モデルを作成します.設計モデルとは,アーキテクチャメカニズムや実装する言語を意識したモデルです.それ以外の基本的なクラスの構造は,オブジェクト構造分析後のモデルと変わりません. オブジェクト構造設計前後を比較するために,学習リモコンドメインのオブジェクト構造分析モデルを図2.1に,オブジェクト構造設計モデルを図2.2に示します.
※クリックすると拡大します.
※クリックすると拡大します.本設計モデルは,分析モデルとは,主に以下の点で異なります.
- クラス名や操作名がプログラミング言語(今回はC++)を考慮した名称になっている.
- 利用するインターフェースへの依存関係が,関連になり,多重度とロール名が記述されている.
- コンストラクタが追加されている.
- クラス間の関係が,プログラミング言語を考慮した形になっている(インターフェースの実現が,抽象クラスからの継承になるなど).
- オブジェクト間に関連を設定するための操作が追加されている.
5に関しては,2.3. システム開始処理設計・実装で詳細に説明します.
では,これらのクラスがどのようにソースコードとして実装されるのか,見ていきましょう.基本的に,1つのクラスを1つのヘッダファイルと1つのソースファイルに実装していくことにします.
まず,学習リモコンドメインで実装する抽象クラス,I_LearningRemocon(ソースコード2.1)とI_RemoconDataReceive(ソースコード2.2)を見てみましょう.それぞれのクラスの操作を,純粋仮想のメンバ関数として宣言しています.
ソースコード2.1.I_LearningRemocon.h#if !defined(I_LEARNING_REMOCON_H__) #define I_LEARNING_REMOCON_H__ #inclult;Common/BasicTypes.h> namespace Remocon { class I_LearningRemocon { public: virtual Common::ER remoconDataRegister(Common::ID id) = 0; virtual void remoconDataRegisterCancel() = 0; virtual void remoconDataSend(Common::ID id) = 0; }; } // End namespace #endif // I_LEARNING_REMOCON_H__
ソースコード2.2.I_RemoconDataReceive.h#if !defined(I_REMOCON_DATA_RECEIVE_H__) #define I_REMOCON_DATA_RECEIVE_H__ #include <Common/BasicTypes.h> #include <Common/RemoconData.h> namespace Remocon { class I_RemoconDataReceive { public: virtual void onReceive(Common::ER er, const Common::RemoconData& data) = 0; }; } // End namespace #endif // I_REMOCON_DATA_RECEIVE_H__
これらの抽象クラスは,ディスパッチャクラスLearningRemoconDispatcher(ソースコード2.3)で実装されます.LearningRemoconDispatcherは,2つの抽象クラスI_LearningRemoconとI_RemoconDataReceiveを継承します.また,privateなメンバ変数として,関連するクラスへのポインタを持ちます.これらのポインタを介して,クラス間のメッセージのやりとりを実現します.ポインタを設定する方法は,2.3. システム開始処理設計・実装で説明します.
なお,privateなメンバ変数は,変数名の末尾にアンダースコア「_」を付加する命名ルールとしています.
ソースコード2.3.LearningRemoconDispatcher.h#if !defined(LEARNING_REMOCON_DISPATCHER_H__) #define LEARNING_REMOCON_DISPATCHER_H__ #include <Remocon/I_RemoconDataReceive.h> #include <Common/BasicTypes.h> #include <Remocon/I_LearningRemocon.h> namespace Remocon { class RemoconDataSend; class RemoconDataRegister; class LearningRemoconDispatcher:public I_LearningRemocon, public I_RemoconDataReceive { public: LearningRemoconDispatcher(); virtual Common::ER remoconDataRegister(Common::ID id); virtual void remoconDataRegisterCancel(); virtual void remoconDataSend(Common::ID id); virtual void onReceive(Common::ER er, const Common::RemoconData& data); private: friend class LearningRemoconFactory; void setRemoconDataRegister(RemoconDataRegister* pRegister); void setRemoconDataSend(RemoconDataSend* pSend); private: RemoconDataSend *pDataSend_; RemoconDataRegister *pDataRegister_; }; } // End namespace #endif // LEARNING_REMOCON_DISPATCHER_H__
2.2.オブジェクトコラボレーション設計・実装
オブジェクトコラボレーション設計では,オブジェクト構造設計で設計したクラスが,オブジェクトコラボレーション分析で分析したとおり,正しく協調動作するかをシーケンス図で確認します.今回の学習リモコンのように,オブジェクト構造分析やオブジェクトコラボレーション分析で,比較的詳細な分析を行った場合,オブジェクトコラボレーション設計で記述するシーケンス図は,操作名などが異なるだけで,オブジェクトコラボレーション分析の結果とほぼ同じになります.このような場合は,あえてオブジェクトコラボレーション設計で,シーケンス図を記述する必要は無いと思います.
オブジェクトコラボレーション設計で,シーケンス図を記述しておいた方がよいケースとしては,
- オブジェクト構造設計にアーキテクチャメカニズムを反映することによって,比較的大きな変更を行った場合.
- 実装まで行った後,最適化の必要性が判明し,オブジェクト構造設計モデルを最適化した場合.
などが挙げられます.
今回の学習リモコンにおいては,オブジェクトコラボレーション設計にてシーケンス図の記述が必要なケースはありませんでした.
参考のために,図2.3に,学習リモコンドメインの「機器制御情報を送信する」ドメインユースケースに対応するオブジェクトコラボレーション分析のシーケンス図を,図2.4に,オブジェクトコラボレーション設計のシーケンス図を示します.
※クリックすると拡大します.
※クリックすると拡大します.シーケンス図のメッセージ呼び出しは,呼び出される側のクラスのソースファイル上に実装します.図2.4のシーケンスに対応するソースファイルの抜粋を,ソースコード2.4に示します.関連先のオブジェクトを指すポインタを経由して,メンバ関数がシーケンス図通りに呼び出されていきます.
ソースコード2.4.機器制御情報を送信する流れLearningRemoconDispatcher.cpp void LearningRemoconDispatcher::remoconDataSend(Common::ID id) { pDataSend_->send(id); } RemoconDataSend.cpp void RemoconDataSend::send(Common::ID id) { Common::RemoconData data; Common::ER er; er = pI_MemoryManagement_->getData(id, data); if (er != Common::SUCCESS) { pI_TransactionComplete_->onTransactionComplete(er); return ; } pI_SignalTransmit_->send(data); }
2.3.システム開始処理設計・実装
この段階で,各ドメインのクラスの構造と,クラス間のコラボレーションまで設計・実装できました.次に,これまで設計・実装したクラスのインスタンスをどこに配置するのか?また,システムが開始するmain関数から,どうやってクラス間のコラボレーションをスタートさせるのか?といったことを考える必要があります.
まず,各ドメインのクラスを生成するために,ドメイン毎にファクトリークラスと呼ばれる,オブジェクトを生成する責務を持つクラスを作ります.図2.5のLearningRemoconFactoryおよびUserInterfaceFactoryがファクトリークラスです.各ドメインのファクトリークラスは,そのドメインのすべてのクラスを生成,所有します.他のドメインも同様にファクトリークラスを用意します.
各ドメインのドメインファクトリークラスのインスタンスは,全ドメインのファクトリークラスを管理する,システムファクトリークラスのインスタンスに所有されます.システムファクトリークラスのインスタンスは,システムで1つだけ適切な場所(main関数内部や,RTOSの初期起動タスクなど)に配置します.これで,今回設計したクラスのインスタンスすべての配置が決まりました.
※クリックすると拡大します.システムファクトリークラスSystemFactoryの定義をソースコード2.5に示します.SystemFactoryは,privateなメンバ変数として各ドメインのファクトリーのスマートポインタを持ちます.スマートポインタとは,指し示す対象への参照が無くなったときに,自動的に後始末処理を呼び出すことが可能なクラステンプレートです.スマートポインタとしては標準ライブラリのstd::auto_ptrや,boostライブラリのboost::scoped_ptr,参照カウント機能付きのboost::shared_ptrなどがよく利用されます.今回は,参照カウント機能が不要であるため,boost::scoped_ptrを利用しました.ソースコード2.5の前半部分で,クラスの宣言のみを行っており,定義ファイルをinlcudeしていません.実装においては,依存関係を極力減らし,メンテナンスしやすいコードになるように心がけました.
ソースコード2.5.SystemFactory.h#if !defined(SYSTEM_FACTORY_H__) #define SYSTEM_FACTORY_H__ #include <boost/scoped_ptr.hpp> namespace Ui { class UserInterfaceFactory; } namespace Memory { class MemoryManagementFactory; } namespace Signal { class SignalTransmitFactory; } namespace Remocon { class LearningRemoconFactory; } namespace Device { class DeviceFactory; } namespace Primary { class SystemFactory { public: SystemFactory(); ~SystemFactory(); void start(); private: boost::scoped_ptr<Device::DeviceFactory> pDeviceFactory_; boost::scoped_ptr<Ui::UserInterfaceFactory> pUserInterfaceFactory_; boost::scoped_ptr<Signal::SignalTransmitFactory> pSignalTransmitFactory_; boost::scoped_ptr<Memory::MemoryManagementFactory> pMemoryManagementFactory_; boost::scoped_ptr<Remocon::LearningRemoconFactory> pLearningRemoconFactory_; }; } // End namespace #endif // SYSTEM_FACTORY_H__
LearningRemoconFactoryは,ソースコード2.6に示すように,privateなメンバ変数として,ドメイン内の3つのクラスのスマートポインタを持ちます.ここで,boost::scoped_ptrを利用するとき注意すべきポイントがあります.それは,LearningRemoconFactoryのデストラクタを(特に行うべき処理が無くても)宣言しておく必要があるということです.boost::scoped_ptrは,テンプレートが実体化するときに指し示すオブジェクトのクラスの(宣言ではなく)定義を必要とします.
SystemFactoryの各メンバ関数が定義されているソースファイル(ソースコード2.7 SystemFactory.cpp)では,LearningRemoconFactory.hをインクルードします.LearningRemoconFactoryのデストラクタの宣言を行っていない場合,SystemFactory.cppコンパイル時に,LearningRemoconFactoryのデフォルトのデストラクタの定義が暗黙に挿入されます.その結果,LearningRemoconFactoryのメンバ変数の3つのboost::scoped_ptr(LearningRemoconDispatcher,RemoconDataSend,RemoconDataRegister)が実体化してしまいます.インターフェースと実装の分離の観点から,これらのクラスの定義は,LearningRemoconFactory.hではインクルードすべきではありません.なお,boost::shared_ptrでは,テンプレート実体化の時にポイントするオブジェクトのクラスの定義を必要としません.
ソースコード2.6.LearningRemoconFactory.h#if !defined(LEARNING_REMOCON_FACTORY_H__) #define LEARNING_REMOCON_FACTORY_H__ #include <boost/scoped_ptr.hpp> namespace Remocon { class LearningRemoconDispatcher; class RemoconDataSend; class RemoconDataRegister; class I_TransactionComplete; class I_LearningRemocon; class I_SignalTransmit; class I_RemoconDataReceive; class I_MemoryManagement; class LearningRemoconFactory { public: LearningRemoconFactory(); ~LearningRemoconFactory(); void setI_TransactionComplete(I_TransactionComplete* pComplete); void setI_SignalTransmit(I_SignalTransmit* pI_SignalTransmit); void setI_MemoryManagement(I_MemoryManagement* pI_MemoryManagement); I_LearningRemocon* getI_LearningRemocon(); I_RemoconDataReceive* getI_RemoconDataReceive(); private: boost::scoped_ptr<LearningRemoconDispatcher> pDispatcher_; boost::scoped_ptr<RemoconDataSend> pDataSend_; boost::scoped_ptr<RemoconDataRegister> pDataRegister_; }; } // End namespace #endif // LEARNING_REMOCON_FACTORY_H__
次に,これらのスマートポインタが指し示すオブジェクトが生成されるシーケンスを考えます.図2.6にシステム全体の初期化シーケンスを示します.最初にアクタであるOS(プログラム起動)がSystemFactoryを生成します.SystemFactoryは,各ドメインのファクトリークラスを生成します.次に,ドメイン間の関連を設定する準備として,関連先となるディスパッチャクラスを取得します.取得するときは,ディスパッチャクラスが継承している抽象クラスのポインタを受け取ります.こうすることでインターフェースと実装の分離を行うことができます.受け取った抽象クラスのポインタを,各ドメインのファクトリークラスに渡します.これで,ドメイン間の関連が設定され,各ドメインの動作開始の準備ができました.最後に,各ドメインの動作を開始させます.開始処理が必要なドメインは,基本的には前編の「5.3.ドメインユースケース分析」で「初期化する」ユースケースを抽出したドメインとなります.ユーザインターフェースドメインは,ドメイン間の関連が設定された後に,開始処理(「初期化する」ドメインユースケースの実行)を行うことで,初期画面の描画を,関連先のデバイスドメインを利用して行います.デバイスドメインは,startメンバ関数の中でメッセージループを開始します.メッセージループでは,タッチパネル押下イベントや,リモコン信号受信イベントをトリガとして,ドメイン間の協調動作を行います.デバイスドメインのstartメンバ関数がreturnするのは,システム終了時となります.
※クリックすると拡大します.SystemFactoryのコンストラクタは,ソースコード2.7に示すように,図2.6のシーケンス図と対応します.コンストラクタでは,まず,各ドメインのファクトリークラスのスマートポインタを,newしたオブジェクトで初期化しています.こうしておけば,万一どこかのファクトリークラスの生成時に例外が発生したとしても,クリーンに終了することが可能となります.オブジェクト生成後の処理の流れは,シーケンス図通りなので,特に説明の必要はないでしょう.
ソースコード2.7.SystemFactory.cpp#include <Primary/SystemFactory.h> #include <Ui/UserInterfaceFactory.h> #include <Memory/MemoryManagementFactory.h> #include <Signal/SignalTransmitFactory.h> #include <Remocon/LearningRemoconFactory.h> #include <Device/DeviceFactory.h> #include <Debug/DebugPrint.h> namespace Primary { SystemFactory::SystemFactory() //各ドメインファクトリーの生成 : pDeviceFactory_(new Device::DeviceFactory), pUserInterfaceFactory_(new Ui::UserInterfaceFactory), pSignalTransmitFactory_(new Signal::SignalTransmitFactory), pMemoryManagementFactory_(new Memory::MemoryManagementFactory), pLearningRemoconFactory_(new Remocon::LearningRemoconFactory) { //各ドメインのディスパッチャを抽象クラスとして取得 Device::I_Device *pI_Device = pDeviceFactory_->getI_Device(); Remocon::I_TransactionComplete* pI_TransactionComplete = pUserInterfaceFactory_->getI_TransactionComplete(); Device::I_TouchPanelPress* pI_TouchPanelPress = pUserInterfaceFactory_->getI_TouchPanelPress(); Remocon::I_MemoryManagement* pI_MemoryManagement = pMemoryManagementFactory_->getI_MemoryManagement(); Device::I_RemoconSignalReceive* pI_RemoconSignalReceive = pSignalTransmitFactory_->getI_RemoconSignalReceive(); Remocon::I_SignalTransmit* pI_SignalTransmit = pSignalTransmitFactory_->getI_SignalTransmit(); Remocon::I_RemoconDataReceive* pI_RemoconDataReceive = pLearningRemoconFactory_->getI_RemoconDataReceive(); Remocon::I_LearningRemocon* pI_LearningRemocon = pLearningRemoconFactory_->getI_LearningRemocon(); //ドメイン間の関連を設定 pDeviceFactory_->setI_RemoconSignalReceive(pI_RemoconSignalReceive); pDeviceFactory_->setI_TouchPanelPress(pI_TouchPanelPress); pUserInterfaceFactory_->setI_Device(pI_Device); pUserInterfaceFactory_->setI_LearningRemocon(pI_LearningRemocon); pMemoryManagementFactory_->setI_Device(pI_Device); pSignalTransmitFactory_->setI_Device(pI_Device); pSignalTransmitFactory_->setI_RemoconDataReceive(pI_RemoconDataReceive); pLearningRemoconFactory_->setI_SignalTransmit(pI_SignalTransmit); pLearningRemoconFactory_->setI_TransactionComplete(pI_TransactionComplete); pLearningRemoconFactory_->setI_MemoryManagement(pI_MemoryManagement); } void SystemFactory::start() { //各ドメインの動作開始 pUserInterfaceFactory_->start(); pDeviceFactory_->start(); } } // End namespace
次に,学習リモコンドメインの生成の流れを見てみましょう.図2.7に示すように,SystemFactoryからのコンストラクタ呼び出しで,ドメイン内のクラスを生成します.生成に続いて,生成したクラス間の関係を設定します.その後,SystemFactoryからの呼び出しに応じてドメイン間の関連を設定します.
※クリックすると拡大します. ソースコード2.8に,LearningRemoconFactoryのコンストラクタを示します.SystemFactoryと同様にスマートポインタをnewしたオブジェクトで初期化します.その後,生成したオブジェクト間の関連を設定しています.関連は通常の(C++言語組み込みの)ポインタで実装するため,pDataRegister_.get()のように,boost::scoped_ptrから通常のポインタを取得するためのメンバ関数get()を呼び出しています.関連を実装する方法として,通常のポインタではなくboost::weak_ptrを利用するという選択肢もあります.
ソースコード2.8.LearningRemoconFactory.cpp(コンストラクタ)LearningRemoconFactory::LearningRemoconFactory() :pDispatcher_(new LearningRemoconDispatcher), pDataSend_(new RemoconDataSend), pDataRegister_(new RemoconDataRegister) { // 内部の関連を設定 pDispatcher_->setRemoconDataRegister(pDataRegister_.get()); pDispatcher_->setRemoconDataSend(pDataSend_.get()); }
ここで,LearningRemoconDispatcherからsetRemoconDataSendへの関連の設定に着目してみましょう.関連を設定するためには,LearningRemoconFactoryがLearningRemoconDispatcherのメンバ関数setRemoconDataSendを呼び出す必要があります.しかし,このメンバ関数setRemoconDataSendは,LearningRemoconFactoryが,初期化時に関連を設定するためにのみ利用するのであって,学習リモコンとしての通常動作時のクラス間,ドメイン間のメッセージのやりとりには利用されません.そこで,ソースコード2.9に示すように学習リモコンドメインの各クラスは,LearningRemoconDispatcherをfriendクラスとして指定し,メンバ関数setRemoconDataSendはprivateとしました.こうすることで,学習リモコンドメインの各クラスは,(そのクラスの)公開インターフェースを最小限にすることが可能となります.逆に,自ドメインのすべてのクラスのprivate部分にアクセスできてしまうLearningRemoconFactoryの実装には注意が必要ですが,ファクトリークラスは極めて単純な責務を果たすだけなので,テストやデバッグも比較的容易です.よって,公開インターフェースを最低限にするメリットの方が大きいと判断して,このような実装としました.
ソースコード2.9.LearningRemoconDispatcher.h(抜粋)class LearningRemoconDispatcher:public I_LearningRemocon, public I_RemoconDataReceive { ...省略... private: friend class LearningRemoconFactory; void setRemoconDataRegister(RemoconDataRegister* pRegister); void setRemoconDataSend(RemoconDataSend* pSend); private: RemoconDataSend *pDataSend_; RemoconDataRegister *pDataRegister_; }; ...省略...
ソースコード2.10にLearningRemoconFactoryクラスの,その他のメンバ関数を示します.これらはすべて図2.7のシーケンス図通りに実装されています.
ソースコード2.10.LearningRemoconFactory.cpp(コンストラクタ以外)I_RemoconDataReceive* LearningRemoconFactory::getI_RemoconDataReceive() { return pDispatcher_.get(); } I_LearningRemocon* LearningRemoconFactory::getI_LearningRemocon() { return pDispatcher_.get(); } void LearningRemoconFactory::setI_SignalTransmit(I_SignalTransmit* pI_SignalTransmit) { // ドメイン間の関連を設定 pDataRegister_->setI_SignalTransmit(pI_SignalTransmit); pDataSend_->setI_SignalTransmit(pI_SignalTransmit); } void LearningRemoconFactory::setI_TransactionComplete(I_TransactionComplete* pComplete) { // ドメイン間の関連を設定 pDataRegister_->setI_TransactionComplete(pComplete); pDataSend_->setI_TransactionComplete(pComplete); } void LearningRemoconFactory::setI_MemoryManagement(I_MemoryManagement* pI_MemoryManagement) { // ドメイン間の関連を設定 pDataRegister_->setI_MemoryManagement(pI_MemoryManagement); pDataSend_->setI_MemoryManagement(pI_MemoryManagement); }
最後に,システムのスタートアップ部を実装します.ソースコード2.11に示すように,T-Kernelアプリケーションは,main関数から開始します.main関数では,タスクを生成して開始します.タスクは,main_task関数から開始し,その中でSystemFactoryを生成して開始します.
これで,実際に動作する形ですべてのコードが実装できました.
ソースコード2.11.main.cppEXPORT int main( INT ac, UB *av[] ) { static ID tskid = -1; // タスクID T_RTSK rtsk; // タスク情報 T_CTSK ctsk; // タスク生成情報 if ( ac < 0 ) { // アンロードunlspg if (tskid >= 0) { exitproc(); while (tk_ref_tsk(tskid, &rtsk) >= E_OK) tk_dly_tsk(1); } return 0; } // タスク生成 ctsk.exinf = PRCTSK_EXINF; // 拡張情報 ctsk.tskatr = TA_HLNG | TA_RNG0; // タスク属性 ctsk.task = reinterpret_cast<FP>(main_task); // タスク起動アドレス ctsk.itskpri = 60; // タスク起動時優先度 ctsk.stksz = 1024 * 4; // スタックサイズ tskid = tk_cre_tsk( &ctsk ); // タスク生成 if (tskid == E_OK) { tk_sta_tsk(tskid, 0); // タスク起動 } return 0; } void main_task( INT stacd, VP exinf ) { try { static Primary::SystemFactory s; s.start(); } catch (...) { printf("Init Fail\n"); return; } printf("[EXIT ]main_task\n"); tk_exd_tsk(); }
今回は,オブジェクト構造設計から,最終的な実装までの流れをご紹介しました. 最後に,今回の各作業でのポイントをまとめておきたいと思います.
- オブジェクト構造設計・実装では,実装言語やアーキテクチャメカニズムを考慮した設計モデルを作成し,モデルに対応したクラス定義を実装します.
- オブジェクトコラボレーション設計・実装では,オブジェクト間のメッセージのやりとりを,クラスのメンバ関数の定義として実装します.
- システム開始処理設計・実装では,ファクトリークラスを導入して,システムの開始から,オブジェクトの生成,オブジェクト間の関連の設定を行った後,システムを開始させます.
実装済みのソースコードをコンパイルしてターゲットにロードした後,無事動作を確認することができました.今回は,テストに関して触れませんでしたが,機会があればご紹介したいと思います.
最後までご覧いただき,ありがとうございました.
- 渡辺博之|渡辺政彦|堀松和人|渡守武和記 著,『組み込みUML ― eUMLによるオブジェクト指向組み込みシステム開発』,翔泳社 ,ISBN:4798102148
- Herb Sutter | Andrei Alexandrescu 著,C++ Coding Standards―101のルール、ガイドライン、ベストプラクティス C++ in‐depth series,ピアソンエデュケーション ,ISBN:4894716860
© 2006 OGIS-RI Co., Ltd. |
|