Hatena::ブログ(Diary)

プログラミング言語を作る日記

2010-12-06 「憂鬱なプログラマのためのオブジェクト指向開発講座」はどうトンデ

[]「憂鬱なプログラマのためのオブジェクト指向開発講座」はどうトンデモなのか

前回の続きです。

なお、私が持っているのは初版第10刷、正誤表がこちらにあるようです。

この人、Cで開発したことあるのかな

オブジェクト指向の教科書ではよくあることですが、この本も、Cによる開発とオブジェクト指向言語(この本の場合はC++)による開発を対比し、「C++の方がこんなにいいでしょ」という説明が随所にあります。そのこと自体は悪いことだとは思いません。

しかし、そういうことを書くなら、Cによるまともな開発についての知識が必要なんじゃないでしょうか。

p.30

クラスの宣言は一般的にヘッダファイルに記述します.C言語でのプログラミングでは,ヘッダファイルをプログラマが書く機会というのはあまり多くありませんでしたが,C++ではクラスを作るたびにヘッダファイルを書くことになります.

いや、本当に本当の入門者でない限り、Cプログラマだってヘッダファイルぐらい書きますってば。ていうか、経験を積んだCプログラマほど、.cファイルよりもヘッダファイルを大事にするものです*1

p.89

C++でもっとも一般的なクラスインスタンスの生成方法は,動的にヒーブ領域に取る,という方法です.つまり,Cでいえばmalloc()です.これを意外に思った人は多いのではないでしょうか.なぜならば,C言語でのプログラミングでは,malloc()でのデータ管理というのはとてもマイナーな方法だからです.しかし,C言語で今ひとつ脇役であった構造体が,C++でクラスに姿を変えて主役になったように,malloc()も姿を変えて主役になったのです.

何に姿を変えたのでしょうか.それはnewです.

この本の連載時の時代状況を考えても、Cにおいてもmalloc()はマイナーではなかったと思います。もちろん今でも状況によっては気楽には使えないかもしれませんが、そういう状況なら、C++のnewだって同じでしょう。構造体が「今ひとつ脇役」というのもどこの世界の話なんだか。

不完全型のような、Cにおける必須のテクニックも「なかったこと」にされています。

p.28

このようにクラスについてわかってくると,C言語のデータに対する考え方は実に楽観的で,構造体などはなんとも危うい仕組みだと思えてきます.FILE構造体なども,絶対に内部に触れてはいけないものなのですが,それを防ぐ仕組みがなにもなく,むき出しになっています.

FILEが無防備に公開されているのは、stdio.hが古いことと、マクロとかの都合によるもの(たぶん)であって、Cの言語仕様としては非公開にできるので『それを防ぐ仕組みがなにもなく』は間違いです*2

インベーダーの話

インベーダーについては前回も書いたのですが、4章のまとめ的なところでクラス図が登場しています。

p.83

f:id:kmaebashi:20101206014311p:image

関連や継承がないのは「まだ習ってないところ」だからです。それはさておき、

  • この「戦車」の右に移動、左に移動というメソッドは、誰が呼んでくれるんでしょうか。「インベーダー」も同様。
  • 細かいことのようですが、インベーダーミサイルや戦車ミサイルに「縦位置」だけあって横位置がないのはなぜなんでしょうか(単なる誤植?)。

クラス図といえばこの章には以下のようなクラス図もあって、

p.72

f:id:kmaebashi:20101206014312p:image

「生徒」に「平均点算出」とかのメソッドを付けてしまうのは、私は設計としておかしいと思うのですが、それはまあいいとして、「テスト」の属性に挙げられている「点数」は何でしょうか。そもそもこの「テスト」のインスタンスは、1回のテストの実施につき1個なのでしょうか。1回のテストの実施の中の、個別の生徒で1個なのでしょうか。

末尾スペース除去可能文字列クラス

p.134から、「プログラムレベルでの継承の例」として、文字列クラスを継承した「末尾スペース除去可能文字列クラス」を作っています(ある程度心得がある人なら、こう聞いただけで頭がクラクラしてくるでしょうが)。

ちょっと考えればわかることですが、末尾スペース除去の対象としたい文字列は、たとえばファイルから読むなりGUIから入力されたりしたもので、そのクラスは「文字列」です。「末尾スペース除去可能文字列クラス」を作ったって、一度生成されたインスタンスのクラスが変わることはないのですから役に立ちません*3

