UMLについて(ソフトウェア工学)
UML(Unified Modeling Language)記法が生まれるまで
巨大なプロジェクトでプログラムを作る場合、対象となるシステムを概念として表現する場合、オブジェクト指向分析(OOA: Object Oriented Analysis)やオブジェクト指向設計(OOD: Object Oriented Design)とよばれるソフトウェア開発方法が重要となる。(総称して OOAD – Object Oriented Analysis and Design)
これらの開発方法をとる場合、(1)自分自身で考えを整理したり、(2)グループで設計を検討したり、(3)ユーザに仕様を説明したりといった作業が行われる。この時に、自分自身あるいはチームメンバーあるいはクライアントに直感的に図を用いて説明する。この時の図の書き方を標準化したものが UML であり、(a)処理の流れを説明するための振る舞い図(以前であればフローチャートやPAD)と、(b)データ構造を説明するための構造図を用いる。
UMLは、ランボーによるOMT(Object Modeling Technique どちらかというとOOA中心)と、 ヤコブソンによるオブジェクト指向ソフトウェア工学(OOSE)を元に1990年頃に 発生し、ブーチのBooch法(どちらかというとOOD中心)の考えをまとめ、 UML(Unified Modeling Language)としてでてきた。
UMLでよく使われる図を列記すると、以下の物が挙げられる。
- 構造図
- クラス図
- コンポーネント図
- 配置図
- オブジェクト図
- パッケージ図
- 振る舞い図
- アクティビティ図
- ユースケース図
- ステートチャート図(状態遷移図)
- 相互作用図
- シーケンス図
- コミュニケーション図(コラボレーション図)
UMLを正しく使うことができるようになれば、UMLで仕様書を書けばそれがそのままプログラムになることが理想的な姿かもしれない。ソフトウェア開発やソフトウェアの保守にソフトウェアツールを利用することは、CASE(Computer Aided Software Engineering)と呼ばれ、そのようなツールをCASEツールと呼ぶ。地元福井の永和システムマネジメントでは、astar* というCASEツールを開発している。
UMLの構造図の書き方の説明。 詳しくは、参考ページのUML入門などが、分かりやすい。
クラス図
クラス図は、構造図の中の基本的な図で、 枠の中に、上段:クラス名、中段:属性(要素)、下段:メソッド(関数)を記載する。 属性やメソッドの可視性を示す場合は、”-“:private、”+”:public、”#”:protected 可視性に応じて、”+-#”などを記載する。
関連
クラスが他のクラスと関係がある場合には、その関係の意味に応じて、直線や矢印で結ぶ。
(a)関連(association):単純に関係がある場合、
(b)集約(aggregation):部品として持つが、弱い結びつき。関係先が消滅しても別に存在可能。(has-a)
(c)コンポジション(composition):部品として持つが強い結びつき。関係先と一緒に消滅。(has-a)
(d)依存(dependency):依存関係にあるだけ
(e)派生(generalization):派生・継承した関係(is-a)
(f)実現(realization): Javaでのinterfaceによる多重継承
上図の例では、乗り物クラスVehicleから自動車Carが派生し(CarからVehicleへの三角矢印―▷)、 自動車は、エンジン(Engine)を部品として持つ(EngineからCarへのひし形矢印―◆)。エンジンは車体と一緒に廃棄なら、コンポジション(C++であれば部品の実体を持つ)で実装する。
自動車は、同じく車輪(Wheel)を4つ持つが、自動車を廃棄してもタイヤは別に使うかもしれないので、集約(部品への参照を持つ)で実装する(WheelからCarへのひし形矢印―◇)。 集約で実装する場合は、C++などであれば、ポインタで部品を持ち、部品の廃棄(delete)は、別に行うことになる。
Javaなどのプログラム言語では、オブジェクトはデータの実体へのポインタで扱われるため、コンポジションと集約を区別して表現することは少ない。
is-a 、has-a の関係
前の課題でのカモノハシクラスで、羽や足の情報をどう扱うべきかで、悩んだ場合と同じように、 クラスの設計を行う場合には、部品として持つのか、継承として機能を持つのか悩む場合がある。 この場合には、“is-a”の関係、“has-a”の関係で考えると、部品なのか継承なのか判断しやすい。
たとえば、上の乗り物(Vehicle)クラスと、車(Car)のクラスは、”Car is-a Vehicle” といえるので、is-a の関係。 “Car is-a Engine”と表現すると、おかしいことが判る。 車(Car)とエンジン(Engine)のクラスは、”Car has-a Engine”といえるので、has-a の関係となる。 このことから、CarはVehicleからの派生であり、Carの属性としてEngineを部品として持つ設計となる。
オブジェクト図
クラス図だけで表現すると、複雑なクラス関係では、イメージが分かりづらい場合がでてくる。 この場合、具体的な値を図に書き込んだオブジェクトで表現すると、説明がしやすい場合がある。 このように具体的な値で記述するクラス図は、オブジェクト図と言う。 書き方としては、クラス名の下に下線を引き、中段の属性の所には具体的な値を書き込んで示す。
パッケージ図
パッケージ図は、クラス図をパッケージ毎に分類して記載する図。 パッケージのグループを、フォルダのような図で記載する。

