プログラマというお仕事

プログラマは職人だ!プロならもっと腕を磨け。
世の中ヘボいプログラマが多すぎる。
少々過激な意見でも言っちゃうよ。

オブジェクト指向

オブジェクト指向超入門

世の中のいわゆるプログラマ、システムエンジニアと呼ばれる人たちのうち、オブジェクト指向を本当に理解して実践できる人はほとんどいません。私のこれまでの経験では5%くらいがいいとこかな、という実感です。要するにプログラマをどっかから20人かき集めてきたら、その中に1人くらいわかってる奴がいればラッキーという事です。もちろんプロの世界での話ですよ。

プロのくせに勉強が足りない!と言ってしまえばそれまでで、実際問題それはその通りだと思います。思いますが、教える側にも問題はあります。きちんと他人に教えられる人があまりに少なすぎるんですよね。
考えてみれば当たり前で、本人がわかってるのと、それを他人にも伝えられるのとは別の話で、わかってる人が5%しかいないなら、その5%のうちの何%が他人にうまく説明できるかって考えれば、全体の1%にも満たないわけですね。

プログラミングの初級〜中級者レベルの人が初めてオブジェクト指向を勉強しようとする時に、大抵の場合まずは「オブジェクト指向入門」とかいうタイトルの本やサイトなどを探す訳ですが、このレベルの人に理解できるようにオブジェクト指向をやさしく解説している記事はほとんど見たことがありません。

オブジェクト指向とは、現実世界の物理的あるいは抽象的なモノや事象を、属性と操作の集合としてモデリングすることです。
オブジェクト指向ではオブジェクトの振る舞いをメソッドとして定義し、オブジェクト同士が相互にメッセージを交換する事によりシステム全体の振る舞いを決めていきます。

はぁ?
こういう文章を書く人は、入門レベルの人間がこれを読んで「なるほど!」と納得できると、本気で思ってるんでしょうか。

難しい事を難しく説明するのは、実はそれほど大変なことではありません。難しいことを如何に易しくわかりやすく説明するかの方が、よっぽど大変で頭を使います。さらに言うと、オブジェクト指向はここまで難解な日本語を使わないと説明できないほど難しい概念ではありません。
結局こういうのは簡単な事をわざわざ難しく説明しているだけで、こんな説明をいきなりされるもんだから、オブジェクト指向はきっと物凄く難しくて特殊な考え方なんだろう、と勘違いしてしまう人が多いのだと思います。そして中途半端にわかったようなわからないような(結局のところ本質は何もわかってない)状態にさせられるプログラマのなんと多いことか。

でそのまま実際のオブジェクト指向言語での開発現場に突っ込まれてソース修正とかやらされる訳ですよ。ところがソースを見てみると思ったよりどうって事もなく、それまでのプログラミング知識で別にどうにかなりそうだなって感じで、わかった気になってそのまま数年経ったら・・・はい、ヘボプログラマがまた一人生産されましたっていう結末です。
プロジェクトに放り込まれた時にあなたが見たそのソースは、今のあなたと同じようなヘボい先輩方が書いたもんだから、それをお手本にしちゃったあなたもヘボくなってしまい、そんなあなたの書いたソースをお手本にしている新人君は未来のヘボプログラマ候補で・・・というように、ヘボプログラマ量産スパイラルが出来上がってしまっているのが現状ではないでしょうか。

そこで普段初心者の方を相手にプログラミングを教えている経験から、『要するにこういうこと』っていうオブジェクト指向の考え方を、なるべくわかりやすい日本語で説明してみたいと思います。

オブジェクト指向設計をああだこうだと深く議論していくと、手段と目的がわかんなくなっちゃう人が時々います。
『こういう場合はAdapterパターンを使って、このクラスをこういう風に継承させて、コイツはこのインターフェースを使うようにして、そんでもってホレホレ・・・』と複雑怪奇なクラス図を描きだします。
どちらかというと勉強熱心でなおかつ自分はイケてるエンジニアだと思ってる人に多いですね。確かにある点では優れている部分もあるし全くダメダメではないんだけど、それにしても、ねぇ・・・ちょっと難しすぎんだよなぁ。そこまでこねくり回さなくても、もっとシンプルにできるじゃん。
というような事を言うと『お前はデザインパターンも知らんのか』などとバカにしてきたりして。
これってあんたの自己満足が8割だろ?て感じで、クラス設計そのものが目的になっちゃうパターンにはまる事があるんですよね。

