データ指向の話 第二章「応用編」 - やまざき@BinaryTechnology
「デスマーチと戦う武蔵流プログラマ やまざき のページ」

TopPage
(サイトマップ)


Book
(書籍)


「火事場プロジェクトの法則」
サポートページ


「LHAとZIP」
サポートページ



Document
(文章)

デスマーチの記録に見る
運命の分かれ道
NEW!

武蔵流プログラマからの提言

武蔵流プログラマが斬る Eclipse

コードデザイン最前線
1
2 3 4 5 6 7 8 9
10 11 12 13 ML

C++で読む
デザインパターン


ポインタ不要論

データ圧縮の基礎

プログラマへの
アドバイス


データ指向の話1 2

インターフェースの話


Diary & Books
(日記と本屋)

やまざきの
はてなダイアリ
(日記)
[] [PC] [資産運用]
[デスマ] [映画] [2足ロボ]

やまざきの本屋


SoftWorks
(ソフトウェア)


(1) DeepFreezer
(ディープ・フリーザー)
高速アーカイバ

(English Page)

(2) Closedown-Planet
(クローズダウン・プラネット)
アクションパズルゲーム


(3) PieceMaker
(ピース・メーカー)
ファイル分割/結合


(4) WakuPita
(枠ピタ)
ウィンドウ移動便利ツール

(English Page)

(5) ググ郎
(Bookmarklet)
選択文字列をGoogleで検索

NEW!


Developing
(開発中)


(1) DeepFreezer2
yz2dlg.dll alpha6


C Magazine特集yz2


Hobby & Favorite
(道楽/お気に入り)


2LegRobo
MindStorms



p.s.
(雑談)


Profile

i_want^^;


やまざきが書いた本


[システム開発]
火事場プロジェクトの法則
どうすればデスマーチをなくせるか?
2006/09/13 発売


LHAとZIP
圧縮アルゴリズム×プログラミング入門

奥村さんと共著です。
2003/12/01 発売


やまざきが寄稿した本


SEの読書術
「本質を読む」力を磨く10の哲学 2006/02発売。



開発の現場 Vol.002
「反デスマーチ大研究」という記事。2005/09/13発売。



Software People Vol.3
「武蔵流プログラマからの提言」という記事。2003/10/31発売。



Eclipse パーフェクトマニュアル vol.1
「武蔵流プログラマが斬る Eclipse」という記事。2003/06/05発売。




データ指向の話 第二章「応用編」

●第二章「応用編」では

 第一章では、できるだけ基本的で簡単なコードを紹介してきました。ここからは、少し応用的で、ちょっと長めのコードを紹介していきます。


●GUIへの適応

 固いプログラミングコードの話が続いたので、少し息抜きをしましょう。「データ指向」の考えをユーザインターフェイス(GUI)に適用する話です。
 例えば名前を入力する↓こんなダイアログがあったとします。 比較的よく見かけるユーザインターフェースだと思います。

  +--------------------------+
  |        _______________   |
  | 名前:[_______________]  |
  |                          |
  | [ OK ]   [キャンセル]  |
  |                          |
  +--------------------------+

 ここで、名前を入力せずに、[OK]ボタンを押したとします。

「名前が入力されていません!」

 おそらく、↑このような警告メッセージが出るかと思います。 あたりまえのように よく見かける動作ですが、 これは「おかしい」です。どこがおかしいのかと言うと、 このようなメッセージが「出ることがおかしい」のです。
 データ指向な考え方では、 「データA がある場合のみ データA の処理が行える」 となるので 「名前データ がある場合のみ OK 処理が行える」 となります。よって、「名前が入力されていない状態で、 OKボタンが押せるのはおかしい」のです。正しいユーザインターフェイスは、名前が入力されるまで OKボタンを押せない(ディセーブル/disable)状態にすべきで、 名前が入力され、OK処理が続行できる状態になったら 押せる状態にするべきです。そうすることで、不必要なメッセージを出す必要もなくなり、 ユーザの混乱を押さえる事もできます。
 OKボタンを押したユーザが悪いのではなく、 OKボタンを押せるように作ってあるプログラム (ユーザインタフェイス)が悪いのです。
 データ指向は、このデータの流れに着目し、 機能(関数)はデータの一部であり 「データが無ければ機能は存在しない」 という考え方なのです。最近の言語は統合環境などを実装しているため、 「ボタンを押したら…」などの「機能の記述」は 簡単に行えるようになりました。しかし、「データが無い場合は押せない」などの データに依存したコードが書きやすいようにはなっていません。ここは 少し面倒ですが、しばらく我慢して実装するとして、 良いツールを作ってくれるように 各メーカーさんにお願いすることにしましょう。


