オブジェクト指向の最近のブログ記事

2015年度のオブジェクト指向プログラミングの講義録を以下にまとめる。

レポート課題は以下のとおり

  • 隠蔽化に関するレポート(複素数クラスの直交座標系・極座標系での実装)
  • 仮想関数に関するレポート(図形クラスに機能を追加し色付き図形に拡張)
  • UMLに関するレポート(自分の特別研究の内容より2つのUML図を記述)

オブジェクト指向プログラミングの最後の総括として、 ソフトウェア工学との説明を行う。

トップダウン設計とウォーターフォール型開発

ソフトウェア工学でプログラムの開発において、一般的なサイクルとしては、 専攻科などではどこでも出てくるPDCAサイクル(Plan,Do,Check,Action)が行われる。 この時、プログラム開発の流れとして、大企業でのプログラム開発では一般的に、 トップダウン設計とウォーターフォール型開発が行われる。

トップダウン設計では、全体の設計(Plan)を受け、プログラムのコーディング(Do)を行い、 動作検証(Check)をうけ、最終的に利用者に納品し使ってもらう(Action)...の中で、 開発が行われる。設計の中身も機能仕様や動作仕様...といった細かなフェーズになることも多い。 この場合、コーディングの際に設計の不備が見つかり設計のやり直しが発生すれば、 全行程の遅延となることから、前段階では完璧な動作が必要となる。 このような、上位設計から下流工程にむけ設計を行う方式は、 トップダウン設計などと呼ばれる。また、処理は前段階へのフィードバック無しで次工程へ流れ、 川の流れが下流に向かう状態にたとえ、ウォーターフォールモデルと呼ばれる。


引用:Think IT 第2回開発プロセスモデル

このウォーターフォールモデルに沿った開発では、横軸時間、縦軸工程とした ガントチャートなどを描きながら進捗管理が行われる。

一方、チェック工程(テスト工程)では、 要件定義を満たしているかチェックしたり、設計を満たすかといったチェックが存在し、 テストの前工程にそれぞれ対応したチェックが存在する。 その各工程に対応したテストを経て最終製品となる様は、V字モデルと呼ばれる。


引用:@IT Eclipseテストツール活用の基礎知識

しかし、ウォーターフォールモデルでは、前段階の設計の不備があっても前工程に戻るという考えをとらないため、全体のPDCAサイクルが終わって次のPDCAサイクルまで問題が残ってしまう。 巨大プロジェクトで大量の人が動いているだから、簡単に方針が揺らいでもトラブルの元にしか ならないことから、こういった手法は大人数巨大プロジェクトでのやり方。

ボトムアップ設計とアジャイル開発

少人数でプログラムを作っている時(あるいはプロトタイプ的な開発)には、 部品となる部分を完成させ、それを組合せて全体像を組み上げる手法もとられる。 この方法は、ボトムアップ設計と呼ばれる。

また、ウォーターフォールモデルでは、前工程の不備をタイムリーに見直すことができないが、 少人数開発では適宜前工程の見直しが可能となる。 特にオブジェクト指向プログラミングを実践して隠蔽化が正しく行われていれば、 ライブラリの利用者への影響を最小にしながら、ライブラリの内部設計の見直しも可能となる。 このような内部構造の改善はリファクタリングと呼ばれる。

一方、プログラム開発で、ある程度の規模のプログラムを作る際、最終目標の全機能を実装したものを 目標に作っていると、全体像が見えずプログラマーの達成感も得られないことから、 機能の一部分だけ完成させ、次々と機能を実装し完成に近づける方式もとられる。 この方式では、機能の一部分の実装までが1つのPDCAサイクルとみなされ、 このPDCAサイクルを何度も回して完成形に近づける方式とも言える。 このような開発方式は、アジャイル開発と呼ぶ。 一つのPDCAサイクルは、アジャイル開発では反復(イテレーション)と呼ばれ、 短い開発単位を繰り返し製品を作っていく。この方法では、一度の反復後の実装を顧客に見てもらい、 顧客とプログラマーが一体となって開発が行われる。


引用:コベルコシステム

エクストリームプログラミング