オブジェクト指向の目的は、同じ処理を複数の箇所に分散させない、処理する範囲を明確に切り分ける、などにより結局のところバグや仕様変更に強い、メンテナンス性の高いシステムを作るためのものであって、オブジェクト指向そのものは単なる手段でしかありません。
そんなわけで基本中の基本となる概念の部分を中心に説明します。実はその部分こそがオブジェクト指向の超チョ〜大事なところで、そこがわかってないで、アクセス制御だとかインターフェースだとかの文法的な知識だけなもんだから、オブジェクト指向の本質がわからない人ばっかりなのだと思うのです。

オブジェクト指向より前は

オブジェクト指向超入門〜第2回〜

オブジェクト指向よりも前の時代のプログラミングスタイルでは、仕様をシステムでどのように実現するかという処理(手続き)がまずあって、その処理の結果としてデータが出来上がります。出来上がったデータは外部のDBなどに保存するなり、そのデータを元にまた別の処理を実行するなりして、全体としてのシステムが作られます。
こういうのを「手続き指向」などと呼んだりします。手続き指向では処理をまず最初に決めて、その処理の結果としてデータが作られる訳で、処理が主,データが従の関係になります。

こう書いても抽象的でピンとこない人が大半でしょうから、具体例を挙げながら説明します。なおここから先はC言語での実装例で説明しますが、単純な例なのでJAVAなどの他の言語しか知らない人でも容易に理解できると思います。もうひとつ念のために断っておくと、実装例のコードは文法的に間違っている場合があります。文法に忠実に書く事よりも論点を重視する為に細かい記法をあえて無視していますので、そのままコンパイラに通して「エラー出るじゃねえか!」と言わないように。

1.構造体を定義する

例として日付をデータとして保持したいというケースを考えます。C言語の変数には整数や文字などの基本的な型しか無いので、日付のようなデータを変数として保持したい場合にはちょっと工夫が必要です。そこで整数型の変数を3つ用意して、それぞれ

int yy; // 年
int mm; // 月
int dd; // 日

のように年月日を保持する事を考えます。このような変数は常に3つセットで使ってはじめて意味があるので、こういう場合にCでは「構造体」というものを定義します。

struct MyDate {
  int yy; // 年
  int mm; // 月
  int dd; // 日
}

MyDateという名前の構造体を定義しました。(MyDateってダサダサな名前はどうなんだ、という意見は置いといて・・・)
いったんこのような定義を作ると、日付型という型があたかも最初から言語に用意されてたかのように変数を作れる訳ですよね。

MyDate today; // 本日の日付
MyDate birth; // 誕生日

todayやbirthといった変数は、それぞれがyy,mm,ddの3つのint値を持っています。これらを構造体の「メンバ」と呼びます。
今日は何日かと聞かれたら today.dd
あなたは何月生まれと聞かれたら birth.mm
のように、「変数名.メンバ名」という書き方で、どの変数のどのメンバを使うのかを指定します。

2.構造体を使う

このような変数を用意したら次にそれを使った処理を考えます。本日の日付と誕生日の差を求めて、この人が何歳なのか計算するという処理を書いてみます。
まずは準備として変数に値を代入します。

// 本日は2008年10月20日とする
today.yy = 2008;
today.mm = 10;
today.dd = 20;

// 生年月日は1970年8月5日
birth.yy = 1970;
birth.mm = 8;
birth.dd = 5;

続いて2つの日付の差を求めます。ここでは年齢を計算したいだけなので、月日はどうでもいいと考えて、
int age = today.yy - birth.yy;

これで「今年で何歳になる(なった)か」は簡単に求まりますが、今日現在で満何歳なのかを知りたいならこれでは駄目ですよね。
両者の月(mm)を比較して誕生日の方が大きかったら1歳減らす、月(mm)が等しかったら次に日(dd)を比較して・・・といった考慮が必要です。2つの日付の差分を求めるという単純な例ではありますが、それでも多少は気を付けなきゃいけない点があって、数行のコードになります。

いったんMyDateを定義してそれを使うようになると、日付の差分を求めるというのはいかにもシステムのあちこちで行うであろう処理だと容易に想像できます。その全ての箇所でいちいちこのような数行のコードを書くのは面倒くさいしバグの元になります。例えばプロジェクトに配属されたばかりの新人君にこの処理を書かせたら、うるう年の2月29日生まれの場合の考慮を、彼はちゃんと実装してくれるでしょうか。

そこでこのようなどこでも行うであろう処理は関数として切り出して、誰かが一度だけ書いてあげれば、他のプログラマはそれを呼ぶだけで済むから生産性が上がるよね、という考えで以下のような関数を用意してあげます。
int dateDiff(MyDate birth, MyDate today);