●GUIの設計

 最近、GUIのプログラムが増えていますが、 「グラフィカル」なものは良いのですが、「グロテスク」な ユーザインターフェイスは考え物です。実行した直後に、 「ズラー!」とボタンが何十個も出てくるプログラムを見かけますが、 それは、ユーザのことを何も考えていないとしか思えません。

 「存在悪」です。

 普通の人であれば、一度に理解できる項目は 7〜8個が限界です。データ指向の「データの隠蔽」という概念がここで効いてきます。

「見せなくて良い情報は 見せてはいけません」

 見せなくて良い情報は見せてもユーザが混乱するだけで、 なんのメリットもないのです。 1つのウィンドウで見せる情報は1つでも少ない方が良いのです。 しかし、必要な情報まで隠してしまうと、使いにくくなるので この切り分けが大変重要になります。では、「どうアプローチすれば良いのか?」となるのですが、 データ指向からの答えは

 「データの流れに着目してください」さらに、
 「データの親子関係にも着目してください」となります。

 例えば、「ファイル」というデータが、あるかどうかが「大きな流れ」 であって「親データ」です。そのファイルを「表示する機能」は ファイルがある場合のみ使用できる 期間限定(データ限定)の機能であり「子データ」なのです。このように「機能」を追いかけるのではなく、「データ」を追いかけることで よりスマートなプログラムが作れます。おそらく現在、多くの人が「機能」を重視してプログラム作っていると思います。 「ファイルの入力処理」とか「ファイルの出力処理」を作り、 その処理(機能)を実行する。といった具合に。この時、ファイルの有無は各処理(機能)にまかされてしまいますが、 そう考えるのではなく、「ファイルというデータ」が先にあり、 そのファイルの存在に対して「入力処理」や「出力処理」を作り込む、 といった具合に考え、設計し構築するのです。この違いが重要なのです。
 GUIにて、ボタンを押せなくするのも1つの手段ですが、 ボタンそのものを表示しないという方向も検討すべきということです。 データの存在と同期して、ボタン(機能)の表示/非表示を行うという。


●コード量

 データ指向では、データの隠蔽、カプセル化のために コード量が増える傾向にあります。 自然にスコープの記述が多くなり、行数(ステップ数ともいう)が増えます。これは、オブジェクト指向であっても、構造化設計であっても同じように コード量は増える傾向にあるので、データ指向に限らず全ての考え方や概念の導入に いえることと思います。しかし、データ指向では、 把握すべきデータ量が隠蔽されて減るため、 コード量とは関係無く、理解しやすくなります。行数が増えても保守性や工数とも無関係なので、 それほど気にすることはありません。 どんどん、行数を増やして見やすく書きましょう。
 変な例えですが、テレビが中でどんな処理をしているのか知らなくても、 番組を見ることはできます。自動車、エアコン、電子レンジ、その他なんでもそうです。どんなに複雑で、どんなにコード量が多くても、 使うために理解すべき項目、管理すべきデータが少なければ 開発や保守は容易に行えます。 さらにユーザにも優しいというおまけも付随してきます。逆に、そうしなければ大規模なコードは開発できないと思います。
 ソフトウエアの規模に関係なく、 理解しなければならない項目は少ないほど 管理すべきデータが少ないほど 効率良く開発でき、バグも少なく、保守も容易であることは、 説明するまでもないでしょう。

 「コード量 ≠ 開発工数、バグ数、保守工数」であり、
 「管理すべきデータ = 開発工数、バグ数、保守工数」なのです。

 また、オブジェクト指向と同じように、データの部品化や抽象化が効果を発揮すると 差分プログラミングができます。 差分プログラミングができるようになると、コード量が減り始めます。 よって、いかにして差分プログラミングを行える状態までコードの状態を持っていくのか?が 課題になるかと思います。これについても、データ指向を用いることで、素早くコードの質を高めることができると思っています。


●データの親子関係

 オブジェクト指向の話を聞くと… 「AとBとCのオブジェクトがあって、 それぞれがメッセージを送受信します。」 などという説明をよく聞きます。まるでわからない という話ではないのですが、 では、いざコーディング…というと 「A,B,C どのオブジェクトから生成するの?」 という疑問がわいてきます。これは、オブジェクト指向ではクラスの設計時に 「データの生成についてなんら考慮されていない」 ことが原因と思います。つまり、オブジェクト指向では、何らかの方法で 「A,B,Cが正しく生成された後の話しかしていない」のです。しかし、実際にコードを書こうとすると、 「A,B,C が正しく生成されるための何らかの条件」 が かなり多いことに気が付くでしょうし、 「A が無ければ B は存在しないなどの親子関係」 が存在することにも気が付くでしょう。
 一方、データ指向では「データの生成/破棄」に着目するとともに 「データの親子関係」に対しても同じように着目します。おそらくどんなデータにも、その優先度(親子関係)があると思われます。 これにより、「コードが書けない」という困ったことにはならないのです。 (余談ですが、逆に親子関係のないデータは「並列に生成できる」 という意味になり、コードの並列化に役立つと思います。)
 例えば、A というファイルがあり、 A というファイルの中に B と C のデータが入っていたとします。 これは B,C より A の方が優先度が高く「親データ」となり、 B,C は「Aの子供データ」となることを暗黙に示しています。B と C の間には優先度はなく、「兄弟データ」といったところでしょう。 A のデータの中を見てみない限り B,C が生成されるかどうかわからないのです。 B が無い可能性もありますし、C もまたしかりです。
 オブジェクト指向の熟練者であれば、B,C は 「データ無しの振る舞いを実装すれば良い」とおっしゃるでしょうが、 そもそも、データが無いのだから オブジェクトを生成するのはおかしいです。データ a の生成に失敗している(ファイル A が存在しない)のだから、 既に意味の無くなっているデータ b, c を生成するのは バグを作り込むだけだと思います。存在しないデータ b, c にアクセスしても正しい動作するように ちゃんとコードを記述する方が何倍もの労力を必要とすると思います。何より「存在しないものが存在することによる勘違い」が恐いのです。 「バグの元」な要素は早めに摘み取るべきです。
 コードにすると 親子関係なし