アジャイル開発を行うためのプログラミングスタイルとして、 エクストリームプログラミング(Xp)という考え方も提唱されている。 Xpでは、5つの価値(コミュニケーション,シンプル,フィードバック,勇気,尊重)を基本とし、 開発のためのプラクティス(習慣,実践)として、 テスト駆動開発(コーディングではテストのためのプログラムも並行して行い,こまめにテストを実行する)や、 ペアプログラミング(2人ペアで開発し、コーディングを行う人とそのチェックを行う人で役割分担をし、 一定期間毎にその役割を交代する)などの方式が取られることが多い。

UML振る舞い図

参考資料図をもとに振る舞い図の説明を行う。

ユースケース図

1507131131_211x192.png

ユーザなど外部からの要求に対する、システムの振る舞いを表現するための図。 システムを構築する際に、最初に記述するUMLであり、システムに対する処理要件の全体像を理解するために記述する。 ユーザや外部のシステムは、アクターとよび人形の絵で示す。楕円でシステムに対する具体的な処理をユースケースとして記述する。 関連する複数のユースケースをまとめて、サブジェクトとして示す場合もある。

アクティビティ図

初期状態から、終了状態までの手順を示すための図。 複数の処理を並行処理する場合には、フォークノードで複数の処理を併記し、最終的に1つの処理になる部分をマージノードで示す。 通常の処理は、角丸の長方形で示し、条件分岐はひし形で示す。

ステートチャート図(状態遷移図)

ステートチャート図は、処理内部での状態遷移を示すための図。 1つの状態を長丸長方形で示し、初期状態から終了状態までを結ぶ。 1つの状態から、なんらかの状態で他の状態に遷移する場合は、分岐条件となる入力とその際に出力する内容を「入力/出力」で矢印に併記する。 複数の状態をグループ化して表す場合もある。

シーケンス図

複数のオブジェクトが相互にやり取りをしながら処理が進むようなものを記述するためのものがシーケンス図。 上部の長方形にクラス/オブジェクトを示し、その下に時系列の処理の流れの線(Life Line)を描く。 オブジェクトがアクティブな状態は、縦長の長方形で示し、そのLife Line間を、やり取り(メッセージ)の線で相互に結ぶ。 メッセージは、相手側からの返答を待つような同期メッセージは、黒塗り三角矢印で示す。 返答を待たない非同期メッセージは矢印で示し、返答は破線で示す。

UML構造図

UMLの構造図の書き方の説明。 詳しくは、参考ページのUML入門などが、分かりやすい。

クラス図

クラス図は、構造図の中の基本的な図で、 枠の中に、上段:クラス名、中段:属性(要素)、下段:メソッド(関数)を記載する。 属性やメソッドの可視性を示す場合は、"-":private、"+":public、"#":protected 可視性に応じて、"+-#"などを記載する。

関連

クラスが他のクラスと関係がある場合には、その関係の意味に応じて、直線や矢印で結ぶ。
(a)関連:単純に関係がある場合、
(b)集約:部品として持つが弱い結びつき。関係先が消滅しても別に存在可能。
(c)コンポジション:部品として持つが強い結びつき。関係先と一緒に消滅。
(d)依存:依存関係にあるだけ
(e)派生:派生・継承した関係
(f)実現: Javaでのinterfaceによる多重継承

上図の例では、乗り物クラスVehicleから自動車がCarが派生し、 自動車は、エンジン(Engine)を部品として持つ。エンジンは車体と一緒に廃棄なら、コンポジションで実装する。
自動車は、同じく車輪(Wheel)を4つ持つが、自動車を廃棄してもタイヤは別に使うかもしれないので、集約で実装する。 集約で実装する場合は、C++などであれば、ポインタで部品を持ち、部品の廃棄(delete)は、別に行うことになる。

is-a 、has-a の関係

前の課題でのFigureクラスで、Color 情報をどう扱うべきかで、悩んだ場合と同じように、 クラスの設計を行う場合には、部品として持つのか、継承として機能を持つのか悩む場合がある。 この場合には、"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を部品として持つ設計となる。

オブジェクト図

クラス図だけで表現すると、複雑なクラス関係では、イメージが分かりづらい場合がでてくる。 この場合、具体的な値を図に書き込んだオブジェクトで表現すると、説明がしやすい場合がある。 このように具体的な値で記述するクラス図は、オブジェクト図と言う。 書き方としては、クラス名の下に下線を引き、中段の属性の所には具体的な値を書き込んで示す。

その他の構成図

その他の構成図としては、コンポーネント図(物理的な構成要素から、システムの構造を表現する図)、 配置図(ハードウェアとアプリケーションの関係を図示したもの)、パッケージ図(パッケージ同士の関係をグループ化した図) なども用いる。