こういうのを共通関数などと呼びます。いったんこのような関数を作ると、差を求める以外にも日付を処理するケースは多々あって『ある日付の○日後の日付を求めたい』『当月の月末の日付を知りたい』『年を和暦に変換したい』などいろんな場所でいろんな要求が出てきます。それらを1つひとつ共通関数として用意してあげると、だんだんと関数の数が増えて充実していって『日付共通関数ライブラリ』なるものが出来上がります。
このようなライブラリはプロジェクトの資産となり、日付以外にも様々な『○○ライブラリ』が作られて、システムの生産性向上に寄与する訳ですね。

この考え方はオブジェクト指向でも何でもなく「構造化プログラミング」などと呼ばれ、オブジェクト指向よりもずっと前から提唱されているスタイルです。オブジェクト指向をわかってない人の大半は、実はこの段階で止まっちゃってるんですよ。この旧来のスタイルが染み付いててそこから脱皮できないからオブジェクト指向の本質が理解できないんですね。
繰り返しますが、「関数ライブラリ」の考え方はオブジェクト指向ではありません。←ここ何気にチョ〜大事!

構造体の問題点

オブジェクト指向超入門〜第3回〜

前回説明したように、構造化プログラミングのスタイルを取り入れてシステム開発を行えば生産性が上がってみんなハッピー!と考えてしまいたくなりますが、実は問題点があります。

問題点その1〜日付は常に正しいか

こんなコードを書いたとしましょう。
today.mm = 13;

この後 dateDiff(birth, today); という関数呼び出しを行うと何が起きるでしょう。このように予期しない値が変数に格納されている場合、何が起きるかは一般的には予見できません。最悪の場合システムがクラッシュします。
悪いのは誰か。月に13を代入する奴が明らかに悪いと片付けるのは簡単です。さすがにこんな明からさまな例を出されても納得できない人もいるでしょうが、代入する値がどこか遠くからやって来た変数である場合など、予期しない値が代入されるケースは十分有り得る、というのはプログラマなら理解してもらえると思います。

問題点その2〜日付は年月日の3つの値を持つのか

例えば、2つの日付の差分日数を簡単に求めるために、MyDateの定義を変更したくなったとします。

struct MyDate {
  int yy; // 年
  int ddd; // 1月1日からの通算日数
}

こんな感じ。そして dateDiff() 関数の中身を今よりもシンプルに書き変えます。
このような変更はプロジェクト全体にとって大騒ぎですよね。today.mm = 12; などとやってる箇所を全て探して修正して回らなければいけません。
これもいかにもな例ではありますが、要はいったん構造体を定義してみんながそれを使い始めてしまったら「ここはもっとうまくやれたのに」と後から後悔しても、もう変更は効かないという事です。

問題点その3〜共通関数は使われているか

ライブラリ内の共通関数を呼べば余計な処理を書かずに済むと言われても、システムの全ての箇所でそのようなコードになっているでしょうか。共通関数を呼ばずに自前で処理を書いている箇所が無いという保証はありません。「そんな揚げ足取らなくたって みんな分別ある大人なんだし」と果たして言えるでしょうか。プロジェクト内には恐ろしいコーディングをしてくれるプログラマが実在します。配属されたばかりの新人君もいれば、破壊的なコードを書く天下無敵のヘボプログラマも本当にいるんです。
オブジェクト指向には個々のプログラマの分別なんてものはハナから信用しないという考えが根底にあります。

オブジェクト指向での解決策

オブジェクト指向超入門〜第4回〜

オブジェクト指向では構造体の問題点を解決するための仕組みが提供されています。前回の記事で構造体での問題点として挙げた事は全て、構造体のメンバを直接いじれるのが原因で起こります。どういう事でしょう。

問題点1では構造体を使っている箇所で mm に 13 という値を直接代入しています。問題点2では mm や dd に値を代入したり、あるいはその値を参照している全ての箇所に変更の影響が及びます。だったら mm や dd などの構造体のメンバを使えないようにしてやれば良いのです。その為にアクセス制御という考え方が導入されました。
変数宣言の前に private と付けるとメンバを直接参照したり値を代入する事は出来なくなります。そのかわりにメンバを使うための関数を提供してあげます。

class MyDate {
  // この変数を直接使うのは許さん
  private int mm;

  // 変数を使いたかったら関数を呼びなさい
  public int getMm() {
    return mm;
  }
  public void setMm(int mm) {
    this.mm = mm;
  }
}

これまで変数の集合でしかなかった構造体に新たに関数がメンバとして加わりました。today.mm = 12; とやっていた箇所は、オブジェクト指向では today.setMm(12); と書くようになります。
問題点1のように today.setMm(13); とやられてしまった場合にどうするかというと、setMm という関数の中でエラーを返すなり、翌年の1月になるようにyyとmmの値をうまいこと調整するなりという処理をやってあげればいい訳です。とにかく13月という無効な日付を保持する状態になってしまう可能性を排除してやります。

