前回の続きです。前の回ではオブジェクト指向として一番重要なのが「メッセージング」という概念だと記述しました。そして、作るべきオブジェクトの単位として
「現実をモデルとし、メッセージングを行える単位にまで整理した構造体」
だと(勝手に)定義しました。
現実をモデルとするのは、それが直感的にわかりやすいからですね。
そこで今回と次回を通じて、上記を想定にしてどのようなクラスを作るべきかを最初は簡単な例から考え、次にもう少し複雑なものを想定して考えていきます。
ちなみに、ソースコードはほとんど記述しません。ロジック部分は、オブジェクト指向とはほとんど関係がないからです。なので主にクラス図を使って説明していきます。
その前に、もっと簡単な例でクラスの役割を考える
「バスの時刻表表示システム」を考える前に、もっと簡単なケースでオブジェクトについて考えてみます。簡単なケースの方が、「何の処理を」「どこに」「どのように」配置するかについて理解がしやすいと思うからです。
というわけで、こんなケースを考えてみましょう。
「紙に、文字を書く」
これをクラスに落とし込んでみましょう。
まず、紙に文字を書くわけですから当然「紙クラス」を作りますよね。
あとはそこに文字を書くんだから、「書く()」メソッドがあればいいんじゃないでしょうか。
ちょっと怖い
いや、紙からいきなり文字が浮かんだりなんてしたら怖いですね。
でも上のクラス図だとそういうことになります。他に誰もいないんだから、紙が文字を書いてます。なにそれ怖い。
ここで、クラスを設計する際には以下のことを考えた方がいいことがわかります。
「クラスには、そのクラスが能動的に処理を行うメソッドを持たせる」
能動的というのがポイントです。
では、誰が「書く」という能動的な動作を行えるでしょうか。ペンでしょうか。
ペンは、書くということはできないですね。ペンが自動的に動いて意思を持ちながら何かを書いてくると「なにそれ怖い」ってなりますね。
そうなると、ぱっと思い浮かぶのは「人」じゃないでしょうか。ということでとりあえず人も登場させてみましょう。
でも、「人」と「紙」があれば文字をかけるとも言えません。書くためにはやっぱり「ペン」のようなものも必要です。もちろん、指を切って血で紙に書くことはできなくもないですが。なにそれ怖い。
改良する
ということで、出来上がるクラス図は以下のようになります。
ついでなので、読むというのも付け足してみました。
ペンは、書くことはできませんが「出力する(または印字する)」ことは能動的にできそうです。
紙の場合、それは出力された文字を「保存する」または「取り出す」ということを能動的に行えるのではないかと思います。
ちょっと冗長的でしょうか。
たしかに、そうとも思えますね。それに引数が3つもあったりすると、なんだか使いづらそうにも思えます。あんまり引数が多いのって好きになれません。
しかし人が「紙」と「ペン」を使って文字を書く場合はこうなるのは仕方ないんじゃないかなとぼくは思います。これがPCであれば、PCの中に「ペン」に相当する機能と「紙」に相当する機能の両方がありますから、人クラスにある「書く」という引数には「PC」と「文字」だけでよくなるのですが。
ただ、間違えても「人」の中に「ペン」と「紙」のメンバ変数を持たせるべきではありません。
なぜなら、「人」は四六時中ずっと「ペン」と「紙」を持ち続けてるわけじゃないからです。「ペン」に「紙」を持たせるのも同様です。
それは既にフィールド(つまりmain関数など)でインスタンス化されているか外から与えられることが前提となります。人は手に取れる位置に「ペン」と「紙」があるか誰かから渡されなければ字を書くことなんてできないんです。
もっと改良する
ところで、人はただ「ペン」だけで書くわけでもないし、「紙」だけに書くわけでもないですね。えんぴつも使えば、上にも書いたようにPCも使います。また壁に書くこともあるし黒板に書くこともあります。(さらに言うと「人」だけが読み書きできるわけじゃなく、ロボットだってできるかもしれませんが、ここでは割愛します)
オブジェクト指向の場合、能力は外出し可能です。
そこで、それぞれが持つ「能力」を考えてみます。すると以下のことがわかります。
- ペン:出力する能力を持つ
- 紙:保存と取り出しをする能力を持つ
それらを外出ししましょう。すると最終的にはこんなクラス図になります。
(面倒なので途中のメソッド名などは省略してます)
ここで、PCのように「出力」と「リソース」両方の能力を持つ用のインタフェースを用意して、それ用の「書く()」をオーバーロードするとより便利で自然になるかもしれませんね。そういうのは他にも「スマホ」だったり「ペン付メモ」だったりあると思うので(最後のは強引だけど)。
これで、人が持っている「書く」と「読む」という処理は、様々なクラスによって再利用することができるようになります。ポリモーフィズムとは、難しい言い方ですけど要は「メソッドの再利用」にほかなりません。同じ振る舞いをするインタフェースであれば、これが可能になります。
なぜか。
それは、メッセージングを用いたオブジェクト指向プログラミングでは「呼び出し元は相手側の処理については一切関与しない」からです。その後の処理についてはまるごと一任します。
インタフェースは、「メッセージング」でのオブジェクト間同士のやりとりを行わせるのに向いています。ダウンキャストでもしない限り、他のものは一切呼べないですから。強制的にメッセージングでのやりとりしかできません(そして、ダウンキャストは推奨しません)。
補足:SOLID原則について
オブジェクト指向設計を行う上で、常に頭にいれておいた方がいい原則として「SOLID原則」があります。それは、5つの原則の頭文字をとったもので、メッセージングでのオブジェクト指向プログラミングを行う上では重要になります。
以下、名前だけ紹介しておきます。
- 単一責任の原則
- 閉鎖開放の原則
- リスコフの置換原則
- インタフェース分離原則
- 依存性逆転の原則
それらについて、こちらの記事がとてもわかりやすいです。
補足:メンバ変数について
いろんなプログラムに接してると、ひとつのクラスに膨大な量のメンバ変数を持つようなケースをよく見かけます。
たくさんのメンバ変数を持たせるのは「凝集度」などの点からあまり好ましいものではありませんが、業務システムなどを作る場合はどうしても少なくするのに無理が生じたりもするのである程度は仕方ないのかもしれません。ただ、メンバ変数を持たせる際には以下の点は常に意識する必要があると思っています。
- それは、本当にそのクラスに責任を持たせるべきか
※そのクラスは責任を持ちすぎていないか(責任を分割できないか) - 不自然ではないか
特に「不自然ではないか」は重要です。なぜなら、しばらくたって再びソースコードを見た時に、自然な場合はすんなりと理解できますが不自然な場合は「あれ、なんでこれここにあるの?」ってなるからです。
オブジェクト指向とは結局のところ整理術だとぼくは思っています。そしてクラスの主人公は「メンバ変数(インスタンス変数)」です。クラスの動作とは結局のところ、そのメンバ変数にどのような動作をさせるかの定義に他ならないからです。なので、不自然な箇所に不自然なメンバ変数があると、それに関連する全体が不自然な動きになります。
上で「間違えても人に『ペン』と『紙』のメンバ変数を持たせるべきではない」と書いたのは、そういう不自然さを持ってしまうからです。メンバ変数を持たせる場合はカプセル化することを前提にすべきですが、「人」にペンと紙のメンバ変数をもたせた時点でどうしても無理が生じて最終的にはsetter/getterに頼るしかなくなるでしょう。もしどうしても持たせたい場合、それは「人」以外のクラスにすべきです。
何がいいでしょうね。日本語ではうまく頭に浮かびませんがしいて言えばTypeWriterクラスですかね。実際にペンを持って出力するタイプライターなんか、古今東西で探せばありそうですし。ただそれはそれで何か不自然な気もしますが。