多重継承とUMLの導入

Figureクラスで図形描画を仮想関数を用いて演習を行ったが、 課題のテーマとした色を用いたクラスを実装する場合には、様々な方法がある。 レポート課題と取り組まれている中とは思うけど、次のテーマの多重継承の導入として総括する。

また、後半は、UMLの基本を説明する。

色付き図形の実装方法

最初の基本となるFigure(FigureColor)が、色情報を持つ方法。 しかしこの方法は、基底クラスから異なるデータ構造として定義することになるため、 全体のプログラムを書き換えることになる。 この場合、すでにFigure,FigureBox...といったクラスでプログラムを 書いている人がいたら、全面的なプログラム修正が必要となるため、 混乱の元になる。

(( FigureColorが色情報を持つ基底クラス ))
class FigureColor {
private:
   int color ;
public:
   FigureColor( int c )
     : color( c ) { }
   virtual void draw( int , int ) = 0 ;
} ;
// FigureColorから派生
class FigureColorBox : public FigureColor {
private:
   int width , height ;
public:
   FigureColorBox( int w , int h , int c ) {
    : FigureColor( c ) ,
       width( w ) , height( h ) {}
   virtual void draw( int , int ) {
   }
} ;

授業で説明した、FigureBoxからFigureBoxColorを派生する方法は、 既存のプログラムの延長で機能を追加できる。このため、既存のコードの 有効利用ができるのが利点。 しかし、FigureBox以外に、FigureCircle,FigureStar,FigureTriangle....といった 様々な派生がすでに存在する時は、色に関するコードが後付なので、 色に関するクラスをさらに作る場合は、色の設定処理が洗練された書き方にできない。

(( Figure→FigureBox→FigureBoxColor ))
class Figure {
   (略)
} ;
class FigureBox : public Figure {
   (略)
} ;
class FigureBoxColor : public FigureBox {
private:
   int color ;
public:
   virtual void draw( int x , int w ) {
      GWsetpen( ... , color , ... ) ; // ださい
      FigureBox::draw( x , y ) ;
   }
} ;

多重継承

前のFigureBoxColorの例は、色のクラスが後付なので、色に関する処理が複雑であったとして、 その処理を隠蔽化するにはどうすればよいか? 一つの方法が、多重継承である。

class Figure {
   (略)
} ;

// 色を扱うクラス
class Color {
private:
   int color ;
public
   Color( int c ) ;
   void setcolor() {
      GWsetcolor( ... , color , ... ) ;
   }
} ;

// 色無しのクラスはそのまま使える
class FigureBox : public Figure {
   (略)
} ;

class FigureBoxColor : public FigureBox , public Color {
public:
   FigureBoxColor( int w , int h , int c )
     : FigureBox( w , h ) ,
      Color( c ) {}
   virtual void draw( int x , int y ) {
      setcolor() ; // Colorのメソッドを使う
      FigureBox::draw( x , y ) ;
   }
} ;

ただし多重継承は以下の理由から、実装が複雑化することから、Javaなどの言語では別の方法を用いる。

