このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
データ構造とアルゴリズム I では様々なプログラムの定石を学びました。 一方、本講義では、自分で作成したプログラムのルールを実際にプログラムにするよ うな定石を学びます。
この授業では、
この講義を習得すると、さまざまなアプリケーションプログラムを作れるよう になります。 また、各種ソフトウェアの試験(資格試験、大学院入試)などの試験範囲にもあ たります。 但し、もっとも基本的なことしか取り上げませんので、 他の難関大学院の受験をする場合は適宜補う必要があります。
プログラムは様々なデータ構造に対して複数の操作を指示することによりアル ゴリズムを記述します。 その結果、目的の処理を行わせることができます。 ここで、全ての処理を曖昧無く記述されたものはプログラムその物でしかあり ませんが、「データの関係」や、「処理の流れ」など、特定の切り口でプログ ラムの一部の構造を理解したい場合があります。
例えば、「Web での買い物サイトにおいて、ユーザの情報を追加したい」など、 機能の拡張などを考えたとします。 この時、もともとの買い物サイトのプログラムを全て読んで理解しようとして も、実際はユーザ情報の処理に関わっているのはプログラムは一部分でしかあ りません。 そのため、効率よくプログラムの修正などを行うには、データやプログラムの 構造、データ間の関連、処理の流れ、責任分担などの切り口で説明された文書 が必要になります。
例えば、今までは買い物ごとに商品の配送先をユーザに入力させていたのを、 ユーザ情報としてユーザのデータベースに保存し、配送先入力で扱えるように したいとします。 この時、プログラムを修正するのに様々な情報が必要になります。
これらを全てプログラムを読みながら決めるのは大変です。 そのため、これらを検討するために必要な書類を作っておくと、プログラムの 保守、修正などが容易になります。
UML(Uniformed Modeling Language 統一モデリング言語)は オブジェクト指向のソフトウェアを設計、記述する際に使う、グラフィカルな 記述法です。 オブジェクト指向のプログラミングにおいて、 UML は様々な利用法があります。 まず、設計をする際に構想などをまとめたりすることができます。 さらに既存のソフトウェアを解析するときなどにも使えます。 これらはアイディアを記述し、ソフトウェアの理解を深めるものです。 また、設計図面として利用することもできます。 つまり、ソフトウェアのドキュメントとして活用できるということです。 さらに、CASE(Computer Aided Software Engineering) ツールを 介して UML によるプログラミングもできます。 NetBeans や Eclipse などのエディタで、 UML を利用して Java のプログラ ミングを支援するようなツールを使うことができます。
UML 2.0 では 13 種類の図があります。 13種類の図は、構造を表すもの(構造図)と、振る舞いを表すもの (振る舞い図) に分類できます。 さらに、振る舞い図の中にはオブジェクト同士の関係を表す 相互作用図として分類できるものがあります。 13 種類の図の分類を下記に示します。
分類 | 名称 | 役割 | |
---|---|---|---|
構造図 | クラス図 | クラスの特性や関係 | |
オブジェクト図 | 実際のインスタンスの構 造 | ||
パッケージ図 | プログラムのコンパイルにおける依 存関係など | ||
配置図 | システムにおけるオブジェクトなどの配 置 | ||
コンポーネント図 | システムの機能毎の分類と関連 | ||
コンポジット構造図 | クラスの内部構造の階層的表現 | ||
振る舞い図 | ユースケース図 | ユーザとシステムの関わり合い | |
状態マシン図 | システムの振る舞いの記述 | ||
アクティビティ図 | フローチャートのような並列的 な手続きの流れ | ||
振る舞い図 | 相互作用図 | シーケンス図 | 各オブジェクト間でのメッセージのやり とりの流れ |
コミュニケーション図 | シーケンス図と同様だが、 オブジェクトの配置を自由にできる | ||
相互作用概要図 | アクティビティ図+シーケンス図 | ||
タイミング図 | ソフトウェアのタイミングチャート |
上記の全ての図をマスターする必要はありません。 必要な状況に応じて活用すれば充分です。 また、本資料では上記のうち、赤で示した図のみを紹介します。 このうち特に重要なのが、クラス図とシーケンス図です。
古くは、フローチャート、変数表、関数表などがこの役割を担ってましたが、 多くの専門家が時間をかけて洗練してきたため、UML は従来の記述に比べて記 述性などが高くなっています。
付録のサンプルプログラムの構造を表現する図表を作 成することを考えます。
オブジェクト図はプログラムのある一点での、オブジェクトの関連を表す図で す。 これは、オブジェクトのメンバが他のオブジェクトを参照しているという関係 を、オブジェクトを面積のある図形(矩形)で書き、参照を矢印で表すことによ り記述できます。 但し、表記を統一するため、UML では矢印などの記法にいくつかのルールを設 けています。
メソッド内で他のオブジェクトを単にローカル変数の様に使用する場合 は関連と言い、単なる矢印で表します。
class A {
public void f(){
B b = new B();
}
}
class B {
public B(){}
}
class Rei {
public static void main(String[] arg){
A a = new A();
a.f(); // この図
}
}
オブジェクト内に他のクラスのオブジェクトをメンバ変数として持つのを、 集約(Aggregation)と言います。 これは収納される側は矢印ですが、収納する側は白抜きの菱形が付いた線で表 します。
class A {
private B b;
public void f(){
b = new B();
}
}
class B {
public B(){}
}
class Rei {
public static void main(String[] arg){
A a = new A();
a.f(); // この図
}
}
オブジェクトを生成する時に他のクラスのオブジェクトを同時に生成し、メソッ ドを透過的に使用するのをコンポジション(Composition)と言います。 これは収納される側は矢印ですが、収納する側は黒塗りの菱形が付いた線で表 します。
class A {
final private B b;
public A(){ b = new B();}
public void f(){
b.f();
}
}
class B {
public B(){}
public void f(){}
}
class Rei {
public static void main(String[] arg){
A a = new A(); // この図
}
}
サンプルプログラムの Ex162 クラス の test メソッドの各行に対してオブジェクト図を書く と下記のようになります。
final Player player1 = new Player("Kaiji");
player1.setTactics(new Tactics1(hb));
final Player player2 = new Player("Funai");
player2.setTactics(new Tactics2(hb));
final Box box = new Box();
box.setPlayer1(player1);
box.setPlayer2(player2);
このように表すとプログラムの一瞬一瞬でデータ構造やアルゴリズムを明かに することができます。
しかし、これでは具体的過ぎて大きなプログラムの構造を説明するのに不向き です。 そのため、これよりも情報量を減らして、抽象化する必要があります。
さて、プログラムの具体的な動きを無視してオブジェクト間の関係を明らかに することを考えます。 このような文書があれば、データ変更などの仕様変更をする際など、プログラ ムの修正部分を把握することができるようになります。
これはつまり、プログラムの一瞬一瞬を捉える話ではなく、プログラムの動作 を総合的に見て参照しうる関係にあるかどうかを考えることにします。 このようにすると、これは個々のオブジェクトの性質、つまり、クラスの構造 を表すことになります。 そこで、クラス A がクラス B のメンバ変数を持つ場合、クラス間の関係とし て [A] → [B] のように書きます。
但し、クラス図は UML において最もよく使われる図なので、様々な詳細な記 法が定められています。 整理して順に紹介しますが、かなり複雑です。 なお、クラスの表記法として、既に学んだ中に CRC カードがあります。 これは、クラスの役割と関係を明確にするために仕様書に出てくる言葉を整理 して作成するものです。 これに似せるため、クラス図も矢印で関係を示すだけではなく、長方形の中に クラス名、メンバ変数名、メソッド名を書きます。
一番上の欄はクラス名、二番目の欄は属性(メンバ変数)、 三番目の欄は操作(メソッド)を表します。 まず、メンバ変数の記法ですが、次の様に定められています。
可視性 名前: 型 多重度 = デフォルト値{プロパティ文字列}
可視性は +(プラス) が public, -(マイナス) が private です。
多重度は配列変数のように[](角カッコ)で表現し、最小要素数と、最大要素数
を .. (ピリオド二つ)で区切って記述します。0 または 1
は 0..1
で表します。
無制限の場合は *(アスタリスク) で表現します。
なお、この属性の表現において、必須なのは名前だけです。
例えば、クラス A がメンバ変数としてクラス B のプライベート変数 b を一 個だけ持っていたとすると、[A]-b→1[B] のように表します。 一方、ローカル変数などで一時的に参照するなど薄い関係は単に点線の矢印で [A]-->[B] のように表します。
操作の書式は次の通りです。
可視性 名前(パラメータリスト): 戻り値の型 {プロパティ文字列}
パラメータリストの書式は次の通りです。
方向 名前; タイプ = デフォルト値
ここで、方向は in , out, inout ですが、無指定だと in と見なされます。
プログラム例とクラス図の対応を示します。
class A {
private int x;
public int getX(){return x;}
public void setX(int n){x=n;}
}
class A {
}
class B {
private A a;
public B(){}
}
class C {
final private B b;
public C(){
b = new B();
}
}
abstract class A {
abstract public void f();
}
class B extends A{
public void f(){}
}
interface A {
public void f();
}
class B implements A{
public void f(){}
}
上記では、属性は名前と型を記述するように書きましたが、もう一つ指定方法 があります。 それは、他のクラスのオブジェクトを持つ場合に、そのクラス(これも長方形 で書かれています)との間に線を引くというものです。 [クラスA]→[クラスB] のように書かれていたら、これはクラス A がクラス B のオブジェクトを含むと言えます。 また、いくつ含むかは数を矢印に記述します。 上記のクラスはつぎのようにも書けます。
さらに、メソッドなどを引き継いで、クラスを拡張するコンポジションにおい ては、始点に黒い菱形を書きます。 また、継承は白い三角の矢印を書きます。 上記の例では InputStream クラスへ白い矢印が引かれます。
付録のサンプルプログラムのクラス図を示します。
オブジェクト指向言語では、データの管理単位はオブジェクトクラスになるの が普通ですが、さらにクラスを管理したり、システムを管理したりと、上位の 構造の管理が必要になることがあります。 そのため、それに応じた図式のルールが定められています。 それが、パッケージ図と配置図 になります。 複数のパッケージを使用する際にはパッケージ図を書きます。 また、複数のノードがネットワークで接続されている時に、データの関係を示 すのに配置図を書きます。
次に複雑な動作を図式を使って説明することを考えます。 例として、Web のショッピングモールを考えます。 このショッピングモールの分析に UML の振る舞い図を利用します。
始めに、ショッピングモールに関わる役割を分析することにしましょう。 ショッピングモールは利用者だけでは成り立ちません。 ショッピングモールを構築するには様々な業務があります。 プログラミングの他に、画面のデザイン、データの入力、サーバの管理などで す。 そのため、ユースケース図では、考えられる様々な仕事をユースケー スとして、楕円の中に記述します。 そして、その仕事をする人をアクターと呼び、各ユースケースに 関連付けます。 なお、通常ユースケースはサブジェクトと呼ぶ長方形の中に入れ、 アクターは外側に配置します。 複数のシステムなどを分析する場合は、複数のサブジェクトになります。
Web ショッピングモールを分析するとこのような図になります。 このように分析すると、仕事の並列化や、業務の工数分析、管理者用のツール の作成、権限や責任の分化などを行うことができます。 これは、さらにテストケースの作成などにも役立ちます。
状態マシン図はオートマトンと似ています。 特定の状態において、外部の入力により、次にどの状態になるかを矢印で記述 します。
多くの Web ページでは、利用者の入力で画面が変化します。 そのため、各画面を状態とみなすことができます。 Web ショッピングモールにおいて、 各画面から、どのような入力で次の画面にいくかを分析したのが、この図になり ます。
フローチャートに対応するのがこのアクティビティ図です 各処理を上から下、あるいは左から右に記述していきます。 大きな特徴として並列処理を記述するための記法が定められています。 この他、ポリモーフィズムを記述する方法なども決められています。
なお、 フローチャートでは命令をそのまま記述したりすることがありましたが、 アクティビティ図では抽象度を保つために処理名を記述します。
次の認証の仕組みを分析したのがこの図になります。
シーケンス図はオブジェクト相互のメッセージのやりとりを表すものです。 アルゴリズムのうち、メッセージのやりとりの関係を記述できます。 これはオブジェクト図において、オブジェクトを縦長の長方形で表して、関連 を時系列に記述したものになります。
始めに、 一番左に、メッセージを受け取るオブジェクト名(a + クラス名など)、その右 には使用するオブジェクト変数を四角で囲んで並べます。 そして、各変数の箱から下に向かって点線の縦線を引きます。 これを生存線と言います。 そして、実際に変数などを使用する場合は下に長細い長方形を書いて active であることを示します。
そして、メッセージを送るオブジェクトから送られるオブジェクトに矢印とメ ソッド名を書き、 もし、戻り値があったら、その型を書いた矢印を受け取る変数へ書きます。
プログラムとシーケンス図の対応を示します。
public void f(A a){
}
A a = new A();
int a = b.c(d);
for(int i=0; i<max ; i++){
if(a.isGood()){
b.win();
}else{
b.loose();
}
}
付録のサンプルプログラムのEx162 クラス の test メソッドのシーケンス図を示します。
UML はプログラム開発やプログラムのドキュメントなどのために作画されます。 このうちプログラム開発においてはディスカッションで用いるため、清書より も自由度が重要になります。 そのため、ホワイトボードなどに手書きで使用するのが主な使用方法になりま す。
なお、有料のソフトの中には UML をプログラミング支援(CASE Computer Aided Software Engineering) を行えるものもあります。 プログラムに変換するのを「出力」と言ったり、プログラムから図 を作成するのを「リバースエンジニアリング」と言ったりします。
一方、作成したプログラムから UML を作画してプログラムのドキュメントと して残す場合は、清書する必要があります。 そのため、様々なツールがあります。 但し、既存のプログラムから UML を生成するようなフリーのツールはまだ機 能が十分ではなく、自動的な生成は今のところ期待できません。
UML の図は基本的には長方形と矢印でできているので通常のドローソフトなど でも作図は困難ではありません。 但し、菱形の矢印や、矢印の上下に数値などを記入するため専用のソフトウェ アの方が利便性は上です。 なお、この教材の上記の UML の記述は汎用のドローソフトの Dia で行ってい ます。 このドローソフトは UML 用の基本図形が既に登録されています。また矢印に 数値などを記入することもできます。
UML の作画に専用のソフトウェアを使用すると、さまざまなソフトウェアによ る支援が可能です。 但し、これ専用なので使用方法を新たに覚えるなどの手間がかかります。 有名な専用ソフトのうち、有料なのは Enterprise Architect (http://www.sparxsystems.jp/) です。 一方、 無料の Aster(旧 Jude) Community http://astah.change-vision.com/ja/index.html は、ユーザ登録が必要ですが、機能が豊富です。
前半で紹介した UML のうち、いくつかは Eclipse のプラグインとして利用可 能です。 これは、図を作ると Java のプログラムソースを出力するような フォワードエンジニアリングのほか、既存のソースコードから図 を作成するリバースエンジニアリングも可能にしています。 Eclipse Wiki によると、 UML ツールには以下のようなものがあります。
このうち、 AmaterasuUML http://amateras.sourceforge.jp/cgi-bin/fswiki/wiki.cgi は比較的シンプルで、作成した Java のプログラム から、ドラッグ&ドロップでクラス図が作成できるなど、 シンプルですが、利用価値は大きいです。
import java.util.Date;
import java.util.ArrayList;
interface Hand {
int wins(Hand h);
int getType();
}
class Hand1 implements Hand {
Hand1(int hand){
this.hand=hand;
}
private int hand;
@Override public String toString(){
String result;
switch(hand){
case 0:
result="Gu";
break;
case 1:
result="Choki";
break;
case 2:
result="Pa";
break;
default:
result="";
}
return result;
}
public int getType(){
return hand;
}
public int wins(Hand h){
int result;
if(hand==h.getType()){
return 0;
}
if((hand+1==h.getType())|| ((hand==2) && (h.getType()==0))){
return 1;
}
return -1;
}
}
interface HandBuilder {
Hand getInstance(int hand);
}
class HandBuilder1 implements HandBuilder {
public Hand getInstance(int hand){
if(hand <0 || hand>2 ){
return null;
}
return new Hand1(hand);
}
}
class HandBuilder2 implements HandBuilder {
private Hand[] hands;
public Hand getInstance(int hand){
if(hand <0 || hand>2 ){
return null;
}
if(hands==null){
hands = new Hand[3];
for(int i=0; i<3; i++){
hands[i] = new Hand1(i);
}
}
return hands[hand];
}
}
interface Tactics {
void setHandBuilder(HandBuilder hb);
Hand getHand();
}
class Tactics1 implements Tactics {
public Tactics1(HandBuilder hb){
this.hb = hb;
}
private HandBuilder hb;
public void setHandBuilder(HandBuilder hb){
this.hb = hb;
}
public Hand getHand(){
return hb.getInstance((int)(Math.random()*3));
}
}
class Tactics2 implements Tactics {
private int counter;
public Tactics2(HandBuilder hb){
this.hb = hb;
counter=0;
}
private HandBuilder hb;
public void setHandBuilder(HandBuilder hb){
this.hb = hb;
}
public Hand getHand(){
Hand hand = hb.getInstance(counter);
counter = (counter+1)%3;
return hand;
}
}
class Player {
private String name;
public Player(String name){
this.name = name;
win=0;
loose=0;
draw=0;
}
private Tactics tactics;
public void setTactics(Tactics t){
tactics = t;
}
public Hand getHand(){
return tactics.getHand();
}
public String result(){
return name+": "+ win +" win(s), "+loose+" loose(s), "+draw+" draw(s)";
}
private int win,loose,draw;
public void win(){
win++;
}
public void loose(){
loose++;
}
public void draw(){
draw++;
}
}
class Box {
private Player player1, player2;
public Box(){}
public void setPlayer1(Player p){
player1 = p;
}
public void setPlayer2(Player p){
player2 = p;
}
public Hand[] examine(){
Hand[] result = new Hand[2];
result[0]=player1.getHand();
result[1]=player2.getHand();
switch(result[0].wins(result[1])){
case -1:
player1.loose();
player2.win();
break;
case 0:
player1.draw();
player2.draw();
break;
case 1:
player1.win();
player2.loose();
break;
}
return result;
}
}
class StopWatch {
private Date now;
public StopWatch(){
now = new Date();
}
@Override public String toString(){
final Date next = new Date();
double result = (double)( next.getTime()-now.getTime())/1000;
now = next;
return String.valueOf(result);
}
}
class Ex162 {
public static void main(String[] arg){
final HandBuilder hb = arg[0].equals("1")
? new HandBuilder1()
: new HandBuilder2();
final StopWatch sw = new StopWatch();
test(hb);
System.out.println(sw);
}
private static void test(HandBuilder hb){
final Player player1 = new Player("Kaiji");
player1.setTactics(new Tactics1(hb));
final Player player2 = new Player("Funai");
player2.setTactics(new Tactics2(hb));
final Box box = new Box();
box.setPlayer1(player1);
box.setPlayer2(player2);
final ArrayList<Hand[]> list = new ArrayList<Hand[]>();
for(int i=1; i<=1000000; i++){
final Hand[] result = box.examine();
list.add(result);
}
System.out.println(player1.result());
System.out.println(player2.result());
}
}