段階的詳細化からコードへの変換

課題プログラムの理解(3) : 段階的詳細化とコードへの変換

トップダウンとボトムアップ

プログラムを構成する方法として,大きく分けて,以下の2通りがあります.

段階的詳細化 (トップダウン)

プログラム全体の流れを記述し,次第に内部を詳細化していく手法.

ボトムアップ

プログラムに必要な部品から組み立てていく手法.

通常は,どちらかの手法だけでプログラムを作るのではなく,両方向からのアプローチをバランスよく実践する必要があります.なぜなら,どちらを採っても,以下の不安があるからです.

  • ボトムアップでは,作成初期段階で必要なプログラム部品を推測するのは難しい.

  • トップダウンでは,ある程度先の段階まで動作するプログラムを作れないので,完成が危険な賭けになる.

そこで,両者の切替えをどう見切るかが,プログラマの手腕を発揮する部分であるともいえます.本演習では,段階的詳細化によって,プログラムの流れをある程度整理できたら,流れを細かい支流に整理して,支流毎に1つプログラムを書いてみて,動作の確認をしていきます.そうして,プログラムの流れと部品の整備を漸進的に進めていきます.

プログラム全体の流れを書いてみよう

プログラム全体の流れを段階的詳細化を使って記述してみよう.

まずは,メインの処理から.

/*** main ***/

while (標準入力から1行読み込む → 成功) {
    読み込んだ行を処理をする;
} // 繰り返し

かなり曖昧な感じですね.どの程度詳細に(あるいは,抽象的に)書けばいいのでしょうか.1つの指針として,流れを書く場合は,制御構造 (if/switch, while/for)を入れて「支流」を数個作ることを意識します.詳細化が進み,支流が細くなってきた時点で,短い関数として実装できそうか判断します.

では,もう一段階詳細化してみます. ここでは,処理の粒度が大きそうな,読み込んだ行を処理をする;について詳細化してみましょう.

/**** 読み込んだ行を処理する ****/

if (行頭が '%' ではじまっているなら) {
  入力文字列をコマンド入力として処理する
} else {
  入力文字列をCSV形式の名簿データとして処理する;
}

さらに,もう一段階詳細化してみます.

/***** 入力文字列をコマンド入力として処理する *****/

2文字目のコマンド文字と4文字目以降の引数を取り出す;
コマンド文字と引数で示された処理を実行;
/***** 入力文字列をCSV形式の名簿データとして処理する *****/

行をコンマで5つの文字列に分割;
5つの文字列を項目毎に構造体や配列に保存;

どちらも似たような構成です.

  • 文字列を解釈できるデータとして準備する

  • 準備されたデータを利用してさらなる処理をする

という流れですね.

さて,詳細化の過程で実際のコード(プログラム)に書けそうな所があったら,コードを記述して,動作の確認をしてみるとよいでしょう.既に,

  • 行をコンマで5つの文字列に分割;

  • 標準入力から1行読み込む;

は,前回の練習課題として書いた関数が使えそうです.文字列分割関数split()と,1行入力関数get_line()ですね.

なお,上記の手順を踏まないで,「CSVを使うんだから,コンマで行を分割するんだろうな,だったら,split()から作って実験するか.」と発想して作ってみるのが,ボトムアップ的発想です.

カンが良ければ,結果は同じかもしれません.

段階的詳細化をコードに変換してみよう

ここまでで,段階的詳細化によってプログラムの流れを整理しました.c言語のコードとして書き直しながら,さらに洗練させていきましょう.

以下,再掲です.

/*** main ***/
while (標準入力から1行読み込む → 成功) { // → get_line()
    読み込んだ行を処理をする;
} // 繰り返し

/**** 読み込んだ行を処理する ****/
if (行頭が '%' ではじまっているなら) {
  文字列をコマンド入力として処理する
} else {
  文字列をCSV形式の名簿データとして処理する;
}