  • 継承は、派生したクラスのデータが基底クラスの後ろに繋がる形で実装される。 データ領域の先頭部分は基底クラスのデータ領域であり、基底クラスの参照は極めて単純。 しかし、多重継承を認めると、データ領域の後ろに派生データを追加する方法では、 途中に別の基底クラスのデータ領域を挟み込む形になるため、基底クラスを参照することが複雑になる。
  • また、基底クラスに同名のメソッドがあった場合、派生クラスでのメソッド呼び出しでどちらを呼び出すべきか、曖昧になる。
  • さらに、ダイヤモンド継承があると、基底クラスのデータが2重化する可能性がある。
    (( ダイヤモンド継承 ))
       動物クラス[誕生日]
      /    \
    鳥クラス[羽]  哺乳類クラス[手]
      \    / 
       カモノハシ
    // 鳥クラスは要素として[誕生日,羽]を持つ
    // 哺乳類クラスは、[誕生日,手]を持つ
    // カモノハシクラスは、[誕生日,羽,誕生日,手]を持つ???
    

UML

UML(Unified Modeling Language)とは、オブジェクト指向で、データ構造や処理の流れを図で表現するための手法として 考えられた。

例えば、処理の流れを図示する場合のフローチャートは、初心者でも分かりやすい。 しかし、長方形のマスの中に複雑な処理を記入するのは困難となる。 改良版として、PADという記法もあるが、あまり普及していない。

歴史的に見れば、 ランボーの提唱したオブジェクトモデル化技法(OMT)、 ブーチがオブジェクト指向設計(OOD)でのBooch法、 ヤコブソンのオブジェクト指向ソフトウェア工学(OOSE)の考え方を元に、 UML(統一モデリング言語)が策定された。

UMLでの図の表現は、データ構造を表現するクラス図、コンポーネント図、オブジェクト図、パッケージ図などがあり、 一方で処理の振舞いを記述する、アクティビティ図、ユースケース図、状態遷移図、相互作用図、シーケンス図などがある。 詳しくは次週の講義にて、説明。

前回の授業で仮想関数の話をしたので、 演習と課題に取組む。

最初に、学科によってはグラフィックスを使う機会も少ないし、 演習もかなり以前だと思われるので、GrWinの使い方を思い出してもらうための資料を配布。

仮想関数とグラフィックス処理

次に、前回の授業の後半で説明した、グラフィックスを扱う仮想関数のサンプルプログラムを示し、 その説明を行う。

このサンプルプログラムでは、グラフィックスを使うということで、 GrWinクラスを作り、コンストラクタでウィンドウの初期化処理、 デストラクタでウィンドウの閉じる処理を記載してある。

class GrWin {
privete:
    int width , height ;
public:
    GrWin( int , int ) ;
    ~GrWin() ;
} ;
// コンストラクタでウィンドウを開く処理
GrWin::GrWin( int w , int h )
: width( w ) ,
  height( h )
{
    GWopen( 0 ) ;
    // 略: PDF資料をみてください
}
// デストラクタでウィンドウを閉じる処理
GrWin::~GrWin() {
    GWquit() ;
}
void main() {
    GrWin gr( 640 , 480 ) ;
    // 描画処理
}

次に、図形を描くクラス(Figure)を考える。 このクラスから四角(FigureBox)と、円(FigureCircle)を派生させる。

// 図形のクラス(純粋仮想基底クラス)
class Figure {
public:
    virtual void draw( int , int ) = 0 ;
} ;

// 四角を描く図形クラス 
class FigureBox : public Figure {
private:
    int width , height ;
public:
    FigureBox( int w , int h ) : width( w ) , height( h ) {}
    virtual void draw( int , int ) ;
} ;
virtual void FigureBox::draw( int x , int y ) {
    GWline( ... , ... , ... , ... ) ;
    // 略: PDF資料参照
}

// 丸を描く図形クラス 
class FigureCircle : public Figure {
private:
    int radius ;
public:
    FigureCircle( int r ) : radius( r ) {}
    virtual void draw( int , int ) ;
} ;
virtual void FigureCircle::draw( int x , int y ) {
    // 略: PDF資料参照
    for( double th = 0 ; th < 360 ; th += 5.0 ) {
        :
    }
}

void main() {
    Figure* array[3] = {
        new FIgureBox( 100 , 100 ) ;
        new FigureCircle( 150 ) ;
        new FigureBox( 50 , 100 ) ;
    } ;
    for( int i = 0 ; i < 3 ; i++ ) {
        array[ i ]->draw( 100 + 100 * i , 150 ) ;
    }
}

前々回の講義では派生と継承の説明にて、次のようなプログラムを説明した。

派生と継承の復習

// 基底クラス
class Person {
private:
  char name[ 20 ] ;
  int  age ;
public:
  Person( const char* s , int a ) {
    strcpy( name , s ) ;
    age = a ;
  }
  void print() {
    printf( "%s %d\n" , name , age ) ;
  }
} ;
// 派生クラス
class Student : public Person {
private:
  char dep[ 10 ] ;
  int  grade ;
public:
  Student( const char* s , int a , const char* d , int g )
    : Person( s , a ) 
  {
    strcpy( dep , d ) ;
    grade = g ;
  }
  void print() { // 継承でなくStudent版のprintを定義
    Person::print() ;
    printf( "= %s %d\n" , dep , grade ) ;
  }
} ;

int main() {
  // 派生と継承の確認
  Person tsaitoh( "tsaitoh" , 50 ) ;
  tsaitoh.print() ;

  Student naka( "naka" , 22 , "PS" , 2 ) ;
  naka.print() ; // Student版を実行

  // 異なる型のデータをごちゃ混ぜにできないか?
  Person* table[2] ;
  table[0] = &tsaitoh ;
  table[1] = &naka ;

  for( int i = 0 ; i < 2 ; i++ ) {
    table[i]->print() ;
  }
  return 0 ;
}

このプログラムのmain() では、tsaitohとnakaのデータを 表示させているが、tsaitohとnakaは、オマケの有る無しの違いはあっても、 元をたどれば、Person型。ごちゃまぜにできないだろうか?

int main() {
   :
  // 異なる型のデータをごちゃ混ぜにできないか?
  Person* table[2] ;
  table[0] = &tsaitoh ;
  table[1] = &naka ;

  for( int i = 0 ; i < 2 ; i++ ) {
    table[i]->print() ;
  }
  return 0 ;
}

C++では、派生クラスは格下げして基底クラスのように振る舞うこともできる。 上記の例では、Studentのポインタ型である、&nakaは、 Personのポインタ型として振る舞うこともできる。

ただし、table[]に格納する時点で、Person型へのポインタなので、 table[i]->print() を呼び出しても、Personとして振る舞うため、 tsaitoh 50 / naka 22 が表示される。

仮想関数

上記のプログラムでは、Student型には、名前,年齢,所属,学年をすべて表示する Student::print() が宣言してある。 であれば、「naka 22」 ではなく、「naka 22 PS 2」と表示したくはならないか?

上記の例では、table[] に格納する段階で、格下げされているため、「naka 22」しか表示できない。 しかし、以下のように書き換えると、「naka 22 PS 2」と表示できる。

// 基底クラス
class Person {
private:
  char name[ 20 ] ;
  int  age ;
public:
  Person( const char* s , int a ) {
    strcpy( name , s ) ;
    age = a ;
  }
  virtual void print() {
    printf( "%s %d\n" , name , age ) ;
  }
} ;
// 派生クラス
class Student : public Person {
private:
  char dep[ 10 ] ;
  int  grade ;
public:
  Student( const char* s , int a , const char* d , int g )
    : Person( s , a ) 
  {
    strcpy( dep , d ) ;
    grade = g ;
  }
  virtual void print() { // 継承でなくStudent版のprintを定義
    Person::print() ;
    printf( "= %s %d\n" , dep , grade ) ;
  }
} ;

int main() {
  Person tsaitoh( "tsaitoh" , 50 ) ;
  Student naka( "naka" , 22 , "PS" , 2 ) ;

  // 異なる型のデータをごちゃ混ぜにできないか?
  Person* table[2] ;
  table[0] = &tsaitoh ;
  table[1] = &naka ;

  for( int i = 0 ; i < 2 ; i++ ) {
    table[i]->print() ;
  }
  return 0 ;
}

上記の例の用に、printメソッドに virtual 宣言を追加すると、 C++では、tsaitoh,naka のデータ宣言時に、データ種別情報が埋め込まれる。 この状態で、table[i]->print() を呼び出されると、 データ種別毎のprint()を呼び出してくれる。 (この実装では、前回授業の関数ポインタが使われている)

ここで、学生さんから、「table[] の宣言をStudentで書いたらどうなるの?」という 質問がでた。おもわず『良い質問ですねぇ〜』と池上彰ばりの返答をしてしまった。

Student* table[] ;
table[0] = &tsaitoh ; // 文法エラー
                  // 派生クラスは基底クラスにはなれるけど、
                  // 基底クラスが派生クラスになることはできない。
table[1] = &naka ;

純粋仮想基底クラス

// 純粋仮想基底クラス
class Object {
public:
  virtual void print() = 0 ; // 中身の無い純粋基底クラスを記述しない時の書き方。
} ;
// 整数データの派生クラス
class IntObject : public Object {
private:
  int data ;
public:
  IntObject( int x ) {
    data = x ;
  }
  virtual void print() {
    printf( "%d\n" , data ) ;
  }
} ;
// 文字列の派生クラス
class StringObject : public Object {
private:
  char data[ 100 ] ;
public:
  StringObject( const char* s ) {
    strcpy( data , s ) ;
  }
  virtual void print() {
    printf( "%s\n" , data ) ;
  }
} ;
// 実数の派生クラス
class DoubleObject : public Object {
private:
  double data ;
public:
  DoubleObject( double x ) {
    data = x ;
  }
  virtual void print() {
    printf( "%lf\n" , data ) ;
  }
} ;
// 動作確認
int main() {
  Object* data[3] = {
    new IntObject( 123 ) ,
    new StringObject( "abc" ) ,
    new DoubleObject( 1.23 ) ,
  } ;
  for( int i = 0 ; i < 3 ; i++ ) {
    data[i]->print() ;
  }
  return 0 ;
} ;

この書き方では、data[]には、整数、文字列、実数という異なるデータが入っているが、 Objectという純粋仮想基底クラスを通して、共通な型のように扱えるようになる。 そして、data[i]->print() では、各型の仮想関数が呼び出されるため、 「123 abc 1.23」 が表示される。

ここで、Object の様な一見すると中身が何もないクラスを宣言し、 このクラスから様々な派生クラスを用いるプログラムテクニックは、 広く利用され、Objectのような基底クラスは、純粋仮想基底クラスなどと呼ばれる。

派生と継承の基本の説明をしてきたので、次に説明する予定の仮想関数の導入として、 実装のために使われている関数ポインタを紹介。

関数ポインタ

関数ポインタとは、関数へのポインタであり、ポインタを変更することで、 処理を切り替えるために使われる。 まずは、動作説明の簡単なプログラムを紹介。

int add( int x , int y ) { // 加算関数
    return x + y ;
}
int mul( int x , int y ) { // 乗算関数
    return x * y ;
}
void main() {
    int (*f)(int,int) ; // int×2引数、返り値intの関数へのポインタ
    f = add ;
    printf( "%d" , (*f)( 2 , 3 ) ) ; // 5を表示
    f = mul ;
    printf( "%d" , (*f)( 2 , 3 ) ) ; // 6を表示
}

関数ポインタを利用すれば、異なるデータに対する処理を、 汎用性高く作ることも可能となる。 例えば以下の vmax() 関数は、自分で用意した大小を比較するだけの関数を渡し、 それ以外の最大値を求めるための処理を行う。 このため、他のデータの最大値を求めたい場合でも、最大値を求める処理を すべて記載するのではなく、対象データの比較関数だけを記述すれば良い。

int intcmp( int* x , int* y ) { // 整数比較関数
    if ( *x > *y )
        return 1 ;
    else if ( *x < *y )
        return -1 ;
    else
        return 0 ;
}
int vmax( void* array ,    // 配列先頭アドレス
          int size ,       // 配列データ件数
          int sizeofdata , // 1件あたりのbyte数
          int(*f)( void*,void* ) ) { // 比較関数
    int i , max = 0 ;
    for( i = 0 ; i < size ; i++ )
        if ( (*f)( array + max * sizeofdata ,
                   array + i   * sizeofdata ) )
            max = i ;
    return max ;
}
int idata[ 4 ] = { 11 , 33 , 22 , 44 } ;
char sdata[ 4 ][ 4 ] = { "ab" , "bc" , "aa" , "c" } ;
void main() {
    int m ;
    // intcmp関数を使って、idata から最大値を探す
    m = vmax( idata , 4 , sizeof(int) , intcmp ) ;
    printf( "%d" , idata[ m ] ) ;
    // strcmp関数を使って、sdata から最大値を探す
    m = vmax( sdata , 4 , sizeof(sdata[0]) , strcmp ) ;
    printf( "%s" , sdata[ m ] ) ;
}

派生と継承

隠ぺい化の次のステップとして、派生・継承を説明する。

派生を使わずに書くと...

元となるデータ構造(例えばPersonが名前と年齢)でプログラムを作っていて、 途中でその特殊パターンとして、所属と学年を加えた学生(Student)という データ構造を作るとする。

// 元となる構造体(Person)                                                                                                                                                  struct Person {
  char name[ 20 ] ; // 名前
  int  age ;        // 年齢
} ;
// 初期化関数
void set_Person( struct Person* p ,
                 char s[] , int x ) {
  strcpy( p->name , s ) ;
  p->age = x ;
}
// 表示関数
void print_Person( struct Person* p ) {
  printf( "%s %d\n" , p->name , p->age ) ;
}
void main() {
  struct Person saitoh ;
  set_Person( &saitoh , "t-saitoh" , 50 ) ;
  print_Person( &saitoh ) ;
}