void  foo()
{
    A  a ; // a,b,c 特に無関係に生成/破棄される
    B  b ;
    C  c ;

    b.plug_to( a ) ; // a と接続
    c.plug_to( a ) ; // a と接続
       ;
}
親子関係あり
void  foo()
{
    A  a ;

    if ( a.Stat() == OK ) // a の生成チェック
    {
        // a が生成されていなければ b, c は存在しない
        B  b ;
        C  c ;

        b.plug_to( a ) ; // a と接続
        c.plug_to( a ) ; // a と接続
          ;
    }
}

もしくは
bool  foo()
{
    A  a ;

    if ( a.Stat() != OK ) // a の生成チェック
    {
        return  false ; // NG
    }

    // a が生成されていなければ b, c は存在しない 
    B  b ;
    C  c ;

    b.plug_to( a ) ; // a と接続
    c.plug_to( a ) ; // a と接続
      ;

    return true ; // OK
}
となります。
 ここで提案したいのは「データの生成時に、親子関係のチェックを行う方法」です。 上記の記述では、一度、b, c のインスタンスを作成した後に、plug_to() メソッドを使って a と接続しています。これをデータの生成時(コンストラクタ) で行うことで、メソッドの数を増やさずに、また処理もすっきりと書けると思います。 生成時チェックに変更
void  foo()
{
    A  a ;

    if ( a.Stat() == OK ) // a の生成チェック
    {
        B  b( a ) ; // b 生成時に a と接続
        C  c( a ) ; // c 生成時に a と接続
          ;
    }
}

もしくは
bool  foo()
{
    A  a ;

    if ( a.Stat() != OK ) // a の生成チェック
    {
        return false ; // NG
    }

    B  b( a ) ; // b 生成時に a と接続
    C  c( a ) ; // c 生成時に a と接続
      ;

    return true ; // OK
}
となります。
 これにより、コードはすっきりしますし、B, C に a と plug_to() する 前の処理(a が接続していない場合の処理)を考慮する必要も無くなりコード量を減らす ことができます。もし、plug_to() が呼ばれなかったら、B や C にはどのような処理を行うのか 悩むところです。 このことも、前で述べた「データの無い空の変数を作らない」という 考え方に通じるモノがあります。
 さらに、もう一つデータ指向を進めると、class A のメンバ内のローカル変数として、 b, c を生成するという書き方にもなります。 この考え方は、オブジェクト指向の「オブジェクトを平行(同じレベル)に生成する」という 考え方と異なるようですが、データ指向としては「より厳しいアクセス制限」と「より狭いアクセス範囲」にデータを置く という考え方として薦めています。
class A
{
    privatebool  Stat() 
    {
        ; // 生成チェック
    }

public:
    void  Do()
    {
        if ( Stat() != OK ) // 生成チェック
        {
            return ;
        }

        // クラス内のメソッドのレベルまで 
        // ローカルな位置に移動。
        B  b( this ) ;
        C  c( this ) ;
          ;
    }
} ;

int main()
{
    A  a ;

    a.Do() ;

    return 0 ;
}
 このコードが、データ指向の「データの親子関係」を適応したコードであるとも言えます。 オブジェクト指向に適応した最初のコードとの違いに注意してください。このあたりの「データの親子関係」を考慮した設計は データ指向の中でも重要な位置を占めています。


●オブジェクト指向の落とし穴

 例えば、A というオブジェクトが B と C という オブジェクトにメッセージを送っていて、 B と C が D というオブジェクトにメッセージを送って いるシステムが存在したとします。 図にすると↓こんな感じ。
  ┌──┬─┐
  ↑    ↓  ↓
  A    B  C    D
        ↓  ↓    ↑
        └─┴──┘

 これをそのままコードに実装しようすると、 D の生成/破棄を誰がするのか?という問題が出てきます。B が D を生成/破棄するとして、では A が B を必要とせず C のみを必要とした場合はどうするのか? 逆の場合もまたしかりです。さらに これを無理矢理実装しようとすると、 「オブジェクト指向って難しい」という結論に到達するようです。オブジェクト指向プログラミング時にあらわれる「落とし穴」といえるでしょう。
 例えば、深く考えずに書くと、d が2つできてしまって、 違う(↓こんな)結果になってしまったりします。
