« [Windows7] Microsoft Tech Fielders 「開発者のための Windows 7 ~まずはここから~」 | トップページ | 螺旋状ウィングレット (Spiroid winglet) »

2009年5月22日 (金)

[プログラム設計事始] メソッドの外部設計(2)

◆ 複雑なメソッド ~ 2入力 / 2出力 の場合

[プログラム設計事始] メソッドの外部設計(1)」 では、 1入力 / 1出力 というシンプルなメソッドの外部設計を、 すべての入出力を定義した表として書いてみました。

入力 / 出力の数が増えても、 考え方は同じです。 ただし、 入力の組み合わせをすべて網羅するので、 表は大きくなります。

ところで、 前回は 入力 / 出力 として、 メソッドの引数と返値だけを考えました。
Program_design03_01
その他に、 メソッドの外部設計で考慮しておくべきことはないでしょうか?

まず、 メソッドの振る舞いに影響を及ぼすメソッド外部の条件があります。 ( 以降、 外部条件と呼ぶことにします。 )
メソッド中で読み込むファイルのデータが、 そうです。 その他に、 データベースを検索した結果や、 メンバ変数の値など、 メソッドの外にあってメソッド内で利用するデータは外部条件です。

さらに、 他のメソッドの返値も外部条件に含めます。
例えば、 DateTime.Now という外部条件 ( システムの現在日時 ) に基づいて、 現在時刻が午後かどうかを返すメソッド…

public bool IsNowAfternoon()
{
    DateTime nowTime = DateTime.Now;
    return IsAfternoon(nowTime);
}

…を、 次のように 2つのメソッドに分解することができます。

public bool IsNowAfternoon()
{
    DateTime nowTime = GetNowTime();
    return IsAfternoon(nowTime);
}

private DateTime GetNowTime()
{
    return DateTime.Now;
}

ここでは、 メソッド GetNowTime() の呼び出しと、 外部条件 DateTime.Now の取得は等価ですね。

これらの外部条件は、 メソッドの振る舞いに影響を及ぼすものですから、 メソッドに対する入力であると言ってもよいでしょう。

また、 メソッドの振る舞いによって、 影響を受けるものが、 メソッドの返値以外にもあります。
まずは、 出力用として渡された引数があります。 そのほかに、 メソッド中で書き換えたファイルや、 メンバ変数などもあります。 そうしたメソッドの外部の変化も、 そのメソッドの出力と考えられます。

まとめると、 次のように考えることができます。
Program_design04_01

【入力】 ( メソッドの振る舞いを決定する )
・ メソッドの引数
・ 外部条件 ( ファイル読み込み, DB クエリ, メンバ変数の読み取り, 呼び出したメソッドの返値 )

【出力】 ( メソッドの振る舞いに影響される )
・ メソッドの返値
・ 出力用の引数
・ 外部変化 ( ファイル書き込み, DB 更新, メンバ変数へ書き込み, メソッド呼び出しの影響 )


以上を踏まえて、 2入力 / 2出力 の外部設計の例として、 入力が引数 1つと外部条件 1つ、 出力が返値 ( 1つ ) とメンバ変数の変化 1つ、 というメソッドを考えてみます。

前回の string BuildMessage(string targetName) を拡張して、 次のようなロジックを作ってみます。

概要 :
文字列 {foo} から、 "Hello, {foo} !" という文字列を作リ出す。 また、 メンバ変数 AmPm に午前/午後の区別を書き込む。
ただし、 "Hello" の部分は、 朝 (5時~10時) は "Good morning"、 昼 (10時~18時) は "Hello"、 夕方 (18時~20時) は "Good evening"、 それ以降は "Good night" とする。

※ メンバ変数に書き込むところは、 非常にわざとらしいのですが、 サンプルということでお目こぼしください。
※※ クラス設計の観点からすると、 メッセージ作成時点の 午前 / 午後 の区別を後から利用したいのだとしても、  加工した 午前 / 午後 ではなく、 加工前の DateTime 型で保持したほうが良いです。 ( 生データの保持、 あるいは再加工に掛かるコストが高すぎる場合は別。 )