パターン1(そのまんま...)

上記のPersonに、所属と学年を加えるのであれば、以下の方法がある。 しかし以下パターン1は、要素名がname,ageという共通な部分があるようにみえるが、 プログラム上は、PersonとPersonStudent1は、まるっきり関係のない別の型にすぎない。

このため、元データと共通部分があっても、同じ処理を改めて書き直しになる。

// 元のデータに追加要素(パターン1)
struct PersonStudent1 {
  char name[ 20 ] ; // 名前
  int  age ;        // 年齢
  char dep[ 20 ] ;  // 所属
  int  grade ;      // 学年
} ;

void set_PersonStudent1( struct PersonStudent1* p ,
                         char s[] , int x ,
                         char d[] , int g ) {
  strcpy( p->name , s ) ; // 同じことを書いてる
  p->age = x ;
  strcpy( p->dep , d ) ;  // 追加分はしかたない
  p->grade = g ;
}
// 名前と年齢だけ表示
void print_PersonStudent1( struct PersonStudent1* p ) {
  // また同じ処理を書いてる
  printf( "%s %d\n" , p->name , p->age ) ;
}
void main() {
  struct PersonStudent1 naka1 ;
  set_PersonStudent1( &naka1 ,
                      "naka" , 22 , "PS" , 2 ) ;
  print_PersonStudent1( &naka1 ) ;
}