実は拙著「Java謎+落とし穴徹底解明」の「3.5.2 ケーススタディ――Point2Dに機能追加」という項は、執筆終盤にこの本を見て、「こんなアホウなことを考える人がいるんだ」とあわててねじ込んだ項です。

この本では、さらにその後、オーバーライドの例として、既存の文字列クラスのsetterをオーバーライドして「末尾のスペースが必ず削除される文字列」クラスも作っています*4

これはとても素晴らしいことです.なぜなら,「文字列」クラスをカスタマイズしたにもかかわらず,その文字列クラスにはまったく手を触れる必要がないのです.従来の方法では,モジュールをカスタマイズすると,今までそのモジュールを使用していた部分のテストは一からやり直しでした.そしてそのモジュールを使用している他のプログラムのソースには,旧バージョンのモジュールが生き続けることになりがちです.これは,同時に複数のバージョンのモジュールをメンテナンスするということになり,保守の面で深刻な問題になっていました.

しかしオブジェクト指向では,そのようなことを気に止めることすら時間の無駄です.

継承を使用すれば,過去のことを何も考えずにばりばりカスタマイズしてもまったく問題がありません.なぜなら,どんなに派手なカスタマイズをしても,その基底クラスのモジュールのソースは1バイトも変わっていないからです.

基底クラスのモジュールのソースは1バイトも変わっていないかもしれませんが、こんなわけのわからない文字列のインスタンスを、たとえば引数とかで渡された側はたまったものではないでしょう。もっともC++の場合、インスタンスの(ポインタでない)実体を扱えるという変わった言語なので、引数で渡される際には文字列になっており問題ないのかもしれませんが、そういうことなら、そもそもsetterをオーバーライドすること自体が役に立ちません(C++ではポリモルフィズムポインタ経由でなければ動かないので)。

ところで、setterをオーバーライドして文字列末尾の空白を削除するというのなら、派生側のsetterは、どのようにして基底クラスが保持している文字列のメンバ変数にアクセスするのでしょうか。基底クラスがメンバ変数をpublicもしくはprotectedにしているのでない限り直接はアクセスできませんから*5、アクセスするには基底クラス側のsetterを呼ぶ必要があります。そのためには「基底クラス名::メソッド名()」とか書く必要があるのですが、この本の中には、これに関する説明はありません。これじゃわからないだろう、と思ったら、やっぱりわからない方がいらっしゃったようです。

http://www.seshop.com/book/qa/14479/thread

この場合は、派生クラス側でスペースを除去し、そこでできた文字列を、基底クラス側の「文字列設定」メソッドを呼んで基底クラスの「文字列」というデータに設定します。ただ、わかりづらいことも否めないと思いますので、今後の検討材料とさせていただきたいと思います。

だってさ。

時代の問題?

Web上のこの本の批判や、前回の記事のブックマークコメントで「この本は古いからしょうがないよね」的な意見が散見されました。確かにこの本には、「当時はみんなこんな(おかしな)こと言ってたねえ」という点もあります。「メンバ関数は必ずvirtual、publicでなくてもprotectedにしとけ」とか。

でも、上で挙げたようなことは単純に間違いであり、「古いからしょうがない」と擁護できるような性質のものではないのではないでしょうか。

ところで、「昔のオブジェクト指向では常識だった」といったことを考えるに、この業界内でのプログラミングの方法論がA→B→Cと遷移したとき、BがAよりはずいぶんマシであり、でもCはBよりずっとよい、というのなら、方法論は進化を続けているということです。Bの時点でCを思いついていた人がいなかったのはしょうがない。

でも、Bが、Aと比べても特にいいことないよね、ということなら、B時点でBを称揚した人は単に間違っていたということです。そして、そういう人が業界内にたくさんいたようなら、今あるCも、疑いの目で見る必要があるように思います。オブジェクト指向では、このあたり、どうなのでしょうか。

その他細かいこと

p.362

さて,それらを学習する前に一つ知っておかなければならないことがあります.これは「実体」です.

C言語では,関数に変数を渡すときは,値渡しとポインタ渡しの2種類の方法がありました.C++では,さらにもう一つの方法として,実体渡しという方法があります.

実体渡しは,値渡しとポインタ渡しの中間のような方法です.

