4158 プログラム設計 Program Design ◇ 担当教官:井上 克郎(いのうえ かつろう) G311号室 Tel:6570 E-mail:inoue@ics.es.osaka-u.ac.jp ◇ 対象 : 計算機科学・ソフトウェア科学コース 3年次 選択(A群) ◇ 単 位 数 : 2 ◇ 開講時期: 1学期 月曜 3時限 ◇ 講義目的: ソフトウェアシステムの完成のためには、プログラミング以外にも、仕様記述、 設計、テスト、保守などいろいろ重要な作業がある。本講義では、プログラ ミングの学習を前提にして、ソフトウェアシステムの設計に関する種々の手法 や考え方を学ぶ。また、演習を通じて設計作業の重要性を身につける。 ◇ 講義内容 : 1.ソフトウェアシステムの開発に関する種々の工程 要求、仕様記述、設計、コーディング、テスト、保守 2.プログラミングに関する考察 記述スタイル、データ定義、抽象データ型、 オブジェクト指向言語 3.フローチャートと構造化プログラミング 問題からプログラム制御構造の導出 分岐、繰返しの基本構造、PAD 4.段階的詳細化 疑似言語プログラム 5.ジャクソン法(JSP) 構造図、命令の割り付け 6.プログラムの検証法 ループ不変式、強正当性 ◇ 教科書 :林雄二著「プログラムの設計の基礎」森北出版 1993 ISBN-4-627-82250-2 ◇ 参考書 : B. Kernighan & R. Pike, The Practice of Programming, Addison-Wesley, 1999, ISBN-0-201-61586-X ◇ 成績評価: レポートおよび試験(中間、期末2回予定)による。 ◇ 受講要件 :「プログラミングA、B、C、D」の知識、およびワークステーショ ンの基本的な操作が必要。 ◇ コメント: ソフトウェア開発作業で非常に重要なのは、設計作業であることを理解してもら いたい。
IF(X.LT.Y) GO TO 30 IF(Y.LT.Z) GO TO 50 SMALL=Z GO TO 70 30 IF(X.LT.Z) GO TO 60 SMALL=Z GO TO 70 50 SMALL=Y GO TO 70 60 SMALL=X 70 ...
は、Fortran プログラムで、GOTO文を多用したもの。初期のFortranは、そう なりがちだった。しかし、もう少し工夫すれば、わかりやすく書けよう。
SMALL=X IF(Y.LT.SMALL) SMALL=Y IF(Z.LT.SMALL) SMALL=Z
制御構造
初期のFortranは、プログラムの流れを変えるための仕掛け(制御
構造)としては、算術IF文、GOTO文などしかなかった。必然的にわかりにくい。
50 IF(C-COMMA) 55, 70, 55 55 IF(C-SCAL) 60, 70, 60 60 IF(C-DASH) 65, 70, 65 65 NC=NC+1 70 ...
GOTOの入り乱れたプログラムはスパゲッティプログラムと呼ばれる。Fortran は、論理IF文の導入で少しはましになった。GOTO文を使わずに済む場合が増す。
50 IF(C.NE.COMMA .AND. C.NE.SCOL .AND. C.NE.DASH) NC=NC+1
さらにその後、設計された言語や近年拡張されたFortranなどは、たくさんの 種類の制御構造を持っている。例えばPascalは、
Idea in mind ------(far)----- -----> Program in low level lang.
Idea in mind ------(close)-> Program in high level lang.
次のPL/IプログラムをGOTOなしに書き換えてみよう。
IF A>B THEN DO ; LARGE=A ; GO TO CHECK ; END; LARGE=B; CHECK:IF LARGE>C THEN GO TO OUTPUT; LARGE=C; OUTPUT: ...
しかし、GOTOが全部なくなればそれでめでたし、という訳ではない。 次のPASCALプログラムについて考えてみよう。
program parser(input,output); label 99; const empty = '*' ; var sym: char; procedure getsym; begin repeat read(sym); write(sym) until sym <> ' ' end {getsym}; procedure error; begin writeln; wirteln(' incorrect input'); goto 99 end{error} ; procedure term; procedure factor; begin if sym in ['A'..'Z', empty] then getsym else if sym='[' then begin getsym; term; if sym=']' then getsym else error end else error end{factore}; begin factor; while sym in ['A'..'Z','[',empty] do factor end{term}; procdure expression; begin term; while sym=',' do begin getsyml term end end{expression} ; begin {main program} while !eof(input) do begin getsyml if sym in ['A'..'Z'] the getsym else error; if sym= '=' then getsym else error; expression; if sym <> '.' then error; writeln; readln; end; 99: end.
このプログラムをGOTOなしにするのはやっかいであろう。エラーが起こった時 のためのフラグを用意して、各制御文で、そのフラグもチェックして飛び出す、 うんぬん。
GOTO文も使いようによっては、アイディアを簡単に表現する重要な手段になる。
データの型
プログラムの流れのみならず、扱うデータもできるだけ、考えを素直に表現で
きるようにしたい。例えば、色を表すデータは、直接その色でいろいろな操作
の指定ができるようにしたい。
(i) x:= 赤;とか。一方
赤 -> 1 青 -> 2 ...を決めておいて、
(ii) x:= 1 ;とかしたくない。上の(i)を可能にするために、Pascalでは列挙型が用意され ている。
type color = (赤, 青, ... ) ; var x: color ; ... x:=青; ...また、いくつかのデータをまとめて一つの操作単位として扱えるように構造を もつデータ型が導入されている。
配列型、レコード型、集合型、ファイル型
type cell = record next: link; data: integer; end ;できるだけ、関連のあるデータは一まとまりの構造にしておこう。構造化言語
Fortranが始めての`高級言語’と呼ばれるプログラミング言語。プログラム 実行の効率を追求していたが、前述のようにいろいろな問題あった。より高度 なアイディアを素直に表現できるような言語が求められた。Algol-58 -> Algol-60 -> Pascal ...
いろいろな制御構造の導入され、データの型の構造化、抽象化などが行なえる。 ここで、同じ動作をするプログラムをそれぞれの言語で書いたものを見てみよ う。
*********** * Fortran * *********** DIMENTION DTA(900) SUM = 0.0 READ 10, N 10 FORMAT(I3) DO 20 I = 1, N READ 30, DTA(I) 30 FORMAT(F10.6) IF (DTA(I)) 25, 20, 20 25 DTA(I) = -DTA(I) 20 CONTINUE DO 40 I = 1, N SUM = SUM + DTA(I) 40 CONTINUE AVG = SUM/FLOAT(N) PRINT 50, AVG 50 FORMAT(1H, F10.6) STOP ************ * Algol-60 * ************ integer N; Read Int (N); begin real array Data[1:N]; real sum, avg; integer i; sum := 0; for i:=1 step 1 until N do begin real val; Read Real (val); Data[i] := if val<0 then -val else val end; for i:=1 step 1 until N do sum := sum/N; Print Real (avg) end end ********** * Pascal * ********** program AbsMean (input, output); const Max = 900; type index = 1..Max; val N: 0..Max; Data: array [index] of real; sum, avg, val: real; i: index; begin sum:= 0; readln(N); for i:= 1 to N do begin readln(val); if val<0 then Data[i] := -val else Data[i] := val end; for i:= 1 to N do sum := sum + Data[i]; avg := sum/N; writeln(avg) end.どのプログラムが理解しやすいだろうか?慣れの問題もあろう。Algol, Pascalでの特徴は、プログラムの動作(動的な性質)とプログラムの表記上の 形式とがかなり一致しているころ。プログラムを理解するために、いちいち GOTO文の飛び先をおっかけたりする必要があまりない。制御構造、ネストの様 子を見ることによって、どういう動作をするプログラムかが推測できる。
付録
ADAのプログラムを見てみよう。ADAは米国の国防省が中心になって作った言語。 Pascalでのいろいろな問題点の改良、モジュール間通信、名前のOverloading などが特徴
package TABLES is type TABLE is array (INTEGER range <>) of FLOAT; procedure BINSEARCH (T: TABLE; SOUGHT FLOAT; out LOCATTION: INTEGER; out FOUND: BOOLEAN) is LOWER : INTEGER range T'FIRST..T'LAST := T'FIRST; UPPER : INTEGER range T'FIRST..T'LAST := T'LAST; MIDDLE : INTEGER range T'FIRST..T'LAST := (T'FIRST+T'LAST)/2; begin loop if T(MIDDLE) = SOUGHT then LOCATION := MIDDLE; FOUND := TRUE ; return; elseif UPPER < LOWER then FOUND := FALSE; return; elseif T(MIDDLE) > SOUGHT then UPPER:=MIDDLE-1; else LOWER:= MIDDLE+1; end if; MIDDLE := (LOWER+UPPER)/2; end loop; end BINSEARCH; end TABLES;
(2)プログラムを理解し易くするための工夫
コメント
もう、いろいろな授業や演習を通じて、プログラムは、単に計算機で動けば、 何でもいい、という考えは、いろいろ困った問題を引きおこすことを知ってい るだろう。
他人が読んで、一体何をしているのか、全然分からない、プログラムを書いた 本人でさえ、少し時間が経てば、何をしているのか全然分からなくなる、とい うのはよくあることである。
while(<>){ if(/\cL/) {print ("\n!!!Warning !!!! Page break found unexpectedly ", "Paper Number: $paper_no Reviewer: $reviewer\n") ; } elsif(/Paper#:/) { $paper_no="" ; $paper_type=""; $pc_submit="" ; $conflict = "" ; $title=""; $author=""; $reviewer=""; $other_reviewer_1="" ; $other_reviewer_2="" ; $overall_eval= "" ;$confidence ="" ; $comments_pc = "" ;$paper_summary ="" ; $justification= "" ;$other_comments="" ; /(\d)(\d)(\d)/ || die "no paper number found"; $paper_no = $& ; $_=$' ; /[eErR]/ || print "No paer type" ; $paper_type = $& ; # print "$paper_no \n $paper_type"; if (/PC\s+submit,\s*/) { $_ = $'; /\]/ ; $pc_submit = $`; # print "PC sumbit ***** $pc_submit"; } ...
これは、井上が数年前に作成した、論文の査読結果を整理するプログラムの一 部である。言語は、Perlという文字列処理の言語である。
このプログラムは、全くコメントが入っていないので、自分でもさっぱり何を やっていたのか、思い出せない。特に、このPerlは、いろいろな記号でいろい ろ重要な意味を表しているので、言語仕様を忘れてしまうと、全く読解の手が かりがなくなってしまう。
/* The newline cache: remembering which sections of text have no newlines. */ /* If the user has requested newline caching, make sure it's on. Otherwise, make sure it's off. This is our cheezy way of associating an action with the change of state of a buffer-local variable. */ static void newline_cache_on_off (buf) struct buffer *buf; { if (NILP (buf->cache_long_line_scans)) { /* It should be off. */ if (buf->newline_cache) { free_region_cache (buf->newline_cache); buf->newline_cache = 0; } } else { /* It should be on. */ if (buf->newline_cache == 0) buf->newline_cache = new_region_cache (); } } ...上は、emacsのサーチ部のソースの一部である。後で、理解するためのいろい ろなコメントが挿入されている。
文毎のコメントよりも、ある程度、ブロック単位で、この部分では、どういう 状況で何をしているかを詳しく書いている。こういう情報が、プログラムのデ バッグや、変更のためには必須である。
名前の付け方
変数名、関数/手続き名に、後あと、何をしているか分かりやすくするための 名前を付けるのは、当然であろう。
aとか、fとか、とりあえず短くて打ちやすいのを選んでしまうと、後で、何の 変数かな、とか、何をする関数かな、とか、内部を読んで、解読する必要があ ろう。newline_cacheとかnewline_cache_on_ofとか、何のための変数か、どの ようなことをするのか、を表す名前にしておくと、想像がつく。ある程度予備 知識を持っていると、プログラムを読みやすい。
インデンテーション(Indentation)
プログラムを読みやすくする手がかりとして、プログラムのインデンテーショ ン(段付け)は、非常に重要である。プログラムの構文にしたがって、プログ ラムの各、文の左側に適当な空白を挿入する。
if文やdo文などを繰り返し重ねて使う場合には、同じ重なりの深さ(ネストの 深さ)レベルにある文どうしは、同じインデンテーションにする。
Level1 Level2 Level3 Level3 Level2 Level3 Level4 Level2 Level1このような、インデンテーションを自動的につけてくれるプログラムがある。 以下は、簡単なCのプログラム(ボーリング点数計算プログラム)の一部であ る。うまくインデントされていない。
compute_score() { if (current_frame_no != 10) { if (current_frame_no - latest_sum_up_frame_no ==3) { fix_score(latest_sum_up_frame_no + 1); ++latest_sum_up_frame_no ; } if (current_frame_no - latest_sum_up_frame_no ==2) {if (game[current_frame_no].strike && game[current_frame_no-1].strike) return; else { fix_score(latest_sum_up_frame_no + 1); ++latest_sum_up_frame_no ; } } /* Here, current_frame_no - latest_sum_up_frame_no ==1 */ if (game[current_frame_no].strike || game[current_frame_no].spare) return; else { fix_score(current_frame_no); ++latest_sum_up_frame_no ; } } else while ( latest_sum_up_frame_no <= current_frame_no) { fix_score(latest_sum_up_frame_no + 1); ++latest_sum_up_frame_no ; }}これにcbコマンド(C Beautifier)をかけて整形すると次のような出力が得られ る。
compute_score() { if (current_frame_no != 10) { if (current_frame_no - latest_sum_up_frame_no ==3) { fix_score(latest_sum_up_frame_no + 1); ++latest_sum_up_frame_no ; } if (current_frame_no - latest_sum_up_frame_no ==2) { if (game[current_frame_no].strike && game[current_frame_no-1].strike) return; else { fix_score(latest_sum_up_frame_no + 1); ++latest_sum_up_frame_no ; } } /* Here, current_frame_no - latest_sum_up_frame_no ==1 * / if (game[current_frame_no].strike || game[current_frame_no].spare) return; else { fix_score(current_frame_no); ++latest_sum_up_frame_no ; } } else while ( latest_sum_up_frame_no <= current_frame_no) { fix_score(latest_sum_up_frame_no + 1); ++latest_sum_up_frame_no ; } }そのほか、プログラムを編集するエディタ(例えばemacs)などに、自動的に インデトを行なう機能を持たせることもできる。さらに、プログラムの入力と 同時にインデントを行なうと同時に、プログラムの構文のチェックを行なった り、次に入力すべき文の候補を表示したりする機能を持たせたエディタ(構文 エディタ)も提案されている。
(3)モジュール化、カプセル化
必要な情報のみを最低限与えて、それ以外の具体的な実現の詳細を見せないこ とを「情報隠蔽」(Information Hiding) という。必要な情報は、必要な範囲 のみに公開する。カプセル化する(encapsulation)とは、あるモジュール単位、 手続き単位などで情報隠蔽を行なうことをいう。
プログラムを作成する場合には、1つの大きなモジュールを作成するのではな く、いくつかのモジュールから構成するようにして、モジュール間の連絡に必 要な情報以外の情報は、その一モジュール内にしか扱えないようにする。(モ ジュール化)
以下、ADAのプログラムで、複素数型の定義を行なっているパッケージの例。 COMPEXの構成要素は privateで宣言されており、外からは見えない。定義され ている関数を用いて利用する。
COMPLEXの宣言 package COMPLEX_TYPE is type COMPLEX is private; I: constant COMPLEX; function "+" (X,Y: COMPLEX) return COMPLEX; function "-" (X,Y: COMPLEX) return COMPLEX; function "*" (X,Y: COMPLEX) return COMPLEX; function "/" (X,Y: COMPLEX) return COMPLEX; function RE (X : COMPLEX) return FLOAT; function IM (X : COMPLEX) return FLOAT; function "+" (X : FLOAT; Y : COMPLEX) return COMPLEX; function "*" (X : FLOAT; Y : COMPLEX) return COMPLEX; private type COMPLEX = record RE, IM :FLOAT:=0.0; end record; I : constant COMPLEX := (0.0, 1.0); end COMPLEX_TYPE; COMPLEXを使う側 declare use COMPLEX_TYPE; X,Y : COMPLEX; Z : COMPLEX := 1.5 + 2.5*I ; begin X:= 2.5+3.5*I; Y:= X+Z; Z:= RE(Z) +IM(X)*I; if X = Y then X:= Y+Z; else X:= Y*Z; end if; end; COMPLEXの定義 package body COMPLEX_TYPE is function "+" (X, Y: COMPLEX) return COMPLEX is begin return (X.RE+Y.RE, X.RE+Y.IM); end; function "*" (X,Y : COMPLEX) return COMPLEX is RP: constant FLOAT := X.RE*Y.RE - X.IM*Y.IM; IP: constant FLOAT := X.RE*Y.IM + X.IM*Y.RE; begin return (RP, IP); end; function RE (X:COMPLEX) return FLOAT is begin return X.RE; end; function IM (X:COMPLEX) return FLOAT is begin return X.IM; end; function "+" (X: FLOAT; Y: COMPLEX) return COMPLEX is begin return (X+Y.RE, Y.IM); end; ... end COMPLEX_TYPE;また、良く出てくるスタックの例で考えてみよう。以下のADAプログラムは、 スタックのインターフェースの定義で、PUSH, POP, EMPTY, FULLの関数が使え ることを示している。
packagte STACK1 is procedure PUSH(X: in INTEGER); procedure POP (X: out INTEGER); function EMPTY return BOOLEAN; function FULL reutnr BOOLEAN; STACK_ERROR : exception; end STACK1;これを使うのには、
declare use STACK1; I, N : INTEGER; begin ... PUSH(I) ; POP(N) ; ... if EMPTY() then PUSH(N); end if; ... end;とする。それぞれの関数の中身については詮索しない。配列で実現されている かも知れないし、また、リストで作っているかも知れない。例えば配列ならば、
package body STACK1 is ST: array (1..100) of INTEGER; TOP: INTEGER range 0..100 := 0; procedure PUSH (X: in INTEGER) is begin if FULL() the raise STACK_ERROR; else TOP := TOP+1; ST(TOP):=X; end if; end PUSH; procedure POP (X: out INTEGER) is begin if EMPTY() then raise STACK_ERROR; else X:= ST(TOP); TOP := TOP-1; end if; end POP; function EMPTY return BOOLEAN is begin reutrn TOP=0; end; fuctnion FULL return BOOLEAN is begin return TOP=100; end; end STACK1;となる。参照する側で STACK1.ST とか STACK1.TOP とかするとコンパイラに 怒られる。
このように、使うべき手続きの名前、受渡しの引数のみを明らかにして、その 内部については詮索しないことを、手続きの抽象化(abstraction)という。
あるデータ型に対して、利用者から見て、その要素の集合、及び要素に対する 演算のみが定義されている時、そのデータ型を抽象データ型 (abstract data type)と言う。そのデー タ型の実現については何の知識もない。前例のSTACK1は抽象データ型である。
(4)オブジェクト指向
最近、いろいろなところでオブジェクト指向という言葉を聞くことだろう。 これにはいろいろな概念を含んでいて、これだという統一的な決まりはないが、 以下の考えが重要。
(i)抽象データ型の考えを進めた計算機構。すなわち、計算を行なうためには、 オブジェクト(object)(プログラム中の定数、変数、ファイル、計算機リソー ス、などなんでも)にメッセージを送ることによって、そのオブジェクトにそ れぞれ定義されている演算(通常メソド methodと呼ぶ)を起動する。
(ii)全てそれぞれに各種の演算を定義するのは大変であるから(例えば、整数 の1、2、3、...それぞれ独立のオブジェクト。それぞれに四則演算、比 較などの演算が必要)、同種のオブジェクトを一まとめにした集合、 クラス(class)を 使う。クラスに対して演算を定義しておいて、そのクラスに属するオブジェク トの操作を制御する。 あるクラスに対して、それに属するオブジェクトをそのクラスのインスタンス (instance)と呼ぶ。 また、クラス定義が与えられているとき、それに属するオブジェクトを新たに 生成することをインスタンス化(instantiation)という。
(iii)通常、クラスには階層(hierarchy)があり、一つの根を持つ木をなす。そ の根が、最上位、葉が最下位のクラスで、上位のクラスのことをsuperclass、 下位のクラスのことをsubclassと呼ぶ。上位のクラスには、より一般的なオブ ジェクトとそれに対する演算が定義され(抽象的)、下位のクラスは、上位の オブジェクトの定義を含んで、更に演算定義などを追加して定義される(具体 化)。このように上位のクラスの諸定義を含むことをクラスの継承(class inheritance)と呼ぶ。 あるクラスのオブジェクトにメッセージを送って演算を起動しようとしたとこ ろ、そこに演算の定義がない場合は、順に上位のクラスに定義がないか調べて いく。 通常、あるクラスの親のクラスは1つであるが、それを複数持てるようにした ものをmultiple inheritanceという。
例: smalltalk Xeroxで開発された最初のオブジェクト指向言語/システム。全ての計算はオ ブジェクトにメッセージを送ることで統一している。
x ← 3 + 4. z ← x + 1 * 5オブジェクト3に +4というメッセージを送って計算をさせてその結果をxに 代入する。次は、xの中身のオブジェクト(7)に +1を送ってできた結果の オブジェクトに*5を送る。したがって (x+1)*5の計算が行なわれるので注意。正方形を書くために、オブジェクトScribeに一連のメッセージを送る。 ここでは、初期状態として、ペン先は、右に向いているという仮定がある。
Scribe penup. Scribe goto: 500@100. Scribe pendn. Scribe goto: 500@400. Scribe go: 300. Scribe turn: 90. Scribe go: 300. Scribe turn: 90. Scribe go: 300.ペン先の動きはこのようになります。
これで、画面の中央下の500、100を左下の頂点とした辺の長さ300の 正方形が書ける。
クラス定義の例: 正方形のオブジェクトを定義したクラスboxの例。
これでboxに対する4つのメソドが定義されている。boxクラスのオブジェクト をインスタンス化するためにはどうするのか?こういう操作のために各クラス に対する高階な操作が定義できるようになっている。すなわち、boxというク ラス定義自体が、一つのオブジェクトであり、それにメッセージを送ることで インスタンス化の操作が行なえる。
![]()
実際に生成するのは、例えば
B2 ← box newAt: 300@200クラス階層の例:boxが、displayObjectというクラスのサブクラスとして定義。 例えば、
![]()
displayObjectでgotoを定義して、shapeはそれを利用。
![]()
![]()
C++について:
C++は、C言語をもとに、オブジェクト指向風に、抽象データ型やクラス定義 を可能にしたもの。Cと良くにている部分が多い。
参考文献:
C++言語入門、小山監訳、アスキー出版 オブジェクト指向プログラミング、石塚、横手、アスキー出版
(あまり分かりやすくないかも…)はやわかりオブジェクト指向、 P. Wegner著、尾内理紀夫訳、共立出版 '92, ISNB4-320-02606-3
以下の例は、複素数クラス complex の使用法とその定義の例。
#include "complex.h" main(){ /* 交流回路の電圧を計算する */ const complex j(0,1); //虚数単位 const double pi=3.1415926535897931; doulbe L= .03, //インダクタンス(ヘンリー) R= 5000, //抵抗(オーム) C= .02, //容量(ファラッド) freq= 60, //周波数(ヘルツ) omega= 2*pi*freq; //角周波数(ラジアン/秒) complex I=12, //電流 Z, //インピーダンス V; //電圧 Z = R +j * omega * L + 1/(j* omega *C) ; V =Z *I; V.print(); }このプログラムの出力は (60000.00, 134.13) クラスの定義は以下の通り。
class complex { double re, im; public: complex(double r=0, doulbe i=0) {re=r; im= i} void print(); friend complex operator +(complex, comlex); friend complex operator *(complex, complex); friend complex operator /(complex, complex); }; void complex::print(){ printf("(%5.2f,%5.2f)\n",re,im); } complex operator +(complex a1, complex a2){ return complex(a1.re+a2.re, a1.im+a2.im); } complex operator *(complex a1 complex a2){ return comlex(a1.re * a2.re - a1.im*a2.im, a1.re*a2.im + a1.im * a2.re); } complex operator /(complex a1, complex a2) { double r= a2.re; /* (r,i) */ double i=a2.im; double ti; /* (tr, ti) */ double tr; tr= r<0? -r:r; ti= r<0? -i:i; if(tr<=ti) { ti= r/i; tr= i*(1+ti*ti); r= a1.re; i=a1.im; } else{ ti= -i/r; tr= r*(1+ti*ti); r= -a1.im; i=a1.re; } return complex(r*ti*i)/tr, (i*ti-r)/tr); }このクラスの値を作るために呼び出される関数をコンストラクタと呼ぶ。クラ ス名称と同じ名前のメンバー関数である。この場合complexである。このクラス の関数、printなどをメンバー関数と呼ぶ。メンバー関数はただ1つのクラス に属し、そのクラスのメンバしかアクセスが許されない。フレンド関数は複数 のクラスのメンバーにアクセスが許される。
プログラム開発過程
プログラムの設計とは一体なんであろうか? 皆さんが今までに作った程度のプログラムでは、その必要性があまり感じられ ないかも知れないが、何万行、何百万行のプログラムを作る、となった場合、 もう少し慎重な姿勢が必要である。通常、プログラムを作ってシステムとして動かしたい、という問題/欲求があっ て、それを基にしてプログラムを作成している。しかし、
問題 -------------------------------->プログラム化されたシステム
というふうに、この間は、ギャップがある。問題から直接プログラムに変換す ることはいろいろ危険が伴う。例えば、問題のあやふやさ、変更の可能性、変 換の難しさ、など。
直接が難しいなら、段階的に、いろいろなステップを経て作業を行なうことにする。
問題--------> 問題を確定する------->プログラムの原図を描く ---- ---> プログラムの詳細を描く-----> 実際のプログラムを作る
このように時間の進行とともに、プログラムの開発作業が進行する様子を表し たものをウォーターフォールモデル(Waterfall Model)と呼ぶ。通常、
1. Requirement 2. Specification 3. Design 4. Coding 5. Testing 6. Maintenance
![]()
の作業に分けることが多い。(別のやり方もあろう)各作業が、もっと詳細な 作業に分割されることもある。そして、各作業で問題が起こった場合には、そ の前の作業、さらにもっと前の作業に戻る。たとえば、Testingで問題が起き れば、Coding、もしくは更に遡ってDesginやSpecificationに戻る。
ソフトウェアシステムが、始めにそれが立案されて、実際に設計、作成されて、 運用されて、やがて使われなくなる、というような一生を、Lifecycle と呼ぶ。
1. 要求定義(Requirement): 開発対象とするプログラムシステムをどのようなも のにするかを確定する。ユーザと協議して、どのような機能をもつシステムに するか明確にして、文章などで書く。これがはっきりしてないために、以後の 開発に重大な支障をきたす場合がある。要求をはっきりさせるために、試作シ ステム(Prototype)を作ってシステムの概要を評価する場合がある(Rapid Prototyping)。
航空機の状態問い合わせシステムの要求仕様の例
- 目的 この仕様の目的は、開発すべき航空機の状態を問い合わせ、それに対応して応 答するシステムへの要求を確立することである。
航空機の状態の問い合わせプログラムの目的は、認められたユーザに対し、ユー ザ・システムの対象の航空機の状態が各型に対する最新の変更動作を決定する ことを許すことである。
- 機能要求
- データ
このシステムは航空機の状態ファイルと動作ファイルの2つのファイルを持つ。 各ファイルは航空機の再信条法を保持するために、日毎に更新される。
日毎に更新される情報は、格納庫に保持されている各型毎の航空機の数と運用中の 航空機の数、そしてこれらの変更である。
変更としては、変更の型(航空機の追加、削除、動作中から修理への状態の変 化など)と変更日時が含まれる。- プログラム
プログラムは航空機の各型の状態問い合わせを受け、実時間での適当な応答を 生成する。問い合わせは、正当な標識番号と航空機の状態ファイルへのアクセス するための正当な認可番号をもつユーザによってなされなければならない。
各問い合わせは、ある型の航空機の状態もしくは、動作の最新情報を要求する。
正当な標識番号は、…である。また、正式な認可番号は…である。2. 仕様記述(Specification): 定義された要求をもとに、プログラムの動作、 機能がどんなものか、ユーザ、開発者に分かるよう、図、仕様記述言語などで 記述する。以後のプログラム開発は、この仕様に基づいて行なわれる。この段 階で、十分、出来上がるシステムの動作、機能の確認を行なっておけば、以後 の作業がスムーズに進む。
type Queue [item] declare NEWQ() -> Queue ADDQ(Queue, item) -> Queue DELETEQ(Queue) -> Queue FRONTQ(Queue) -> item ISNEWQ(Queue) ->Boolean APPENDQ(Queue, Queue) -> Queue; for all q, r : Queue, i : item let ISNEWQ(NEWQ) = true ISNEWQ(ADDQ(q, i))= false DELETEQ(NEWQ) = NEWQ DELETEQ(ADDQ(q, i)) = if ISNEWQ(q) then NEWQ else ADDQ(DELETEQ(q), i) FRONTQ(NEWQ)=UNDEFINED FRONTQ(ADDQ(q, i))= if ISNEWQ(q) then i else FRONTQ(q) APPENDQ(q, NEWQ) = q APPENDQ(r, ADDQ(q, i)) = ADDQ(APPENDQ(r, q), i) end end Queue QUEUEの代数的な仕様3. 設計(Design):プログラムの構造を決める。モジュールの構造、機能の分割/ 構成を行なう。抽象的に記述したものや、具体的に記述したものなど、複数の レベルに分けて設計を行なうことも多い。実際のプログラミングに用いる言語 に近い疑似言語を用いることがある。
![]()
4. コーディング(Coding):プログラムの作成。
5. テスト(Testing):プログラムが正常に動くかどうかの確認。ユニットテス ト、統合テスト、システムテストなど、いくつかの段階に区別することあり。
6. 保守(maintenance):作成したシステムを実際に運用している間に出てきた バグを修正したり、また、要求される機能に変更が生じた時にプログラムの追 加、修正などを行なう。大幅な変更を要求される時は、新たに、別のシステム を作成する時のように、要求定義から始めることもある。
電報解析問題
つぎの文章で与えられる自然語の記述から、要求定義、仕様記述、設計、コー ディング、等の作業を行なう例題。
電文列を解析するプログラムを作成したい。この電文列は、英字、数字、空白 の列として装置上に与えられる。これをあらかじめ定められた大きさのバッファ に読み込みそこで解析する。電文の中の単語は1個以上の空白によって区切ら れている。各電文は単語ZZZZで区切られる。電文列は空文、つまり、単語をまっ たく含まない電文によって終る。それぞれの電文に対して、課金できる単語数 を数え、長過ぎる単語を調べる。単語ZZZZとSTOPは課金せず、12文字よりも 長い単語は長過ぎる単語である。解析結果として、単語数と長過ぎる単語が存 在するかどうかを示し、電文を奇麗に印刷する。 (P. Henderson and R. A. Snowdon, An experiment in structured programming, BIT, 12, 38-53, 1972).
要求定義:
この文章の中から得られる要求としては、*電文列を解析するプログラムを作成したい。
*この電文列は、英字、数字、空白の列として装置上に与えられる。
*これをあらかじめ定められた大きさのバッファに読み込みそこで解析する。
この場合は、問題がすっきりとした文章でまとめられている。ここに至るまで が大変である。この場合は、要求定義/仕様記述の大方の労力は使わなくて良 い例である。
仕様記述:
[1]電文の中の単語は1個以上の空白によって区切られている。
[2]各電文は単語ZZZZで区切られている。
...
{演習問題:仕様記述として使える文章の断片を全て探してみよう。}
しかし、これらの抽出された仕様をよく検討してみると、いろんな問題を含ん でいることがわかる。例えば、「美麗な」出力とは?、STOPの役割は? などが不明である。実際の開発では、こういうことを洗い出して、客などに問 い合わせて確認することが大切。
仕様をもう少し、形式的な表現で行なうことを考える。例えば、
[1'] word_stream := null | word, space_list, word_stream word := non-space, non-space__list non-space := A | B | ... | Z | 0 | 1 | ... | 9 | non-sp_list:= null | non-space, non-sp_list space_list := space, sp_list sp_list := null | space, sp_list
など。ただ、このままでは、数に関する条件など書きづらい(例えば12文字 を越えるなど。書けないことはないが)ある状態Sの時、単語の中の文字数を 数えるカウンタcountを考えて、入力される各文字chに対してどうカウンタが 動くかについて定義すると
[1''] count(input(S)) = if (ch == ' ')then 0 else count(S)+1このように、入力一文字ごとに対する状態遷移を定義して、全体の仕様を記述 する方法もある。
こういうふうな整理をして、まとめて状態遷移図を書くこともいい方法だろう。
{演習問題:状態遷移図を工夫して書いてみよ。いくつかの図に分けると考え やすい。}
設計:
前段階で得られた仕様を基にして、プログラムの構造を考える。例えば、状態 遷移で仕様を記述したならば、それに基づいて、プログラムの処理の流れを定 義したり、モジュールの構造を決めたりする。例えば、先ほどの例で、単語を 処理するモジュール、電文を処理するモジュールなどの関係は、
電文列の処理モジュール | 一電文の処理モジュール | 一単語の切りだしモジュール
とするとよい。また、例えば一単語の切りだしモジュールの中は、
Procedure word begin while(ch is space) {* Here we are in off-word state*} advance to next character and get it into ch; while(ch is non-space) {* Here we are in in-word state*} begin increment WC; advance to next character and get it into ch; end; endとすればよいことがわかる。ただし、これは、まだ、完全なプログラムになっ ていない。入出力方法、大域変数の扱い、最初に文字を誰が読み込むか、ファ イルの終りの処理などなど。こういう点で、わからないことが出てくると、も との仕様をチェックしたり、顧客に問い合わせたりする必要が出てくる。
プログラム設計の例題
酒屋問題この問題は、一つの対象となる問題を決めて、それに対していろいろな方法/ 技法を用いてプログラムの作成を行ない、それそれの方法/技法の比較検討を 行なうために設計されたもの。
二村、雨宮、山崎、淵、新しいプログラミング・パラダイムによる共通問題の 設計、情報処理 Vol.26, No.5, pp.458-459 (1985-5).
酒屋の倉庫の受け付け係の仕事を行なうプログラムを作成する問題。実際にい ろいろな方法で解かれている。例えば特定のプログラミング言語によるものは、
並列オブジェクト指向言語、Concurrent Prolog, 属性文法、モジュラプロ グラミング言語、ストリーム言語、関数型言語、
また、設計法によるものとしては、
複合設計、構造化プログラミングワーニエ法、ジャクソンシステム開発法、 系統的ソフトウェア設計法、データ構造に基づく設計法、PAD/PAMによる方 法、MOTHER SYSTEMによる設計、構文と意味の記述によるプログラムの作成、 変換法プログラミング
などが実際に行なわれている。
酒屋問題
ある酒類販売会社の倉庫では、毎日数個のコンテナが搬入去れてくる。その 内容はビン詰めの酒で、1つのコンテナには10銘柄まで混載できる。扱い銘 柄は約200種類ある。倉庫係は、コンテナを受取りそのまま倉庫に保管し積 荷票を受付係へ手渡す。また受付係からの出庫指示によって内蔵品を出庫する ことになっている。内蔵品は別のコンテナに詰め替えたり、別の場所に保管す ることはない。 空になったコンテナはすぐに搬出される。 積荷票:コンテナ番号(5桁) 搬入年月、日時 内蔵品名、数量(の繰り返し) さて受付係は毎日数10件の出庫依頼を受け、その都度倉庫係へ出庫指示書 を出すことになっている。出庫依頼は出庫依頼票または電話によるものとし、 1件の依頼では、1銘柄のみに限られている。在庫がないか数量が不足の場合 には、その旨依頼者に電話連絡し、同時に在庫不足リストに記入する。そして 当該品の積荷が必要量あった時点で、不足品の出庫指示をする。また、空にな る予定のコンテナを倉庫係に知らせることになっている。 出庫依頼:品名、数量 送り先名 受付係の仕事(在庫なし連絡、出庫指示書作成および在庫不足リスト作成) のための計算機プログラムを作成せよ。 出庫指示書:注文番号 送り先名 コンテナ番号* 品名、数量* 空コンテナ搬出マーク* (*これら3つの繰り返し) 在庫不足リスト:送り先名 品名、数量 ・なお移送や倉庫保管中に酒類の損失は生じない。 ・この課題は現実的でない部分もあるので、入力データのエラー処理などは簡 略に扱っても良い。 ・以上あいまいな点は、適当に解釈して下さい。構造化分析、構造化設計法
最近、はやりのCASE(Computer Aided Software Engineering)というのは、 ほとんどがこの構造化分析(Structured Analysis)、構造化設計法(Structured Design)(まとめてSA/SDとよぶことが多い)に基づいている。この方法 は、与えられた仕様書からデータフローダイアグラムDFDという仕様書を 作成する過程(SA)と、このDFDからモジュール構造を表すシステムの設 計図を作成する過程(SD)とから成り立っている。 このDFDやシステム設計書の書き方が重要である。しかし、必ずこうだか らこうなる、といったような工学的な手法ではなく、ノウハウ的な約束事がい ろいろあって、それに従って作業を進める。以下、SA/SDの幾つかのキー ポイントをまとめる。詳細は、別紙、資料参照。(1) データの流れを表すDFDは階層的に作成される。最上位のものをコ ンテキストダイアグラムと呼んで、システムと外部とのデータの流れを定義す る。上位のレベルDFDと下位のレベルDFDのデータフローは、対応がとれ ていなければいけない。
(2)DFD中の矢印で表される各データは、その内容をデータディクショナ リで定義する必要がある。
(3)最下位のDFDのバブルの処理は、ミニ仕様書で定義する。
(4)得られたDFDを基にして、モジュール構造図を作る。DFDの中で、 入力処理をしている部分、中心的な作業をしている部分、出力処理をしている 部分をそれぞれ見つけ出し、順次処理を行なうようにモジュールを配置する。 この配置には一般に自由度がある。
(5)選択的な実行を行なうようなDFDに対しては、トランザクション分析 を用いて、分岐制御を行なうモジュールを設ける。
...
[演習] 先ほどの酒屋の共通問題に対してSA/SDを適用してDFD、モ ジュール構造図を作ってみよ。
ジャクソンシステム開発法
この方法(Jackson System Development)は、イギリスの M. A. Jackson が、 提案したプログラム開発法で、以前、自分で提案した、ジャクソン構造的プロ グラミング(Jackson Structured Programming)を基にしている。この方法は、 SA/SD法に比べて、開発過程のより幅広い部分をより詳細に担当する。すなわ ち、対象となる仕様記述から対象(Entity)、事象(Action)となる言葉の選び出 し方、そこからの、構造図の作成の仕方、また、最終的にプログラムの骨格を 生成する過程などを含んでいる。 詳細は別紙の解説記事参照。
オブジェクト指向設計法
最近、急速に広まりつつある方法。G. Booch らが提唱した方法。既存のプロ グラム設計法では、主にプログラムのモジュールの階層構造を出力とすること を目指しているが、そういうふうな、通常のサブルーチン、親プログラムの関 係に馴染みにくいソフトウェアシステムの設計に便利。個々のプログラムのモ ジュールに最終的に対応するオブジェクトを仕様から捜し出し、そのオブジェ クト間の関係、およびオブジェクトの行なうオペレーションを定義し、それか らオブジェクト指向言語の例えばクラス定義を得る。 詳細別紙。
ソフトウェアのテスト
参考書:玉井他著:ソフトウェアのテスト技法、共立出版ソフトウェアのテストは、その品質を保つための重要な作業である。学生実験 のプログラムでは、通常、チョコチョコっと動かしてみて、大体正常な動作が 確認できれば、もう、それで出来たような気になるが、多数のユーザに対して 出荷するソフトウェアは、非常に大きな手間をかけて、十分なテストを行なう。 これが、十分でなく、実際にユーザが使っている時に重大な障害が発生すると、 ある時には社会問題にまで発展する時がある。
用語 故障(failure) -- テスト、運用時に現れる不正な動作 欠陥(fault) -- 故障を引き起こすプログラム上の誤り、バグ エラー(error) -- 欠陥を生じさせた人間の間違った行動大きなプロジェクトになると、システムの設計の開始と同時に、テストの計画 をたてる場合が多い。また、専門のテストのみを行なうチームや人を配置して、 開発者とは独立に行なうことがある。テストは、プログラム作成者が主に行なうモジュール単位の単体テスト、各モ ジュールを集めてテストを行なう統合テスト、そして、最終的にユーザの環境 で行なう受け入れテスト、という順で作業が進む。
テストは、プログラムの品質(quality)を確かめるための手段である。品質と は、通常、
テストをそのやり方 で分類すると、ブラックボックステスト、ホワイトボックステストに分けられ る。
・ブラックボックステスト:プログラムの内部構造を見ずに、与えられた仕様を 基に、入出力の関係だけをテストする方法。正攻法、手間大 同値分割法など
・ホワイトボックステスト:上とは逆に、プログラムの内部の構造に着目してテ ストデータを生成する方法。仕様から要求されるものが落ちる可能性あり。効 率よし。
・カバレッジによるテストデータの選択
C0:全ステートメント中で何パーセントのステートメントが実行されたか
C1:全分岐方向で何パーセントの分岐が実行されたか
カバレッジを計測するツールあり。
・変異テスト(mutation test)
プログラムの一部を、わざと機械的に変更し、その変更結果がテストで検出で きるかどうかでテストデータの善し悪しを判定する方法。
・エラー数の推定
過去の同種のプロジェクトデータから出るエラーの数を推定したり、また、信 頼度成長曲線によって今、どの辺まで虫出しを行なっているか推定する。