パターン2(元データの処理を少し使って...)

パターン1では、同じような処理を何度も書くことになり、面倒なので、 元データ用の関数をうまく使うように書いてみる。

// 元のデータに追加要素(パターン2)
struct PersonStudent2 {
  struct Person person ;
  char          dep[ 20 ] ;
  int           grade ;
} ;
void set_PersonStudent2( struct PersonStudent2* p ,
                         char s[] , int x ,
                         char d[] , int g ) {
  // Personの関数を部分的に使う
  set_Person( &(p->person) , s , x ) ;
  // 追加分はしかたない
  strcpy( p->dep , d ) ;
  p->grade = g ;
}
void print_PersonStudent2( struct PersonStudent2* p ) {
  // Personの関数を使う。
  print_Person( &p->person ) ;
}
void main() {
  struct PersonStudent2 naka2 ;
  set_PersonStudent2( &naka2 ,
                      "naka" , 22 , "PS" , 2 ) ;
  print_PersonStudent2( &naka2 ) ;
}

このパターン2であれば、元データ Person の処理をうまく使っているので、 プログラムの記述量を減らすことはできるようになった。

しかし、print_PersonStudent2() のような処理は、元データ構造が同じなのに、 いちいちプログラムを記述するのは面倒ではないか?

そこで、元データの処理を拡張し、処理の流用ができないであろうか?

