前回、サービス(を表すクラス)が何をするか、というコードを示しました。ここで、サービス クラス(の定義)をもう一度見てみます。
class LocalService
{
private:
SC_HANDLE handle;
static unsigned int FixTimeout(unsigned int timeout);
DWORD WaitForState(DWORD status, unsigned int timeout);
public:
LocalService(void);
virtual ~LocalService(void);
DWORD Open(SC_HANDLE manager, LPCTSTR serviceName, DWORD desiredAccess);
void Close(void);
DWORD Start(unsigned int timeout = 30);
DWORD Stop(unsigned int timeout = 30);
};
Open メソッドによって、サービスとリンクします。これは、コンストラクタに持って行っても良かったのですが、Open という操作に失敗したときにインスタンスがどのような状態として存在するべきか?というのを考えて、Open に分けることにしました。
Close メソッドによって、Open で接続したサービスから切り離します。これは、.NET Framework だと IDisposable インターフェイスを実装し、Dispose メソッドを明示実装で隠蔽する様にして実装することになります。
Start メソッドは、接続しているサービスを起動します。同様に、Stop メソッドで停止させます。
これを、初回のように、順序を付けてみます。
サービス マネージャにアクセスする。
指定のサービスにアクセスする。
停止させる。
起動させる。
サービスのインスタンスを破棄する。
サービス マネージャのインスタンスを破棄する。
このようになります。そして、問題に対応するのも、ここは変わりません。なぜなら、ここに書いたのは「サービスに対して指示をするオブジェクト」がすることだからです。「依存しているサービスを列挙する」とか、「それらを止める」などは、サービス オブジェクトが責任を持ってすることで、サービスに対して指示をするオブジェクトには関係がないのです。
これがどういうことかというと、設計の段階で、誰(オブジェクト)がすることに対して責任を持つのか、明確にするということです。
たまに、「ログ フラグが true なら、ログを出力する関数を呼ぶ」というコードを見かけます。次のような感じです。
// 例1
if (doOutputLog) {
LogWrite(logString);
}
// 例2
if (doOutputLog && logSize < 1024) {
logWrite(logString);
}
これは、違います。ログを出力するように命令するオブジェクトは、出力フラグのオン/オフにかかわらず、「ログ出力をする」と、命令すればいいのです。ログ出力オブジェクトが、命令を受け取った後、「ログの出力をするか?」検討すればいいのです。もし、ログ フラグが「デバッグ構築の時は常に ture」という条件に変わっても、変更する箇所は「ログ出力を行う」一カ所です。出力指示の箇所で判別していれば、全ての判別箇所を変更しなければならないし、漏れがないかチェックすることは困難です。
次に、サービスの「停止させる」を見ます。
停止信号を送る。
停止状態になるまで待つ。
ここに、修正を加えます。すると、こうなります。
依存していて、動作しているサービスを列挙する。
上記サービスについて、停止させる。
停止信号を送る。
停止状態になるまで待つ。
また、サービス クラスにプロパティを一つ追加します。
class LocalService
{
private:
SC_HANDLE handle;
static unsigned int FixTimeout(unsigned int timeout);
DWORD WaitForState(DWORD status, unsigned int timeout);
std::list dependentServices;
public:
LocalService(void);
virtual ~LocalService(void);
DWORD Open(SC_HANDLE manager, LPCTSTR serviceName, DWORD desiredAccess);
void Close(void);
DWORD Start(unsigned int timeout = 30);
DWORD Stop(unsigned int timeout = 30);
};
このように、設計の中心にオブジェクトを据え、オブジェクトの責任を明確にしていれば、手を加えるべきところが他から独立します。変更が他から独立するということは、変更による影響が、他のところに及ばないということです。
今回は「再起動」であるため、「止める」と「動かす」の2つに変更が及びます。また、「止めたものを動かす」ために、「止めた」という情報を覚えておかなければなりません。そのために、インスタンス変数として、depententServices を追加しました。しかし、この追加は“インスタンス変数”であるため、インスタンス間では独立しており、互いに影響を与えません。また、追加した変数のアクセス指定子は private です。このため、クラス外からは追加したということがわかりません。このため、変更は LocalService クラス内で閉じていると言えます。
変更の影響が外部に及ばないとのメリットは、わかりますか?
先ほど、ログを出力する箇所の例を出しました。今度は、オブジェクト指向で同じコードを例示します。
// Log クラス
private BOOL Log::DoOutput(void) {
return (doOutputLog == LOGGING_ALL ||
(doOutputLog == LOGGING_LIMIT && logSize < maxSize));
}
public void Log::Write(LPCTSTR message) {
if (DoOutput()) {
cout << message;
logSize += _tcslen(message);
}
}
// 呼び出し
Log log;
log.Write(message);
このように、「どのようなときに書き出すか」という判断を、ログというオブジェクトが行えば、ログというものがどのようなものになっても、変更するところは Write メソッドの中だけで済みます。この場合、doOutputLog を設定しているところも変更しますが、全てのコードから doOutputLog を“使用しているところ”を探す必要はありません。
もっとも、この程度は「構造化プログラミング」の範疇ですが。
構造化プログラミングでは、変更の可能性がある箇所を切り出したり、同じ処理をしているところをまとまることで、変更による影響が広がらないようにしました。オブジェクト指向は、切り出す箇所を「誰がするべきか」という視点で行い、「誰」によってまとめていると言ってもよいでしょう。その目的は、変更に強いプログラムにすることです。変更に強いとは、変更による影響が及ぶ範囲を小さくし、変更する箇所を物理的に少なくすることです。
投稿日時 : 2008年9月30日 22:04