これまで、月に13を代入するようなバカな事をするのは、そんな事をする方が悪いに決まってるという考え方でした。オブジェクト指向では、クラスを使う側はどんな事をやらかすかわかったもんじゃない、と考えます。日付を保持するというのがクラスの役目であるなら、変な日付が格納された状態になってしまう可能性を排除し、どこぞのバカがトンデモない使い方をしても変な日付が格納される事がないようにしておきます。それは使う側ではなくクラス自身の責任だと考えるのです。

問題点2では日付が年月日の3つの値を持つ事を前提に today.setMm(12); のようなコードを書いていたのに、クラスのメンバに mm が無くなってしまいました。この場合 setMm を呼んでいる全ての箇所を変更して回る必要はなく、setMm という関数自体はそのまま残して、その中身を(ddd という変数に適切な値を格納するように)書き変えてあげるだけで済みます。
こうなるともはや、日付を表現するために年月日の3つの値を持つという事実は、クラスを使う側にはどうでもよくなります。実際この例では、3つの値を持つ前提でクラスのメンバ関数を使っていましたが、実は変数は2つしかありませんでした、というオチですよね。
あるデータを表現する為にどんな変数をいくつ使って実現するかはクラス内部の都合であって、使用者はそんな事は知ったこっちゃないし知る必要もありません。使用者にとって大事なのは setMm という関数の呼び方だけです。
こういうのを実装の隠蔽とかクラスのカプセル化といって、オブジェクト指向のチョ〜重要な考え方です。

実装詳細を隠蔽すれば問題点3も解決します。提供されている関数を使わずに自前で処理を書く手段が無くなってしまう訳ですから、関数が使われている事は完全に保証されます。

オブジェクト指向のポリシー

オブジェクト指向超入門〜第5回〜

これまでの構造体に対して関数をメンバとする事ができるとなると、日付共通ライブラリなるものに用意されていた関数は全てクラスのメンバになります。ある日付の10日後の日付を求める為の共通関数の呼び出し addDay(today, 10); は today.addDay(10); となります。
これは見た目にはコードの表記法がちょっと変わった程度の違いでしかありませんが、実は非常に大きな考え方の変化があります。これまでの発想では日付に日数を足すという処理がまずあって、そいつに足される日付と足す日数の2つのデータを渡してやり、処理結果として10日後の日付というデータを受け取る、というように処理に従属する形でデータが現れます。

オブジェクト指向ではこの処理とデータの関係が180度逆転します。まず日付というデータ(=オブジェクト)がありきで、処理はそれを行うべきデータ自身にやらせるという発想になります。today.addDay(10); という記述は「todayさん、あなたに10日足してください。」と日付オブジェクトに対してお願いしているという意味になります。日付に10日足すという仕事は誰がやるべきかというとそれは日付自身であって、自分のことは自分で責任を持ってやる(=他人が面倒を見る筋合いは無い)という考え方です。

『データ自身に仕事をさせる』あるいは『自分のことは自分で責任を持つ』という考え方はオブジェクト指向への大きなパラダイムシフトです。処理とデータの主従関係が逆転するというのは、天と地がひっくり返るほどの非常に大きなポリシーの変化であり、この点を理解しないとオブジェクト指向の本質は理解できません。オブジェクト指向をわかっていない人はこの部分がわかっていないのです。

わかっていない人がクラス設計をやると「日付ユーティリティクラス」なるものがあって、そのメンバに addDay(MyDate date, int days) という関数を作っている例を実際の現場でよく目にします。構造体と共通ライブラリの発想そのまんまでオブジェクト指向はまるで無視です。こういう共通関数を作る為には MyDate のメンバ変数は全て丸見えになっていないと実現不可能な訳ですが、全ての変数に対して単純なgetterとsetterが用意されているだけだったりします。

class MyDate {
  private int dd;
  public getDd() {
    return dd;
  }
  public setDd(int dd) {
    this.dd = dd;
  }
}

わかってない人がやらかす典型的な例で『変数はprivateで定義しましょう』『変数をアクセスする為のgetterとsetterを変数とペアで用意しましょう』と何も考えずに条件反射的にコーディングしてしまっています。変数をprivateにする理由を今一度思い出してみましょう。getterとsetterを常にコーディングするのと変数をpublicで宣言するのとで何か違いがあるのか考えてみましょう。
このようなコードでは構造体での問題点はひとつも解決しません。