IT専科から引用
コンポーネント図とコンポジット構造図
コンポジット構造図は、クラスやコンポーネントの内部構造を示すもので、コンポーネント図は、複数のクラスで構成される処理に、 インタフェースを用意し、あたかも1つのクラスのように扱ったもの。 接続するインタフェースを飴玉と飴玉を受けるクチのイメージで、提供側を◯───で表し、要求側を⊃──で表す。

IT専科から引用
配置図
配置図は、システムのハードウェア構成や通信経路などを表現するための図。 ハードウェアは直方体の絵で表現し、 デバイスの説明は、”≪device≫”などを示し、実行環境には、”≪executionEnvironment≫” などの目印で表現する。

IT専科から引用
振る舞い図
参考資料をもとに振る舞い図の説明を行う。
ユースケース図

ユーザなど外部からの要求に対する、システムの振る舞いを表現するための活用事例や機能を表す図がユースケース図。 システムを構築する際に、最初に記述するUMLであり、システムに対する処理要件の全体像や機能を理解するために記述する。 ユーザや外部のシステムは、アクターとよび人形の絵で示す。楕円でシステムに対する具体的な処理をユースケースとして楕円で記述する。 関連する複数のユースケースをまとめて、サブジェクトとして示す場合もある。
アクティビティ図
処理順序を記述するための図にはフローチャートがあるが、上から下に処理順序を記述するため、縦長の図になりやすい。また、四角枠の中に複雑なことを書けないので、UMLではアクティビティ図を用いる。
初期状態●から、終了状態◉までの手順を示すためのものがアクティビティ図。 フローチャートに無い表現として、複数の処理を並行処理する場合には、フォークノードで複数の処理を併記し、最終的に1つの処理になる部分をマージノードで示す。 通常の処理は、角丸の長方形で示し、条件分岐はひし形で示す。
ステートチャート図(状態遷移図)
ステートチャート図は、処理内部での状態遷移を示すための図。 1つの状態を長丸長方形で示し、初期状態●から終了状態◉までを結ぶ。 1つの状態から、なんらかの状態で他の状態に遷移する場合は、分岐条件となる契機(タイミング)とその条件、およびその効果(出力)を「契機[条件]/効果」で矢印に併記する。 複数の状態をグループ化して表す場合もある。
シーケンス図
複数のオブジェクトが相互にやり取りをしながら処理が進むようなもののタイミングを記述するためのものがシーケンス図。 上部の長方形にクラス/オブジェクトを示し、その下に縦軸にて時系列の処理の流れの線(Life Line)を描く。 オブジェクトがアクティブな状態は、縦長の長方形で示し、そのLife Line間を、やり取り(メッセージ)の線で相互に結ぶ。 メッセージは、相手側からの返答を待つような同期メッセージは、黒塗り三角矢印で示す。 返答を待たない非同期メッセージは矢印で示し、返答は破線で示す。
コミュニケーション図
クラスやオブジェクトの間の処理とその応答(相互作用)と関連の両方を表現する図。
応答を待つ同期メッセージは -▶︎、非同期メッセージは→で表す。複数のオブジェクト間のやりとりの相互作用を表現する。
タイミング図
タイミング図は、クラスやオブジェクトの時間と共に状態がどのように遷移するのかを表現する図。
状態変化の発生するタイミングや、時間的な遅れや時間的な制約を図で明記するために使われる。
オブジェクト指向プログラミング(ソフトウェア工学)
オブジェクト指向プログラミングは、最近の多くのプログラム言語で取り入れられている機能。
今回は、構造化プログラミング → オブジェクト指向(クラス,メソッド)、コンストラクタ、派生・継承、仮想関数の概念を紹介する。
オブジェクト指向プログラミングの歴史
最初のプログラム言語のFortran(科学技術計算向け言語)の頃は、処理を記述するだけだったけど、 COBOL(商用計算向け言語)ができた頃には、データをひとまとめで扱う「構造体」(C言語ならstruct {…}の考えができた。(データの構造化)
// C言語の構造体 // データの構造化 struct Person { // 1人分のデータ構造をPersonとする char name[ 20 ] ; // 名前 int b_year, b_month, b_day ; // 誕生日 } ;
一方、初期のFortranでは、プログラムの処理順序は、if 文と goto 文で組み合わせて書くこと多く、処理がわかりにくかった。その後のALGOLの頃には、処理をブロック化して扱うスタイル(C言語なら{ 文 … }の複文で 記述する方法ができてきた。(処理の構造化)
// ブロックの考えがない時代の雰囲気をC言語で表すと int i = 0 ; LOOP: if ( i >= 10 ) goto EXIT ; if ( i % 2 != 0 ) goto NEXT ; printf( "%d " , i ) ; NEXT: i++ ; goto LOOP ; // 処理の範囲を字下げ(インデント)で強調 EXIT: --------------------------------------------------- // C 言語で書けば int i ; for( i = 0 ; i < 10 ; i++ ) { // 処理の構造化 if ( i % 2 == 0 ) { printf( "%d¥n" , i ) ; } } --------------------------------------------------- ! 構造化文法のFORTRANで書くと integer i do i = 0 , 9 if ( mod( i , 2 ) == 0 ) then print * , i end if end do
このデータの構造化・処理の構造化により、プログラムの分かりやすさは向上し、このデータと処理をブロック化した書き方は「構造化プログラミング(Structured Programming)」 と呼ばれる。
この後、様々なプログラム言語が開発され、C言語などもできてきた。 一方で、シミュレーションのプログラム開発(例simula)では、 シミュレーション対象(object)に対して、命令するスタイルの書き方が生まれ、 データに対して命令するという点で、擬人法のようなイメージで直感的にも分かりやすかった。 これがオブジェクト指向プログラミング(Object Oriented Programming)の始まりとなる。略記するときは OOP などと書くことが多い。
この考え方を導入した言語の1つが Smalltalk であり、この環境では、プログラムのエディタも Smalltalk で記述したりして、オブジェクト指向がGUIのプログラムと親和性が良いことから、この考え方は多くのプログラム言語へと取り入れられていく。
C言語にこのオブジェクト指向を取り入れ C++ が開発される。さらに、この文法をベースとした Java などが開発されている。最近の新しい言語では、どれもオブジェクト指向の考えが使われている。
クラスの導入(構造体でオブジェクト指向もどき)
例えば、名前と年齢の構造体で処理を記述する場合、 以下の様な記載を行うことで、データ設計者とデータ利用者で分けて 仕事ができる。
// この部分はデータ構造の設計者が書く // データ構造を記述 struct Person { char name[10] ; int age ; } ; // データに対する処理を記述 void setPerson( struct Person* p , char s[] , int a ) { // ポインタの参照で表記 strcpy( p->name , s ) ; p->age = a ; } void printPerson( struct Person* p ) { printf( "%s %d¥n" , p->name , p->age ) ; } // ------------------------------------------- // この部分は、データ利用者が書く int main() { // Personの中身を知らなくてもいいから配列を定義(データ隠蔽) struct Person saitoh ; setPerson( &saitoh , "saitoh" , 55 ) ; struct Person table[ 10 ] ; // 初期化は記述を省略 for( int i = 0 ; i < 10 ; i++ ) { // 出力する...という雰囲気で書ける(手続き隠蔽) printPerson( &table[i] ) ; } return 0 ; }
このプログラムの書き方では、mainの中を読むだけもで、 データ初期化とデータ出力を行うことはある程度理解できる。 この時、データ構造の中身を知らなくてもプログラムが理解でき、 データ実装者はプログラムを記述できる。これをデータ構造の隠蔽化という。 一方、setPerson()や、printPerson()という関数の中身についても、 初期化・出力の方法をどうするのか知らなくても、 関数名から動作は推測できプログラムも書ける。 これを手続きの隠蔽化という。
C++のクラスで表現
上記のプログラムをそのままC++に書き直すと以下のようになる。特徴として 構造体を進化させた class 宣言の中に、データ構造とデータ構造を使う関数をまとめて記述する。
#include <stdio.h> #include <string.h> // この部分はクラス設計者が書く class Person { private: // ■ クラス外からアクセスできない部分 // データ構造を記述 char name[10] ; // メンバーの宣言 int age ; public: // ■ クラス外から使える部分 // データに対する処理を記述 void set( char s[] , int a ) { // ■ メソッドの宣言 // pのように対象のオブジェクトを明記する必要はない。 strcpy( name , s ) ; age = a ; } void print() { printf( "%s %d¥n" , name , age ) ; } } ; // ← 注意ここのセミコロンを書き忘れないこと。 // この部分はクラス利用者が書く int main() { Person saitoh ; saitoh.set( "saitoh" , 55 ) ; saitoh.print() ; // 文法エラーの例 printf( "%d¥n" , saitoh.age ) ; // ■ age は private なので参照できない。 return 0 ; }
用語の解説:C++のプログラムでは、データ構造とデータの処理を、並行しながら記述する。 データ構造に対する処理は、メソッド(method)と呼ばれる。 データ構造とメソッドを同時に記載したものは、クラス(class)と呼ぶ。 そのclassに対し、具体的な値や記憶域が割り当てられたものをオブジェクト(object)とかインスタンス(instance)と呼ぶ。
コンストラクタ
データ構造を扱ううえで、データの初期化や廃棄処理は重要となるが、書き忘れをすることも多い。そこで、C++ ではコンストラクタで初期化を簡単に書ける。
// コンストラクタを使って書く class Person { private: char name[10] ; // メンバーの宣言 int age ; public: Person( char s[] , int a ) { // ■ コンストラクタ strcpy( name , s ) ; age = a ; } void print() { printf( "%s %d¥n" , name , age ) ; } } ; int main() { Person saitoh( "saitoh" , 57 ) ; // ■ コンストラクタで宣言&初期化 return 0 ; }
派生と継承
オブジェクト指向では、元となったデータ構造(class ? struct ?)を拡張した時の記述が便利。例えば、前述の Person に住所も覚えたかったとしたら どう書くだろうか?
// オブジェクト指向を使わずに記述 // Personを要素として持つ PersonAddr を定義 struct PersonAddr { struct Person person ; char addr[ 20 ] ; } ; // PersonAddr のメソッド void setPersonAddr( struct PersonAddr* p , char nm[] , int ag , char ad[] ) { setPerson( p->person , nm , ag ) ; strcpy( p->addr , ad ) ; } void printPersonAddr( struct PersonAddr* p ) { printPerson( p->person ) ; printf( "%s" , p->addr ) ; return 0 ; }
オブジェクト指向では、こういった場合には、Person を拡張した PersonAddr を定義する。この時、元となるクラス(Person)は基底クラス(あるいは親クラス)、拡張したクラス(PersonAddr)は派生クラス(あるいは子クラス)と呼ぶ。
// C++ 流に記述 class PersonAddr : public Person { // ■ Personから派生したPersonAddr private: // ~~~~~~~~~~~~~派生 char addr[ 20 ] ; public: PersonAddr( char nm[] , int ag , char ad[] ) : Person( nm , ag ) { // ■ 基底クラスのコンストラクタで初期化 strcpy( addr , ad ) ; // ■ 追加部分の初期化 } } ; int main() { Person tohru( "tohru" , 57 ) ; PersonAddr saitoh( "saitoh" , 45 , "Fukui" ) ; tohru.print() ; // tohru 57 saitoh.print() ; // saitoh 45 ■ 継承を使って表示 return 0 ; }
この例では、saitoh は PersonAddr であり、それを表示するための PersonAddr::print() は定義されていないが、saitoh.print() を実行すると、基底クラスのメソッド Person::print() を使ってくれる。このように基底クラスのメソッドを流用してくれる機能を継承と呼ぶ。
仮想関数
前述の継承のプログラムでは、PersonAddr 専用の print() を定義してもいい。また基底クラスと派生クラスが混在する配列を作ることもできる。しかし、以下のようなプログラムでは問題が発生する。
class PersonAddr : public Person { private: char addr[ 20 ] ; public: PersonAddr( char nm[] , int ag , char ad[] ) : Person( nm , ag ) { strcpy( addr , ad ) ; } void print() { // ■ PersonAddr 専用の print() を宣言してもいい Person::print() ; // 基底クラス Person の print() を使う printf( "%s" , addr ) ; } } ; int main() { Person tohru( "tohru" , 57 ) ; PersonAddr saitoh( "saitoh" , 45 , "Fukui" ) ; Person* family[] = { // tohru と saitoh のポインタ配列 &tohru , &saitoh // &saitoh は Person* に降格されている } ; tohru.print() ; // tohru 57 saitoh.print() ; // saitoh 45 Fukui for( int i = 0 ; i < 2 ; i++ ) // tohru 57 family[ i ]->print() ; // ■ "saitoh 45"としか表示してくれない return 0 ; }
family[] のデータを全部表示したいのなら、”tohru 57, saitoh 45 Fukui” と表示されてほしい。
こういった場合に、データのclassに応じて適切なメソッドを呼び出すメカニズムとして仮想関数がある。
class Person { private: char name[ 20 ] ; int age ; public: Person( char nm[] , int ag ) { strcpy( name , nm ) ; age = ag ; } virtual void print() { // ■ ここに virtual が追加された|仮想関数 printf( "%s %d" , name , age ) ; } } ; class PersonAddr : public Person { private: char addr[ 20 ] ; public: PersonAddr( char nm[] , int ag , char ad[] ) : Person( nm , ag ) { strcpy( addr , ad ) ; } virtual void print() { // ■ ここに virtual が追加された|仮想関数 Person::print() ; printf( "%s" , addr ) ; } } ; int main() { Person tohru( "tohru" , 57 ) ; PersonAddr saitoh( "saitoh" , 45 , "Fukui" ) ; Person* family[] = { // tohru と saitoh のポインタ配列 &tohru , &saitoh } ; for( int i = 0 ; i < 2 ; i++ ) // tohru 57 family[ i ]->print() ; // ■ saitoh 45 Fukui と表示 // print は 仮想関数がそれぞれ定義してあるので // tohru は、"tohru 57"と表示されるし return 0 ; // saitoh は、"saitoh 45 Fukui"と表示される。 }
このプログラムでは、Person::print() と PersonAddr::print() がそれぞれ仮想関数で定義されているので、tohru, saitoh の各インスタンスには、型情報が埋め込まれている。このため family[0]->print() では “tohru 57” が表示されるし、family[1]->print() では “saitoh 45 Fukui” が表示される。
多態性・ポリモーフィズム
このように、派生クラスと仮想関数使ってプログラムを書くと、その派生クラスに応じた処理を呼び出すことができる。このように共通の基底クラスから様々な派生クラスを作りながらプログラムを書き、その派生クラス毎にそのデータに応じた処理を実行させることができる。インスタンスがデータ種別に応じた動きをすることは多態性(ポリモーフィズム)と呼ばれる。
企業見学 2EI オーディオテクニカ、jig.jp
今日は、2EIの企業見学で、オーディオテクニカ・フクイ さんと、jig.jp さんを訪問しました。
オーディオテクニカ・フクイ
午前中は、越前市のオーディオテクニカ・フクイさんを見学させて頂きました。
クラッシックな蓄音機
個人的には、エジソンは蓄音機の開発で有名だが機能「ハード」にこだわった。一方、ベルリナーは円盤式レコードで音楽の「ソフト」と一緒に「ハード」を開発していったから、エジソンは最終的にレコードから撤退したが、ベルリナーはレコードを発展させた…のお話が面白かった。
最後に巨大な電波暗室にて記念写真
にゃんこ!!