このメソッド string BuildMessageAndSetAmPm(string targetName) ( わざとらしい例にしたので、 メソッド名もわざとらしい名前になってしまいました ) のコードを先に示しておくと、 次のようになります。

private enum AmPm {
    Am,
    Pm
}

private AmPm _AmPm;

public string BuildMessageAndSetAmPm(string targetName)
{
    string greet;
    int nowTime = DateTime.Now.Hour;

    if ((0 <= nowTime) && (nowTime < 5))
        greet = "Good night";
    else if ((5 <= nowTime) && (nowTime < 10))
        greet = "Good morning";
    else if ((10 <= nowTime) && (nowTime < 18))
        greet = "Hello";
    else if ((18 <= nowTime) && (nowTime < 21))
        greet = "Good evening";
    else
        greet = "Good night";

    if (nowTime < 12)
        this._AmPm = AmPm.Am;
    else
        this._AmPm = AmPm.Pm;

    return string.Format(null, "{0}, {1}!", greet, targetName);
}

では、 外部設計を前回と同じように表にまとめてみましょう。
入力は 2つあります。

  • 引数 targetName : null か、 空文字 か、 1文字以上 の 3通り。
  • 外部条件 システム時刻 : 朝 (5時~10時)、 昼 (10時~12時 と 12時~18時)、 夕方 (18時~20時)、 それ以降 (20時~24時と、 0時~5時) の 6通り。 ( DateTime.Now を 「信用」 して、 それ以外はありえないものとします。 )
    ※ または、 朝 (5時~10時)、 昼 (10時~18時)、 夕方 (18時~20時)、 それ以降 (20時~24時と、 0時~5時) の 5通りと、 午前 / 午後 の 2通りの組み合わせ。 そうした場合、 ありえない組み合わせ ( たとえば、 朝 かつ 午後 など ) が 4通り出てきますので、 それらを抜くと、 やはり最終的には 6 通り。

この 2つの組み合わせですから、 3 × 6 の 18通りの入力パターンがあります。 それらすべてを列挙し、 それぞれの出力 ( メンバ変数 AmPm と 返値 の 2つ ) を漏れなく書き込めばよいのです。

引数 外部条件 外部変化 返値
string targetName システム時刻 t メンバ変数 AmPm string
null 0:00 <= t < 5:00 午前 (NullReferenceException)
null 5:00 <= t < 10:00 午前 (NullReferenceException)
null 10:00 <= t < 12:00 午前 (NullReferenceException)
null 12:00 <= t < 18:00 午後 (NullReferenceException)
null 18:00 <= t < 20:00 午後 (NullReferenceException)
null 20:00 <= t < 24:00 午後 (NullReferenceException)
"" (空文字) 0:00 <= t < 5:00 午前 "Good night,  !"
"" (空文字) 5:00 <= t < 10:00 午前 "Good morning,  !"
"" (空文字) 10:00 <= t < 12:00 午前 "Hello,  !"
"" (空文字) 12:00 <= t < 18:00 午後 "Hello,  !"
"" (空文字) 18:00 <= t < 20:00 午後 "Good evening,  !"
"" (空文字) 20:00 <= t < 24:00 午後 "Good night,  !"
"{foo}" (1文字以上) 0:00 <= t < 5:00 午前 "Good night, {foo} !"
"{foo}" (1文字以上) 5:00 <= t < 10:00 午前 "Good morning, {foo} !"
"{foo}" (1文字以上) 10:00 <= t < 12:00 午前 "Hello, {foo} !"
"{foo}" (1文字以上) 12:00 <= t < 18:00 午後 "Hello, {foo} !"
"{foo}" (1文字以上) 18:00 <= t < 20:00 午後 "Good evening, {foo} !"
"{foo}" (1文字以上) 20:00 <= t < 24:00 午後 "Good night, {foo} !"

これまた前回同様、 まったく同じ出力になるところは省略して書いても良いです。