まず、従来からある「値渡し」と,ここで学ぶ「実体渡し」の違いについて考えてみましょう.値渡しでは,関数に渡されるのは,あくまでもその変数の「コピー」です.ですから,渡されたデータをいくら書き換えても,元のデータには全く影響がありません.しかし、実体渡しの場合はその「変数そのもの」,つまり実体が渡ります.これを書き換えたら,元データにはもちろん影響します.それは,元データそのものなのですから.

私の知る限りにおいて、「実体渡し」という言葉をこういう意味(つまり「参照渡し」の意味)で使っている例は、この本と、この本の読書ノート的なWeb記事でしか見たことがありません。たいてい、「実体渡し」と言えば「値渡し」の方を意味するんじゃないでしょうか。

言っていることはわかります。私などは「ポインタじゃなくて実体を渡す」という意味で「実体渡し」という言葉を使ってきたわけですが*6、「コピーじゃなくて実体を渡す」という意味で「実体渡し」という言葉を使って悪いことはないでしょう(どうせきちんと定義された用語ではないので)。でも、あまり一般的な使い方とは思えません。私の周りだけかもしれませんが、Cプログラマにとって「ポインタじゃなくて実体」という言い回しはかなりされると思っていて、わざわざ混乱を招くような用語を使う意味はあるのでしょうか。この本の中でも、p.173には、集約の実装方法として

通常の関連をC++で実装する際には,メンバにそのクラスのポインタを持つことによって実装する,ということは以前学習した通りです.それに対して集約の実装は,メンバにクラスの実体を持ちます.ポインタではなく,実体なのです(LIST7-4).

という記述もあって、どう解釈したものかと思っているのですが。

また、グローバル変数について以下のような記述があって、

p.207

C言語のプログラミングにおいて,一般的に「グローバル変数は基本的に使ってはいけないもの」とされています.ほとんどの人はそれに反対はしないでしょう.

(中略)

この問題をオブジェクト指向的視点から見ると,これは「解決できない問題」というよりは,「解決しようとせずにほったらかしになっている問題」というようにしか見えないといっても過言ではありません.なぜならば,オブジェクト指向の概念に基づいてプログラミングを行うと,グローバル変数に似た構造の変数が無数に登場しますし,そしてそれを管理することが当然になっているからです.

それはなんでしょうか.(もうおわかりだと思いますが)それはクラスのデータメンバです.

なぜグローバル変数が他の変数と違って特別扱いされるかというと,それは関数が終わっても消えてなくならず,その結果,複数の関数から読み書きが可能だからだといえるでしょう.そしてこれはまさに,クラスの中でのデータメンバと同じ性質です.なぜならデータメンバは,複数のメンバ関数から読み書きされるというのが基本的な性質であり,それがメンバ関数内で宣言される通常の変数ともっとも違う点だからです.もし,グローバル変数という存在が持っているそのような性質そのものに問題があるのだったら,それはクラスの構造,さらにはオブジェクト指向という概念自体が否定されることになってしまいます.

クラスのデータメンバは、確かに複数のメンバ関数から読み書きされ、クラスが大きくなりすぎればグローバル変数的な様相を呈します。しかし、通常のデータメンバはインスタンス単位です。グローバル変数と対比すべきはstaticなデータメンバで、この本ではp.156からその説明が出てきます――が、ここではグローバル変数の話はカケラも出てきません。

どうもこの人、システムに対し1個だけ存在するのか複数存在するのかとか、多重度とか、そういったところの扱いがずいぶん無頓着に感じます。上のp.72のクラス図で、「テスト」が何に対してひとつなのかが変なことになっているのもその現れじゃないかなあ。

*1:p.255には『これはC言語の頃からの原則ですが,「関数プロトタイプ宣言やクラスなどの宣言」は.hファイル,「その実際の実装」は.c,.cppファイルになります』という記述もあるのですが。

*2:少なくともFILE構造体は一般ユーザはポインタ経由でしか使わないので。

*3:いったんインスタンスまで作り直してしまうなら別ですが。

*4:追記:「末尾スペース除去可能文字列クラス」はどう考えても役に立ちませんが、「末尾のスペースが必ず削除される文字列」クラスのほうは、使い道がなくもない気はします。よい設計かどうかはさておき。

*5:この本でも、メンバ変数はprivateにすることを勧めています。

*6:本とかでは「値渡し」と書いてきたと思いますが

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/kmaebashi/20101206/p1