オブジェクトに仕事をさせる、ということ

関数に仕事をさせる、ということ

あるまとまった処理があるとき、それを「関数」(サブルーチン)にするのは、 ド素人を除き、プログラマなら誰でも日常的にやっていることでしょう。

たとえばC言語では、「文字列を数値に変換する」という関数があります。

「文字列」と「数値」はCでは全く違う型であり、 「"123"」という文字列はそのままでは「123」という数値として 扱うことはできません。 「文字列を数値に変換する」は、自力でも書こうと思えば書けますが、 まともなプログラマならatoi()などのライブラリ関数を使うことでしょう ※1

  int a;
  char str[] ="123";

  a = atoi(str);

もし、ライブラリ関数にatoi()相当の関数がなかったとしたら、 きっと自分で同様の関数を作るはずです。

ライブラリ関数を使うにせよ自分で同等の関数を書くにせよ、 ある程度まとまった処理は関数に切り出すものです。 「文字列を数値に変換する」という処理が必要になるたびに、 毎回そこに長々とコードを書いていたら「バカ」です。

そして、いったん関数を作ってしまったら、 今後「文字列を数値に変換する」という処理が必要になったときには その関数を呼び出せばよいわけです。

この場合、atoi()という関数に「文字列を数値に変換する」という仕事をさせている、 と言うことができるでしょう。 atoi()を使う側は、atoi()が内部で具体的にどんな処理を行っているのか、 その詳細を知る必要はありません。

…何当たり前のことを書いてるんだって? いや、お気持ちはわかりますがもう少しお付き合いください。

データを「ひとつだけ」抱えることのデメリット

Cの標準ライブラリの中から、もうひとつ例を挙げます。

Cにはstrtok()という関数があり、これを使うと、 文字列を指定した区切り文字で分割することができます。

たとえば、「蔵書管理プログラム」で、本のデータを以下のような 「コロン区切り」の形式で、テキストファイルで保存しているとします。

書名:著者名:訳者名:価格[改行]

ただ、共著・共訳の場合もありますから、著者名と訳者名については 複数書けるようにした方がよさそうです。 そこで、著者名、訳者名はコンマで区切って記述することにすると、 「蔵書管理プログラム」のデータファイルは具体的には以下のようになるでしょう。

UMLリファレンスマニュアル:James Rumbaugh,Grady Booch,Ivar Jacobson:石塚 圭樹,日本ラショナルソフトウェア:6400
プログラミング言語Java:Ken Arnold,David Holmes,James Gosling:柴田 芳樹:3800
Java言語仕様:James Gosling,Guy Steel,Bill Joy,Gilad Bracha:村上 雅章:5500

こういうテキストファイルを「蔵書管理プログラム」に読み込むためには、 まず、コロンで区切られた文字列を切り出す必要があるでしょう。 strtok()では、以下のようにすることでそれを実現できます。

  char *bookname;
  char *writer;
  char *translator;
  char *price;

  /* 書名を取り出す。
     lineに、データファイルの1行が格納されているものとする。 */  
  bookname = strtok(line, ":");
  /* 著者名を取り出す */  
  writer = strtok(NULL, ":");
  /* 訳者名を取り出す */  
  translator = strtok(NULL, ":");
  /* 価格を取り出す */
  price = strtok(NULL, ":");

strtok()では、最初の呼び出しのときにだけ、切り出す元になる文字列 (この場合はline)を渡し、以後はNULLと区切り文字を渡すだけで、 次々に切り出した文字列を返してくれます ※2

呼び出す側は実装の詳細について意識する必要はありません。 つまり、strtok()の使用者は、「文字列を区切り文字で切り出す」という作業を strtok()に「お任せ」していると言えます。