class D
{
public:
    void  mess_d( char *  in_str ) const
    {
        puts( in_str ) ; // b, c から受けたモノを表示
    }
} ;

class B
{
public:
    void  mess_b( char *  in_str ) const
    {
        D  d ; // 例えばこんなところで d を作ってみる
        d.mess_d( in_str ) ; // b → d に送る
    }
} ;

class C
{
public:
    void  mess_c( char *  in_str ) const
    {
        D  d ; // これじゃぁ、b, c 別々に d を
               // 作ることになるからダメじゃん。
        d.mess_d( in_str ) ; // c → d に送る
    }
} ;

class A
{
public:
    void  message() const
    {
        {
            B  b ;
            b.mess_b( "a to b" ) ; // a → b に送る
        }
        {
            C  c ;
            c.mess_c( "a to c" ) ; // a → c に送る
        }
    }
} ;

int  main()
{
    A  a ;
    a.message() ; // a → b, c に送る

    return 0 ;
}
 この問題の原因は おそらく前に話した「データの親子関係」だと思います。データの親子関係をぬきに、メッセージのやり取りだけの送受信の関係で オブジェクトの生成/破棄を行おうとしているために起こる問題だと思います。
 では、データの親子関係を調べてみましょう。D は B, C にとって無くてはならない重要な存在のようですから、
  A > D > B、C   (親>子)

とこのような関係になると推測されます。そして、メッセージの流れを追記すると
  ┌─────┬─┐
  ↑          ↓  ↓
  A > D > B、C
        ↑    ↓  ↓
        └──┴─┘

 こうなります。 「データの親子関係はメッセージの親子関係(メッセージの方向)とは無関係」 なのです。メッセージは親から子だけではなく、子から親へ渡る場合もあるのです。 むしろ、子から親へ渡るメッセージの方が多いと思います。このデータの親子関係をふまえて実装することはたやすいと思います。 C++のコードにすると(かなり省略してるのに…長い…^^;)
class D
{
public:
    void  mess_d( char * in_str ) const
    {
        puts( in_str ) ; // b, c から受けたモノを表示
    }
} ;

class B
{
public:
   void  mess_b(
        const D &  in_rd,
        char *     in_str
    ) const
    {
        in_rd.mess_d( in_str ) ; // b → d に送る
    }
} ;

class C
{
public:
    void  mess_c(
        const D &  in_rd,
        char *     in_str
    )
    {
        in_rd.mess_d( in_str ) ; // c → d に送る
    }
} ;

class A
{
public:
    void  message() const
    {
        D  d ;
        {
            B  b ;
            b.mess_b( d, "a to b" ) ; // a → b に送る
        }
        {
            C  c ;
            c.mess_c( d, "a to c" ) ; // a → c に送る
        }
    }
} ;

int  main()
{
    A  a ;
    a.message() ; // a → b, c に送る

    return 0 ;
}
こんな感じでしょうか。さすがに長いですね。
 ここで言いたいことは「データの親子関係とメッセージの向き」の話なのですが、 言いたいことはわかりますか?
 オブジェクト指向ではやはり、この「メッセージの向き」については あまり深く 考察されていないようです。(そもそも、データの親子関係という概念が無いからとも思いますが。)データの親子関係の概念が入ると、必ずメッセージの向きが、 親から子と、子から親への2方向の向きがあることがあきらかになるのですが、 オブジェクト指向では、このあたりをひとまとめに「メッセージ」としていますね。
 データ指向では、メッセージの向きより、データの親子関係を重視して考え、 実装することを推奨しますので、どうしても「親から子なのか?、子から親なのか?」 という問題がついてまわりますが、これが本来の姿ではないかと思います。
 今回はちょっと難しかったかもしれませんね。 あせることはありませんので、ゆっくり考えて下さい。


●3階層アプリケーション

 最近のシステムの設計方法というか、システム構築の概念のようなモノに 3階層アプリケーションがあります。 3階層とは、システムを
