プログラマというお仕事

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

オブジェクト指向

オブジェクトは常に正しい

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

オブジェクト指向のポリシーでもうひとつ重要な点は、保持しているデータは常に矛盾や破綻がないという事です。どっかのバカがムチャクチャな使い方をしても、決して13月42日のような日付になってしまってはいけません。それは使う側ではなくクラス自身に、そのような事態になってしまう可能性を排除する責任があるのでした。
その為に変数をprivateにし、アクセスメソッドでは渡された引数の値を適切にチェックして、変数同士が矛盾しないように処理する必要があります。そのようにして考えられる穴を出来る限り塞いでおくほど「堅牢な」クラスになります。クラスが堅牢になるほどシステムのバグは少なくなり、またバグが起こってもその原因が特定の箇所に限定され修正しやすくなります。

ではクラスのメソッドをこのようにしっかり作っておいて破綻の可能性を排除すれば、オブジェクトが予期しない状態になるのを100%防げるでしょうか。どんなにメソッドを頑丈に作っても、オブジェクトの状態が不定になる可能性がひとつだけ残っています。それはオブジェクトをnewした直後です。

MyDate today = new MyDate();
today.addDay(10); // どうなる?

オブジェクトを生成した直後には正しい日付は保持されていません。このクラスではnewした後に setYy(2008); setMm(12); などを呼んでもらう事を想定しています。これは言ってみれば使用者に使い方を強制している事になります。しかしオブジェクト指向では「どこでどんな無茶な使い方をされるかわかったもんじゃない」「使用者は分別を持ってくれるはず、なんてのはハナから信用しない」という思想が根底にあります。だからオブジェクトが矛盾した状態にならないようにするのはクラス自身の責任なのでした。であればnewした直後でさえも破綻の可能性を排除するのはやはりクラスの責任です。コンストラクタはその為に導入された文法です。

pulic MyDate() {
  // 2000年1月1日を初期値とする
  yy = 2000;
  mm = 1;
  dd = 1;
}

普通は本日の日付を取得してきて初期値とする、などが実用的な実装なのでしょうが、とにかく「変な」日付が保持される可能性はこれで完全に排除できる訳です。コンストラクタはこのようなオブジェクト指向のポリシーを貫くための文法です。太古の昔から言われている「変数は宣言したらまず初期化しよう」という初心者用の教科書に書いてあるようなくだらない仕事をするために導入されたのではありません。
わかってない人はコンストラクタで単純な初期化しかしない場合が見受けられます。この場合、型がintだからという単純な理由で変数にゼロを代入するといった具合です。0年0月0日という日付オブジェクトが出来上がるようではコンストラクタの役割を果たしていません。

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

オブジェクト指向超入門〜第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で宣言するのとで何か違いがあるのか考えてみましょう。
このようなコードでは構造体での問題点はひとつも解決しません。

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

オブジェクト指向超入門〜第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も解決します。提供されている関数を使わずに自前で処理を書く手段が無くなってしまう訳ですから、関数が使われている事は完全に保証されます。

構造体の問題点

オブジェクト指向超入門〜第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〜共通関数は使われているか

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

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

オブジェクト指向超入門〜第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つひとつ共通関数として用意してあげると、だんだんと関数の数が増えて充実していって『日付共通関数ライブラリ』なるものが出来上がります。
このようなライブラリはプロジェクトの資産となり、日付以外にも様々な『○○ライブラリ』が作られて、システムの生産性向上に寄与する訳ですね。

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