引数 外部条件 外部変化 返値
string targetName システム時刻 t メンバ変数 AmPm string
null 0:00 <= t < 12:00 午前 (NullReferenceException)
null 12:00 <= t < 24:00 午後 (NullReferenceException)
"" (空文字) 0:00 <= t < 5:00 午前 "Good night,  !"
"" (空文字) 5:00 <= t < 10:00 午前 "Good morning,  !"
"" (空文字) 10:00 <= t < 12:00 午前 "Hello,  !"
"" (空文字) 12:00 <= t < 18:00 午後 "Hello,  !"
"" (空文字) 18:00 <= t < 20:00 午後 "Good evening,  !"
"" (空文字) 20:00 <= t < 24:00 午後 "Good night,  !"
"{foo}" (1文字以上) 0:00 <= t < 5:00 午前 "Good night, {foo} !"
"{foo}" (1文字以上) 5:00 <= t < 10:00 午前 "Good morning, {foo} !"
"{foo}" (1文字以上) 10:00 <= t < 12:00 午前 "Hello, {foo} !"
"{foo}" (1文字以上) 12:00 <= t < 18:00 午後 "Hello, {foo} !"
"{foo}" (1文字以上) 18:00 <= t < 20:00 午後 "Good evening, {foo} !"
"{foo}" (1文字以上) 20:00 <= t < 24:00 午後 "Good night, {foo} !"

このようにして、 複数の入力がある場合は、 ありえる入力の組み合わせすべてに対して、 出力を定義してあげるわけです。
ですが、 もうお気づきだと思います。 入力が 5個も 10個もあったら、 爆発的に表が大きくなってしまいますね。 それぞれが 2通りしかなくても、 入力が 10個あれば 2^10 で 1024 通りにもなっていしまいます。

そのような 「入力パターンの爆発」 が起きると、 大きな表を書くこともたいへんですが、 そもそも、 何千ものパターンを把握すること自体が困難です。
そこで、 できればパターンの総数を減らせないか、 あるいはパターンの総数が減らないにしても、 せめて一度に扱うパターンの数は減らせないか、 と考えます。
メソッドの場合は、 そのメソッドを上手く複数のメソッドに分割してやることで、 表のサイズを小さくできることがあります。

上の表でまず気になるのは、 出力に "Hello, …" が 2行連続して登場しているところでしょう。
なぜそうなっているかというと、 その 2行ではメンバ変数 AmPm に書き込む内容が違うからですね。 そこで、 メンバ変数 AmPm に書き込むためのメソッド SetAmPm() を新しく作ることにしてみます。 すると、 BuildMessageAndSetAmPm(string targetName) メソッドは、 メンバ変数に 午前 または 午後 を書き込むのではなく、 常に SetAmPm() を呼び出すだけでよくなりますね。
"Hello, …" が 2行連続しているところは、 まったく同じになります。

"{foo}" (1文字以上) 10:00 <= t < 12:00 SetAmPm() 呼び出し "Hello, {foo} !"
"{foo}" (1文字以上) 12:00 <= t < 18:00 SetAmPm() 呼び出し "Hello, {foo} !"

よって、 この 2行は、 まとめて 1行にできます。

"{foo}" (1文字以上) 10:00 <= t < 18:00 SetAmPm() 呼び出し "Hello, {foo} !"

次に、 "Hello" だとか "Good morning" だとかの挨拶の言葉を作ってくれるメソッド GetGreet() を、 新しく作ることにします。 すると、 BuildMessageAndSetAmPm(string targetName) メソッドは、 GetGreet() の返値を使って、 同じ方法でメッセージを組み立てることができるようになります。
5通りあった出力パターン…

"{foo}" (1文字以上) 0:00 <= t < 5:00 SetAmPm() 呼び出し "Good night, {foo} !"
"{foo}" (1文字以上) 5:00 <= t < 10:00 SetAmPm() 呼び出し "Good morning, {foo} !"
"{foo}" (1文字以上) 10:00 <= t < 18:00 SetAmPm() 呼び出し "Hello, {foo} !"
"{foo}" (1文字以上) 18:00 <= t < 20:00 SetAmPm() 呼び出し "Good evening, {foo} !"
"{foo}" (1文字以上) 20:00 <= t < 24:00 SetAmPm() 呼び出し "Good night, {foo} !"