(1) プレゼンテーション層(GUI)
(2) 機能層(アルゴリズム/ロジック)
(3) データ層(データベース)
の3層に分けて考えるということを示しているようです。この考え方は、実際のシステム構築にも適応できるほどの よく出来た考え方だと思います。
一般に
(1) プレゼンテーション層 → View
(2) 機能層               → Process
(3) データ層             → Data
と表記されるようですが、View は Interface とした方が より概念的にあっていると思います。
余談になりますが、オブジェクト指向に当てはめてみると
(1) プレゼンテーション層 → パブリックなメソッドとメンバ
(2) 機能層               → プライベートなメソッド
(3) データ層             → プライベートなメンバ
となるのではないかと思います。
 一般的なシステム構築方法というか、処理の流れを考えると (一般的に「処理の流れを追い掛けながらシステムを設計/構築する」 という傾向を元にしています。) どうも Interface から処理の流れを追い掛けてシステムを 構築する「機能重視の設計」になっているように思えます。
 処理の流れ図にすると、このようになります。
  Interface : +--+           +-→
                 |           |
  Process   :    +-+  +-+  +-+
                   |  | |  |
  Data      :      +--+ +--+

 Interface を列挙し、より細かい Process に分け、Data アクセス部分を作りこんで行くという。 この処理の流れを追い掛けた構築方法は、トップダウン的な構築方法となります。
 ちなみにC++のコードをイメージすると、こんな↓感じ。