このように、ある程度まとまった仕事を「関数」に切り出すことで、 それを使う側は実装詳細に立ち入る必要がなくなりますし、 その関数は、おそらく他の場所で再利用することも可能になるでしょう。 オブジェクト指向のメリットとしてよく言われる 「実装の隠蔽」とか「再利用性」といったものは、 オブジェクト指向とは無関係に、 「ライブラリ」という形でずっと昔から提供されていたのです。

---しかし、C言語の例では、atoi()はともかくとしてstrtok()の方は、 かなり致命的な問題があります。 「コロンによる切り出し」を行っている最中に「コンマによる切り出し」 を行おうとすると、strtok()は正常動作しなくなるのです。

  ...
  /* 書名を取り出す */
  bookname = strtok(line, ":");
  /* 著者名(複数)を取り出す */  
  writers = strtok(NULL, ":");
  /* コンマで区切られた著者名を順に切り出す */  
  i = 0;
  writer[i] = strtok(writers, ",");
  i++;
  while (name != NULL) {
      writer[i] = strtok(NULL, ",");
      i++;
  }
  ...

strtok()では、2回目以降の呼び出しでは引数として NULLと区切り文字しか与えていないため、 このような仕様で文字列をコロンで区切って切り出すには、 「どこまで処理を行ったか」ということを、strtok()側で 覚えておいてくれなければいけません。 ただし、そのための領域は「ひとつしかない」ため、 「コロンで区切る」という処理の最中に「コンマで区切る」という処理を割り込ませると、 ひとつしかない領域が上書きされてしまうわけです。

大規模なプログラム開発は、多くのプログラマにより行われます。 「コロンで区切る」処理と「コンマで区切る」処理は、 別のプログラマが担当するかもしれません。 つまり、たとえばあなたが「この文字列をコンマで区切るプログラムを書いてね」 と依頼されたとき、迂闊にstrtok()を使うことはできません。 あなたの書くプログラムの外側で、既にstrtok()が使われているかもしれないからです。

ところで、世間一般では、「グローバル変数はよくない」と言われています。 なぜいけないのかといえば、「グローバル変数は どこからでも書き換えることができるからよくない」という理由が 挙げられることが多いようです。 それはそれで確かに問題ではあると思うのですが、 このケースでは、「どこまで処理を行ったか」を押さえている変数は strtok()の中に閉じています。外部から勝手に書き換えることはできません。 この意味では、 その変数は「グローバル変数」ではないということになるのでしょう。

にも関わらず、strtok()は、 「よそで使われていないかどうか」を 強く意識しなければ使えない関数になってしまっています。 strtok()は「どこまで処理を行ったか」をいうことを変数で記憶しているわけですが、 その変数は、間接的とはいえ、誰かがstrtok()を使うたびに書き換えられてしまうのです。

この問題は「グローバル変数」と同種の問題と言えます ※3

何が問題なのかと言えば、結局、それが「ひとつしかない」ということです。

Javaの場合

オブジェクト指向言語であるJavaでも、strtok()に相当する機能は クラスライブラリで提供されています。

Javaの場合、StringTokenizerというクラスを使用します。

  ...
  // コロンで区切るためのStringTokenizerを生成
  StringTokenizer st = new StringTokenizer(line, ":");
  // 著者名を(複数)取り出す
  writers = st.nextToken();

  // コンマで区切られた著者名を順に切り出すために
  // 新たなStringTokenizerを生成
  StringTokenizer st2 = new StringTokenizer(writers, ",");
  i = 0;
  while (st2.hasMoreTokens()) {
    writer[i] = st2.nextToken();
    i++;
  }
  ...

Javaの場合、「コロンで切り出す」という処理のためにStringTokenizerを newする必要がありますが、 「コンマで切り出す」という処理を行う際には、新たに別のStringTokenizerを 生成することができます。これにより、Cにあった、

つまり、たとえばあなたが「この文字列をコンマで区切るプログラムを書いてね」 と依頼されたとき、迂闊にstrtok()を使うことはできません。 あなたの書くプログラムの外側で、既にstrtok()が使われているかもしれないからです。