…は、 1通りの定義で済むようになります。

"{foo}" (1文字以上) GetGreet() の返値 "{bar}" SetAmPm() 呼び出し "{bar}, {foo} !"

このようにして、 新しく作ったメソッド 2 つも含めて全部を書くと、 次のようになります。

string GetGreet(DateTime t)
引数 外部条件 外部変化 返値
システム時刻 t (無し) (無し) string
0:00 <= t < 5:00 "Good night"
5:00 <= t < 10:00 "Good morning"
10:00 <= t < 18:00 "Hello"
18:00 <= t < 20:00 "Good evening"
20:00 <= t < 24:00 "Good night"
void SetAmPm(DateTime t)
引数 外部条件 外部変化 返値
システム時刻 t (無し) メンバ変数 AmPm (無し)
0:00 <= t < 12:00 午前
12:00 <= t < 24:00 午後
string BuildMessageAndSetAmPm(string targetName)
引数 外部条件 外部変化 返値
string targetName GetGreet(DateTime.Now) の返値 メンバ変数 AmPm string
null "{bar}" (1文字以上) SetAmPm() 呼び出し (例外発生)
"" (空文字) "{bar}" (1文字以上) SetAmPm() 呼び出し "{bar},  !"
"{foo}" (1文字以上) "{bar}" (1文字以上) SetAmPm() 呼び出し "{bar}, {foo} !"

分割前は 18パターン ( 省略して 14パターン ) あったものが、 トータルで 10パターン、 個々の表は 2~5パターンに減らすことができました。

※ ここで、 GetGreet() も SetAmPm() も例外を出さないこと、 また、 GetGreet() は必ず 1文字以上の文字列を返してくれることに、 注意してください。 もしも、 例外が出たりするようであれば、 BuildMessageAndSetAmPm() の定義が増えることになります。

なお、 このようにメソッドを分割することは、 クラスの内部設計に対する干渉であるとも言えます。
プロジェクトによっては、 勝手にメソッドを増やすことをルールで禁じている場合があります。 そういうときは、 しかたがないので、 メソッドの内部を分割する ( C# では {} で括ってスコープを切り、 引数の代わりにローカル変数を使う ) ことで、 逃げたりします。

最後に、 このようにメソッドの外部設計を分割したときの、 コードを載せておきます。

private enum AmPm {
    Am,
    Pm
}

private AmPm _AmPm;

private void SetAmPm(DateTime nowTime)
{
    if (nowTime.Hour < 12)
        this._AmPm = AmPm.Am;
    else
        this._AmPm = AmPm.Pm;
}

private string GetGreet(DateTime nowTime)
{
    int hour = nowTime.Hour;

    if ((0 <= hour) && (hour < 5))
        return "Good night";
    else if ((5 <= hour) && (hour < 10))
        return "Good morning";
    else if ((10 <= hour) && (hour < 18))
        return "Hello";
    else if ((18 <= hour) && (hour < 21))
        return "Good evening";
    else
        return "Good night";
}

public string BuildMessageAndSetAmPm(string targetName)
{
    DateTime nowTime = DateTime.Now;

    SetAmPm(nowTime);

    string greet = GetGreet(nowTime);
    return string.Format(null, "{0}, {1}!", greet, targetName);
}

|

« [Windows7] Microsoft Tech Fielders 「開発者のための Windows 7 ~まずはここから~」 | トップページ | 螺旋状ウィングレット (Spiroid winglet) »

プログラミング」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)


コメントは記事投稿者が公開するまで表示されません。



トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/209349/45093126

この記事へのトラックバック一覧です: [プログラム設計事始] メソッドの外部設計(2):

« [Windows7] Microsoft Tech Fielders 「開発者のための Windows 7 ~まずはここから~」 | トップページ | 螺旋状ウィングレット (Spiroid winglet) »