int main()
{
    Interface  intf ;         // intf から作る
    Process    proc( intf ) ; // intf → proc へ接続
    Data       data( proc ) ; // proc → data へ接続

    data.Do() ;               // data → proc → intf 

    return 0 ;
}
 これはイメージなので、あまり深く考えないでください。^^;
 このようにシステムを作ると おそらく 「ファイルがありません」とか「未サポートの機能です」という メッセージが頻繁に出るシステムになるでしょう。(^^; そうではなく、データ指向の考え方で 処理の流れを書きなおすと このようになります。
  Interface :     +--+           +--+
                  |  |           |  |
  Process   :   +-+  +-+  +-+  +-+  +-+
                |      |  | |  |      |
  Data      : +-+      +--+ +--+      +-→

 たいした違いではないのですが (途中は同じで「最初と最後が違う」だけです) 考え方が逆転しています。
 ちなみにC++のコードをイメージすると、こんな↓感じ。
int main()
{
    Data       data ;         // data から作る
    Process    proc( data ) ; // data → proc へ接続
    Interface  intf( proc ) ; // proc → intf へ接続

    intfe.Do() ;              // intfe → proc → data

    return 0 ;
}
(これもイメージです。^^;)
 Interface から構築/処理するのではなく 「Data から構築/処理する」のです。まず、どのような Data があるのか決定します。 そして、Data にアクセスする Prpcess を決定します。 その上で、Interface を構築するのです。この考え方の方がシステム設計/構築しやすいのです。なぜなら、Data の仕様は安定的で、設計初期の段階で 決定していて変更がかかりにくい。 一方、Interface の仕様は不安定で、設計後期の段階で 変更がかかりやすいからです。よって、Data を木の幹と考え、Interface を枝葉と考え システム構築する方がより自然だと言えます。
 余談になるのですが、 以前から Windows系プログラミングは設計/構築しにくいと思っていました。 このあたりが原因なのかもしれません。(^^;


●抽象化と再利用化

 「抽象化」とは、オブジェクト指向の概念に出てくる言葉の 「抽象化」のことです。オブジェクト指向では、クラスを作るときに「抽象化」と いう作業を行うのですが、この作業は主に、データ(メンバ)と 機能(メソッド)を「一般的な概念にまとめる」ということ を指しています。ここで問題とするのは、 安易に「クラスを抽象化して再利用しよう!」とうたい、 クラスを肥大化/複雑化させ、さらにその事を制限/警告する文も みられない。 この部分が「悪」の根源のような気がするのです。
 あえて極端な例を上げてみると。「人」という概念は一般的だと思います。 しかし、システム構築時にはそう一般的な概念ではないと思います。単純に「住所録」を作る場合の「人」という概念と、 「3Dグラフィックシステム」を作る場合の 「人」の概念はまったく違います。同じ「人」というクラスであっても構築しようとするシステムによって 内容がまったく異なるのです。 「住所録」の人クラスに「3次元座標データ」などあるはずは無いし 「3Dシステム」の人クラスに「電話番号」などというデータは 決して存在しないのです。それぞれの人クラスは「一般的な概念」ではなく「システム固有の概念」で まとめられているのです。そして一番困るのは、これらのクラスを再利用(差分プログラミング)する場合だと思います。
 これから作ろうとしているシステムに「人」という概念があるとして、はたして、
 「住所録の人クラス」を利用するべきか、
 「3Dシステムの人クラス」を使用すべきか、
 それとも、ゼロから作り直すべきか…。

 確かに「文字列(String)」や「時間(Time/Date)」などの よりデータに近い 底辺にある小さなクラスは再利用しやすいと思いますが、 少し高度な(他人の作った)クラスはなかなかそうはいかないのです。
 ここですこしまとめてみると

・クラスの抽象化は、構築するシステムによって異なる。
・クラスの抽象化は、構築する人の概念の違いによって異なる。
・こうして抽象化されたクラスは再利用しにくい。

となります。
 よって、すべてのクラスに対して 再利用化を考えて クラスをより抽象的に改良し、肥大化/複雑化すべきではないのです。むしろ、他システムへの再利用の配慮は無視して、 より、データに近い底辺の方向に、 シンプルに特化したクラスの構築のほうが そのシステムの開発や保守が楽になるのです。 また、肥大化/複雑化したクラスよりも、シンプルで小さいクラスの方が 再利用(差分プログラミング)もしやすいと思います。STL の std::string や std::vector はとても使いやすい(再利用しやすい) クラスですが、もっとシンプルでも良いと思います。
 ここでの結論は「抽象化/再利用化」にこだわり過ぎるのは 「メリット」どころか「デメリット」になるということ。とくに、データから遠い概念 (トップダウン/ボトムアップで言うところのトップに近い概念) は、無理に抽象化しても、再利用の可能性は低いと思います。 ならば、無理に再利用化に力を入れてクラスを肥大化させるよりも 再利用は無視して、シンプルで保守のしやすい 「わかりやすいクラス」の構築に力を入れるべきです。より、データに近いところを優先して「抽象化/再利用化」すべきなのです。


●プログラミング概念の全体像

 データ指向という言葉が、プログラミング概念のどのあたりの位置になるのか、 私の考えている概念を図にしてみます。注意していただきたい点は、この図は広く認知されているわけではなく、 単純に「私の頭の中にある概念を言葉にして並べてみた」というレベルの モノなので、なんの文献も参考にはしていません。よって、 一般の常識との違いがあるかもしれません。いや、あるでしょう。 それは、各個人の解釈の違いだということで、深く考えないでください。 また、この概念が正解であるなどと思い込まないでください。^^;  大きな流れとして、「プロシジャ指向」と「データ指向」からの 「オブジェクト指向」へのアプローチという流れがあると考えます。 これは、図のAとBの線によってあらわされています。私の概念では「オブジェクト指向」とは、まだまだ流動的で、 今後は「インターフェース指向」とあらわしている新たな 概念へ向かっている気がします。「インターフェース指向」とは、私が勝手につけた名前です。一般的な言葉ではありません。 簡単に説明すると STL でいう、イテレータのような 「アルゴリズムとオブジェクトをつなぐための実体の無いクラス (抽象化されたメソッドの集まり) のような概念を推奨する設計/実装」を示しているつもりです。
 ただイテレータは、データ指向な考えでは 「ポインタと同じように、必ずしも実体が保証されていない」とか、 「親から子への方向のメッセージしかない」という、ことがあるため、 「イテレータはインターフェース指向のサンプルそのものである」とは言い切れません。 現在、インターフェース指向に一番近い概念/実装がイテレータとアルゴリズムの関係だと 思っています。
 この概念を進めると、クラスとは、物理データを隠蔽し、アクセス制限とデータの管理を 主な実装とし、アクセス方法を抽象化したインターフェースを提供するモノへと 変わって行くように思えます。
 そしてアルゴリズムは、抽象化されたインターフェースに対して、 実装が行われ、直接物理データをアクセスすることは無くなる方向へ変わって行くとも思うのです。 こうすることで、再利用化や保守性の具体的な実装が行えるとも考えているのです。 その方向として、一番近い位置にあるのが STL だと思うのです。
 この図に、大きな意味を持たせたいとは思っていませんが、今後はCの流れのような アプローチが重要ではないかという思いを描かせてもらいました。まだまだ、漠然とした図なので、「ここは違う!」などと怒らないでくださいね。^^;


●ポインタ有害論

 K&Rの本にも、第5章ポインタと配列の最初のページに 「ポインタは goto 文と共に、理解することの不可能なプログラムをつくってしまう もとになることも多い。」と書かれていますが、 データ指向でもポインタの扱いは「悪」です。ポインタというのは、そもそもデータに付随するモノなのですが 実際にC言語でのポインタを見てみると、データの有無とはまったく無関係に 生成/破棄することができます。これは、データ指向の「どこで生まれてどこで死ぬか?」 という問題におおきくかかわっています。
 データの生成/破棄に完全にリンクしたポインタであれば それほど問題はないのですが、 そのような制約も無い多くの言語では粗製乱造が関の山です。 (C++でのリファレンス(参照)をデータにリンクしたポインタと考えることもできないことではないが、 リファレンスをポインタの一種とという考え方に対しては、私は少し違和感を覚えます。)
int main()
{
    char *  ptr ; 
    {
        char  buff_str[] = "test" ; 
        ptr = buff_str ;  
    }
        ; 
    puts( ptr ) ; // <- ここで表示されると
                  //    思ったら大間違い。^^; 
                  //    buff_str[]はすでに破棄されて 
                  //    いるので中身は不定です。
}
したがって、ポインタを使わないようにするしかありません。
 Java にはポインタはありません。 (無いのではなく完全に隠れている とも言えます。) (逆に すべてがポインタ とも言えるかもしれません。^^; ) しかし、Java でプログラムを書けないということはありません。 ポインタは無くてもプログラムは書けるのです。
 C言語などではポインタを避けてプログラムを書くことは 難しいことです。(C言語でポインタを使わないのは奇跡に近いですね^^; )こうなると「できるだけ使わないようにしましょう」としか 言えないのが現状です。

 ポインタを使うときにはできるだけ「データに密着した実装」となるように心がけましょう。

 C言語の goto文と同じように、正しく使えば問題はないのですが やたら使うと「バグの元」になります。 ポインタの使用には最深の注意をお願いします。 ● ポインタを無くすには  ポインタを無くすとバグを減らせる と書きましたが、 その具体的な方法を示します。
 まず、残念ながらC言語では不可能です。ポインタを無くすことはできません。^^;
 C++へ移行することをお薦めします。 ゴージャスなC言語として使うこともできますし、 無理にクラスなどの機能を使う必要も無いと思います。 (そこがC++の良いところでもあります。)
 C++であれば、 ポインタを撲滅するには、

・文字列は char * ではなく STL の std::string を使う。
・動的配列も malloc() ではなく STL の std::vector を使う。

 ポインタはこれらの STL の中に隠蔽されますから、 表面上ポインタという概念は出てきません。
 そして、引数宣言では

 ・ 'const &' (コンスト・リファレンス)または '&'(リファレンス)を使う。

 このことで引数のポインタ宣言を置き換えることができます。これらの記述を使うことで、100% とはいかないまでも あらかたのポインタは消えて行きます。
 ただ、やはり、高速化や、特殊なアルゴリズムを実現するのに どうしてもポインタが必要になる場合があります。 その場合は、なるべくクラスなどに隠蔽する (リソースの確保や解放は、コンストラクタ/デストラクタを使い確実に行う) ことで、ポインタの無い(少ない)安全なコードを書くことができます。


●継承有害論?

 オブジェクト指向には「継承」という考え方があり、 この「継承を使いこなすことがオブジェクト指向プログラミングである」 という風潮があります。しかし「データ指向」の考え方から言わせてもらうと 「継承は悪である」 となります。(いきなり暴言か?!^^;)
 ただし、継承という機能には大きく以下の2つの意味があると思っています。

・実装継承 (具象クラスの拡張)
・インターフェイス継承 (インターフェイス/抽象クラスの実装)

 今回ここで問題としているのは「実装継承」のほうです。 「インターフェース継承」にについては後に記述します。オブジェクト指向には「継承」の他に「包含」という考え方があるのですが、 なぜかこの「包含」を軽視しています。 この理由については まったくわかりません。ただの習慣のような気もします。 (できることなら知りたいと思いますが、どなたかわかりませんか?。)
 オブジェクト指向の「包含」の考え方は「継承」よりも、 簡単でわかりやすいと考え方だと思います。なぜなら「継承:AはBである」と抽象的というか観念的な説明に対して 「包含:AはBを持っている」と物理的な説明になり 「包含」の考え方のほうが直感的に理解しやすいのです。「自動車は乗り物である」というより「自動車はエンジンを持っている」と 考えたほうがわかりやすいし、 「データ指向」では 「データは何を含んでいるのか?」「何と何で構成されているのか?」 といったことが重要であり、 「乗り物」などという「抽象的な機能」については重要視しません。
 C++のコードで「継承」はこう書きます。
class A : public B  // class A は class B を継承する
{                   // class A は class B である??
    ;
}
「包含」はこう書きます。
class A
{
public:
    B  m_b ;  // class A は class B を包含する
    ;         // class A は class B を持っている
}
 「データ指向」では、class A に class B を「継承」すると 親クラス(スーパークラス)class B の持つデータや機能などが そのまま class A に継承され、「データの隠蔽」どころか class B のすべてががそのまま表に出てしまいます。 (カプセル化されないとも言われます。)
 データ指向ではこの点(親クラスのすべてが露呈すること)に「継承への不満」を感じます。 ここ、継承には「アクセス制限」や「情報隠蔽」という概念がないのです。そこにデータがあるなら、名前を付けて意識したいと思うのです。 そこにデータがあるなら、意識して必要なもののみを公開し、 必要の無いものは隠蔽すべきと思うのです。さらに、継承を1度ではなく2度3度と繰り返し継承することで、 事態はさらに悪化してしまいます。
 この継承の欠点は「包含」を使用することで解消されると思います。
class A
{
    // class A は class B を包含するが、外からは見えない。
    private: B  m_b ;
      ;
}
 これは、class B を隠蔽できることも意味しています。
 補足すると、 クラスの設計で「最低限のアクセスのみ public にする」のは前提の話でして、 それでも たいていのクラスに public が存在します。 (中にはコンストラクタのみの class もありますが。^^;)
 ここで危惧していることは、class の設計が最適であっても、 多くの場合、「継承」すれば子クラスの public は「増える」 というところなのです。そして、この「増え」は「継承」の回数に依存します。 継承を使えば使うほど増えるのです。さらに、結果として増えてしまったクラス内の public メンバや メソッドを保守するのも困難になります。「包含」であれば、これをコントロールすることができると思います。
 例えば、
class B
{
public:
    int  GetA() ; // 値 a を参照
    int  GetB() ; // 値 b を参照
     :
} ;

class A : public B  // class B を継承
{
public:
   int  GetC() ; // 値 c を参照
     :
} ;
となっていた場合、 GetA() が class B では使用していた(public の意味がある)けど、 class A では使用しないので public の意味が無くなったとします。それでも、class A では GetA() が見えてしまうのです。
そこで、包含を用いて
class A : 
{
    private: B    m_b ; // class B を包含

public:
    int  GetC() ;       // 値 c を参照

public:
    int  GetB()         // class B アクセス 追加
    {
        return m_b.GetB() ; // class B の値 m_b を参照
    }
} ;
とすれば、GetA() は見えなくなり、GetB() もそのまま使える。 (GetB() をスルーするコードを追記することになりますが)
 これはこれで継承より わかりやすくなり、保守しやすくなっていると思います。このような書き方を「デザインパターンの本(Gamma本)」の中では「委譲」と呼んでいるようです。 「継承」と「包含」の間にある概念と位置づけられているようです。おそらく、継承で書けるコードはそのまま包含でも書けると思います。 (現在私は、継承を使わずにコーディングしていますが、特に問題はおきていません。)確かに、継承は、オブジェクト指向の心髄でもありますし、 現在存在する多くのシステムで利用され、効果を発揮していることは 理解しています。 しかし、それは「包含では書けない」という理由にはならないと 思っています。さらに、包含で書いた場合のデメリットも見えていないので 現状では、「継承は悪」と言われる要素はあっても 「包含は悪」と言われる要素はないのではないでしょうか?そんなわけで、「データ指向」では「包含(委譲)」をお勧めします。


●インターフェース指向?

 この章では、インターフェースという言葉がたくさん出てきています。 「データ指向」の話を書きはじめてからだいぶ時間が経ちますが、 最近は、この「インターフェース」という言葉に「重要性」を感じはじめています。「データ指向」ではデータを隠蔽するためにクラスを用いてアクセス制限 (インターフェースを設ける)を推奨してきました。また、継承は悪であると言いながらもインターフェース継承は 否定していません。むしろ、私のコードにはインターフェース継承はたくさん使われいます。
 クラスの抽象化についても、使いにくくなるので意味が無いと書きました。 しかし、インターフェースを切り出すという意味でのクラスの抽象化となると話が別なのです。
 私の中でも、まだ混沌とした考え方なのですが、 「インターフェースと実装を分けて考える」とか 「クラスの抽象化ではなくて、インターフェースとして抽象化すべきでは」という 考えがあり、その概念を「インターフェース指向」と呼びはじめています。いまだ、まとまった概念ではないので、 ここではっきりと「こう!」とは言えないのですが、 今後のプログラミングにおける(私の中での)キーワードになるのは 確かです。
 「データ指向」につづき「インターフェース指向」の概念がどう 表面化するのかわかりませんが、 プログラミングに興味をお持ちのみなさんもこのキーワードについて 考えてみてください。 そして、なにか思い付いたら私にもそのことを教えてください^^;。よろしくお願いします。

 悩めるプログラマより。^^;


●まとめ

 以上で 第二章は終わりです。  この章では、データ指向が他のプログラミングの概念に対して、どのような インパクトを与えるのか?をまとめてみたつもりです。 ここまで読んでくれた方の頭の中に、 今までとは違った角度からのプログラミングの概念が一つでも増えていれば、 この章を書いた意味があると思っています。
 第一章では、コードの話を主に書きましたが、それは表面上というか どう具体化するか?と言う話が主でした。 実際にはデータ指向とはシステム設計に対する アプローチの方法の一つの考え方なので、 最終的にコード上にどう現れるのかは言語によって異なる話なのです。 このデータ指向は、言語を超えた概念だと思っています。
 この文章を読んでくれたみなさんの中に、何かが残れば幸いです。

 ご意見、ご質問など、掲示板の方で常に受け付けています。つづく…といいですね。(^^;



データ指向の話 第i一章「基礎編」へ戻る。

last update 2003/02/06
since 1998/07/14


やまざきのおすすめエレクトロニクス


やまざきのおすすめ本

やまざきのおすすめDVD

やまざきのおすすめCD



Copyright(c) 1998-2006.
YAMAZAKI Satoshi.
All rights reserved.

since 1997/12/15


このページのURLをメールで送る(友人・知人に教えてあげる)
このページを「お気に入り」に追加する(忘れないように…)
● お手紙はこちら↓。仕事の話は大歓迎です。(忙しくて返信できなかったらごめんなさい。)