基底クラスから派生クラスを作る

オブジェクト指向では、元データ(基底クラス)に新たな要素を加えたクラス(派生クラス)を 作ることを「派生」と呼ぶ。派生クラスを定義するときは、クラス名の後ろに、 「:」「public/protected/private」基底クラス名を書く。

// 基底クラス
class Person {
private:
  char name[ 20 ] ;
  int  age ;
public:
  Person( const char s[] , int x ) {
    strcpy( name , s ) ;
    age = x ;
  }
  void print() {
    printf( "%s %d\n" , name , age ) ;
  }
} ;

// 派生クラス
class Student : public Person {
private:
  char dep[ 20 ] ;
  int  grade ;
public:
  Student( const char s[] , int x ,
           const char d[] , int g )
    : Person( s , x ) // 基底クラスのコンストラクタ
  {
    strcpy( dep , d ) ;
    grade = g ;
  }
} ;

void main() {
  Person saitoh( "t-saitoh" , 50 ) ;
  saitoh.print() ;

  Student naka( "naka" , 22 , "PS" , 2 ) ;
  naka.print() ;
}

ここで注目すべき点は、main()の中で、Studentクラス"naka"に対し、naka.print() を呼び出しているが、パターン2であれば、print_PersonStudent2()に相当するプログラムを 記述していない。 しかし、この派生を使うと Person の print() が自動的に流用することができる。 これは、基底クラスのメソッドを「継承」しているから、 このように書け、名前と年齢「naka 22」が表示される。

さらに、Student の中に、以下のような Student 専用の新しい print()を記述してもよい。

class Student ...略... {
  ...略...
  void print() {
    Person::print() ;
    printf( "%s %d\n" , dep , grade ) ;
  }
} ;
void main() {
  ...略...

  Student naka( "naka" , 22 , "PS" , 2 ) ;
  naka.print() ;
}