という問題は発生しません。この差はどこから出てきたのかと言えば、 Javaでは、必要に応じて新たなStringTokenizerを作り出すことができる ということからです。

Cのstrtok()でも、strtok()という関数にお願いして、 「文字列を切り出す」という仕事をさせることができました。

しかし、strtok()は「ひとりしかいない」ため、 「コロンで区切る」という仕事をさせている最中に 「コンマで区切る」という仕事をさせると混乱してしまったわけです。 それに対し、JavaのStringTokenizerでは、 必要に応じて新たなStringTokenizerを作り出すことができます。 これなら、上記のような問題は発生しません。 利用者側の必要に応じて、「自分専用の」StringTokenizerを作ることができるからです。

つまり、

ということです。

「どの」オブジェクトに仕事をさせればよいのか

ここまで述べてきたように、オブジェクト指向では、 複数存在するかもしれない「オブジェクト」に仕事をさせるわけですが、 オブジェクトが複数存在するかもしれない、ということは、 オブジェクトに仕事をさせるときは 「どのオブジェクトに頼むのか」をはっきりさせなければならない、 ということです。

StringTokenizerの例では、

  writers = st.nextToken();
  ...
    writer[i] = st2.nextToken();

このように、st, st2という指定を行うことで、 「どのオブジェクトに仕事をさせるのか」を明示的に示していました。

前に説明したオセロの例でも、 board.c内にstaticで盤面を保持している場合は

  board_put(x, y, senteOrGote);

と書くだけで石を置くことができたのに、 board_create()を使ったマルチプルインスタンス版では

  board_put(board, x, y, senteOrGote);

と、第1引数でboardを指定しなければならないようになっています。

---オブジェクト指向を既にマスターしている人からすれば、 「何を当たり前のことを言っているんだ?」 と思えるかもしれません。 しかし、私の経験からすると、オブジェクト指向の初学者は、 かなり高い確率でここでつまづくように思えます。

既に掲示板に書いた話ですが、以前、新人向けのJava研修の講師をやったとき、 X-Drawという簡単なドローツールを作ってもらったことがあります。

そして、とある新人君は、(AWTの)Canvasに線を引きたい、という時、 その場でCanvasをnewしてそのCanvasに線を引いてくれました。 もちろんそのCanvasと、実際に画面に貼られているCanvasは違うCanvasですから、 画面に線は表示されず、彼は悩んでいたわけです。

また別の新人君は、描画した図形を保持する「ShapeCollection」というクラスについて、 画面の再描画のため必要になったところでいきなりnewしてくれました。 当然、新たに作り出されたShapeCollectionは空っぽなので、 画面には何も描画されませんでしたけど ※4

「わかっている人」からすれば、「んなアホな」と思えるようなポカでしょう。 実際、私と一緒に研修の講師役をしていた同僚は、そんなことを言っていました。

しかし、オブジェクト指向の初学者がひっかかるのは、 実はこういうところなんじゃないかと私は思います。

Cなどの言語では、「関数」にお願いすれば、何らかの仕事をしてもらうことができた。 それに対しオブジェクト指向言語では「オブジェクト」にお願いするわけですが、 そのためには「どのオブジェクトにお願いするか」を 明確にしなければいけないわけです。 もっとも、オブジェクトは複数生成することができるのだから、 「どのオブジェクトか」を指定しなければいけないのは当たり前のことなのですが、 それを理解するには、「オブジェクトは複数生成することができる」ということを あらかじめ強く意識していなければいけない、と思うわけです。

ここまでの例を図にすると、以下のようになるでしょう。


まとめ



ひとつ上のページに戻る | ひとつ前のページ | ひとつ後のページ | トップページに戻る
mailto:PXU00211@nifty.ne.jp

このページに対してご意見・ご質問・ご感想等をいただいた場合、 公開することがあります。