/***** 文字列をコマンド入力として処理する *****/
2文字目のコマンド文字と4文字目以降の引数を取り出す;
コマンド文字と引数で示された処理を実行;

/***** 文字列をCSV形式の名簿データとして処理する *****/
行をコンマで5つの文字列に分割; // → split()
5つの文字列を項目毎に構造体や配列に保存;

各処理を関数と見立てて,Cらしく書き直してみましょう.以下のように名前を決めてみます.

項番

処理名称

関数名

1

main

main()

2

標準入力から1行読み込む

get_line()

3

読み込んだ行を処理をする

parse_line()

4

文字列をコマンド入力として処理する

parse_command()

5

文字列をCSV形式の名簿データとして処理する

parse_csv()

6

コマンド文字と引数で示された処理を実行

exec_command()

7

5つの文字列を項目毎に構造体や配列に保存

new_profile()

8

行をコンマで5つの文字列に分割

split()を利用

parseは,日本語で「構文解析」といいます.(参考:parse - 英辞郎 on the WEB

構文解析とは,文字列を解析して,文法に合致するかチェックすることです.本課題のプログラムでいえば,文法とは,仕様に合致したCSV書式や%コマンドの書式ですね.

ここでは,parse_???によって,構文解析と同時に,「内部構造(構造体や配列)への代入」や,「各コマンド(%Qや%P)への引数の中継」も担うことが期待される関数としています.

これで概形はできました.もう少し,関数の関係を見直しつつ,推敲してみましょう.

  • [項番4, 6]parse_command()が成すべき処理のほとんどは,exec_command()が司ることになりそうです.parse_command()相当の処理はparse_line()の中で実装してしまうことにしましょう.

  • [項番5, 7, 8]parse_csv()がなすべき処理は,new_profile()による構造体への代入そのものに見えます.parse_csv()new_profile()をまとめて,1つの関数new_profile()にしてしまいましょう.

  • parse_csv()の意味を残して,new_profile_fromcsv()という名前のほうがよかったかもしれませんが,今のところCSV以外からstruct profileを作る機能がないので,new_profile()としておきます.

注釈

考察の参考ここでは,やや相反するポリシーでparse_???の実装を考えているように見えます.レポートBの執筆に取り掛かるころに,考察を通して振り返ってみると,考察の取っ掛かりがあるかもしれません.

以下は,上記命名に基づいて,プログラムの流れをCらしく書き直した結果です.各関数間で受け渡すべき引数は,今のところ考慮に入れていません.

Listing 7 名簿管理プログラムの流れ ver.1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
exec_command()
{
  /* コマンド文字と引数で示された処理を実行 */
}

new_profile()
{
  split();   // (splitを利用して) 行をコンマで5つの文字列に分割
  /* 5つの文字列を項目毎に構造体や配列に保存 */
}

parse_line()
{
  if (/* 行頭が '%' ではじまっているなら */) {
    /* parse_command : 文字列をコマンド入力として処理する */
    ;                // 2文字目のコマンド文字と4文字目以降の引数を取り出す
    exec_command();  // コマンド文字と引数で示された処理を実行
  } else {
    /* parse_csv : 文字列をCSV形式の名簿データとして処理する */
    new_profile();
  }
}

main()
{
  while (get_line()) {  // get_line() == 1 や get_line() != 0 でも良い
      parse_line();
  } // 繰り返し
}

main()について,実際に実装例を考えてみましょう.これまでの実装を思い出せば,およそ,以下のような関数として書くことになりそうです.

1
2
3
4
5
6
7
8
9
int main()
{
  char line[MAX_LINE_LEN + 1];

  while (get_line(line)) {
    parse_line(line);
  }
  return 0;
}

parse_line()には,get_line()で読取った行lineが必要ですから,そのまま引数としています.

なお,複数の関数を呼び出す際には,定義をする順序に注意が必要です.ぱっと注意点が思いつかない人は,教科書K&R(例えば,p.32 や p.88)を読んで,復習してください.