この場合は、継承ではなく機能が上書き(オーバーライト)されるので、 「naka 22 / PS 2」が表示される。

派生クラスを作る際の後ろに記述した、public は、他にも protected , private を 記述できる。

public    だれもがアクセス可能。
protected であれば、派生クラスからアクセスが可能。
          派生クラスであれば、通常は protected で使うのが一般的。
private   派生クラスでもアクセス不可。

前回の隠蔽化の話を受け、実際のプログラムの例を課題に説明。 複素数クラスを(実部,虚部)で実装した後に、(絶対値,偏角)に直したら...

基本プログラム(実部と虚部)

複素数を扱うクラスを作るのであれば、基本的には以下の様なコードとなるだろう。 複素数どうしの簡単な加算・乗算を記載する。

class Complex {
private:
    double re , im ;
public:
    Complex( double x , double y ) {
        re = x ;
        im = y ;
    }
    // 上記コンストラクタは、以下のようにも書ける。
    // Complex( double x , double y )
    // :   re( x ) , im( y )
    // { メンバ以外の初期化... }
    void print() {
        printf( "%lf+j%lf¥n" , re , im ) ;
    }
    void add( Complex &z ) {
        re = re + z.re ;
        im = im + z.im ;
    }
    void mul( Complex &z ) {
        double x = re * z.re - im * z.im ;
        double y = re * z.im + im * z.re ;
        re = x ;
        im = y ;
    }
} ;

int main() {
    Complex a( 1 , 2 ) ;
    Complex b( 2 , 3 ) ;
    a.add( b ) ;
    a.print() ;
    a.mul( b ) ;
    a.print() ;
    return 0 ;
}

Complexクラス内部をリファクタリング

しかし、前述プログラムでは、mul()メソッドは、add()メソッドよりは、 複雑なものとなっている。 しかし、複素数の乗算は、(絶対値と偏角)を用いれば、絶対値の乗算・偏角の加算で 処理は簡単に記述できる。そこで、クラス内部を乗算と偏角で処理をするように変更してみる。

class Complex {
private:
    double r , th ;
public:
    Complex( double x , double y ) {
        r = sqrt( x*x + y*y ) ;
        th = atan2( y , x ) ; // atan2は象限を考慮してくれる
    }
    void print() {
        printf( "%lf ∠ %lf¥n" , r , th / 3.141592 * 180.0 ) ;
    }
    void add( Complex &z ) {
        // ここは面倒な式になっちゃう
    }
    void mul( Complex &z ) {
        r  = r  * z.r ;
        th = th + z.th ;
    }
} ;

int main() {
    Complex a( 1 , 2 ) ;
    Complex b( 2 , 3 ) ;
    a.add( b ) ;
    a.print() ;
    a.mul( b ) ;
    a.print() ;
    return 0 ;
}

ここで重要なポイントは、2つめの絶対値∠偏角のプログラムの呼び出し側 main() は、 1つめのプログラムとまるっきり同じである。

このように、オブジェクト指向の隠蔽化を行っていれば、当初のクラス設計が悪くて後で変更 したくなった場合、利用者側からの見た目の動作を変更せずに、内部のデータ構造や処理メソッドを 変更が可能となる。 このように、利用者側からの見た目を変更せずに処理の内部を変更すること、 リファクタリング と呼ぶ。これにより、プログラムの不備や問題点があっても、積極的にプログラムを 改良できることから、不備の少ない安全なプログラムを作れるようになる。

隠蔽化の課題

以上の2つのプログラムで複素数の計算メソッド、加算(add),除算(sub),乗算(mul),除算(div)...その他を (実部,虚部)、(絶対値,偏角)で記載し、適切に記述をすれば、呼び出し側main()を まるっきり同じものにできることを通して、隠蔽化についてレポートにまとめよ。

レポートでは、以下の点を記載すること。(レポートは、本科中間試験の頃までに提出が望ましい)

  • 2つの方式でのプログラム例
  • 上記プログラムに対する説明
  • 上記プログラムが正しく動作していたことが判る結果
  • この課題から判る考察

2015年12月

    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 30 31    

アーカイブ

Google

このアーカイブについて

このページには、過去に書かれたブログ記事のうちオブジェクト指向カテゴリに属しているものが含まれています。

次のカテゴリはシステム設計です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。