なんの資格も持っていない野良Java使いで、
数多いるプログラマの平均以下のスキルなのだけれども、教科書ではわかりにくかったオブジェクト指向について少しだけれどもわかってきた。
もちろん野良なので、正しい設計や考えとは違うかも知れない。
オブジェクトってなんだ?インスタンスってなんだ?クラスってなんだ?
教科書でいえば、哺乳類はクラスで犬はインスタンスとかそんな漠然とした『イデア論』のような話をされて
実際の設計にそれの何が役に立つのかってのは全くわからない。
そもそもクラスやオブジェクトの役割ってなんなんだ?
僕は数年間下手なりに触ってきた中でわかったクラスの役割は3つある。
- 型
- 機能
- 状態
この3つだ。
型
型とは文字列ならString、整数ならIntegerなどデータを扱う上での変数の種類だ。
クラスは型を定義する。
『型』というのは変数を宣言するときに定義する。
int num = 0;
のようにね。
クラスは他のクラス(やインターフェース)を継承して、他の型との関係を表現できる。
例えばRPGやネットゲームなどでミュータントという種族のキャラクターが出てくる場合、Humanという抽象クラスを継承する形で
Mutantというクラスを定義した。
abstract class Human {}
class Mutant extends Human {}
すると、『ミュータントは人間(という種族)だ。』という関係が作られる。
人間という広い範囲の中にミュータントという種族がいるということだ。(ジャニーズの中にSMAPがいるのと同じだ)
さあ、次にミュータント以外にもノーマルの人間を作りたい。
class NormalHuman extends Human {}
とすると、このように継承して新たなクラスを作る。
『ノーマルな人間は人間(という種族)だ』という関係が作られる。
この関係が生きてくるのが、人間全体に関係するような操作などだ。
void atack(Human human)
例えば上のメソッドは『人間を攻撃する』という意味になる。(メソッドは動詞で引数は目的語だ)
この人間というのはミュータントもノーマル人間も含まれる。
このようにクラスは、データはどのような種類なのかということと
他の種類とどういう関係なのかということを表現することができる。
つまり意味の集合というか、意味を定義した物だ。
だから、クラスは意味的に関連のある一つの概念を表すべきである。
機能
クラスにはメソッドを持たせることができる。
機能とは端的にいえばメソッドのことだ。
クラスはある一つの概念を表した物だが、その概念が持っている機能をメソッドにする。
さっきのゲームの例えだと、すべての人間は射撃いう技を使えるとする。
この場合さっきの抽象クラスHumanに shotというメソッドを作る。
でも、ミュータントにはshotの他に魔法が使えるとすると
Mutantクラスにmagicというメソッドを作る。
そのうえミュータントは射撃が通常とは特別な効果があったとすると、
Mutantクラスでshotメソッドをオーバーライドする。
メソッドとはそのクラスという概念が持っている機能を表す。
だから意味が通じない機能をクラスにつけちゃだめ。
状態
状態の前にインスタンスについて少し整理をする。
今まで型や機能では、クラスという抽象的な枠組みの中でしか話をしていなかったが、
今回はインスタンスという具体的な話になってきた。
Human player1 = new Mutant();
Human player2 = new Mutant();
このようにHuman型をnewすると、Humanという型の実体として
player1とplayer2が生を受けることになった。
newの左側がMutantになっているのは、Human型だけれども中身はMutant型という意味。
日本語にすると、『プレイヤー1は人間(ミュータント)だ。』という意味だ。
人間という広い概念の範囲でplayer1を定義しているが、実際はMutantに準拠するということだ。
こうやって、クラスという概念から実際の個体としてのインスタンスが表現できた。
男の子というクラスには【タクヤ、マサヒロ、シンゴ、ゴロウ、ツヨシ】というそれぞれ別の個体が存在するだろう。
そういう個体を表現するのがインスタンスであって、
その個体はなんの種なのかを表現しているのがクラス。
では、状態の話にうつる。
状態とはそれぞれの個体が持っているパーソナルデータのようなもの。
例えば、Humanにはヒットポイントという数値があったとする。
そしてHPをセットするメソッドがあったとする。
Human player1 = new Mutant();
player1.setHP(999);
この場合、Humanというクラスにはヒットポイントを保持するインスタンス変数があって、
セットすることによってHuman型の一つの実体であるplayer1というインスタンスはHP:999という状態を持つことになった。
同じくHuman型のPlayer2のヒットポイントは別の数値になる。
クラスは概念だけれども、インスタンスは個々の実体。
String str1 = “みんなおはよう”;
と変数を宣言すると、
JavaのString型はStringという概念としてのクラスとは別に、str1というインスタンスに『みんなおはよう』という状態をもつことになる。
str1の状態はstr1のものだし、ほかのString型には関係のないものだ。
クラスはこの状態を保持する受け皿を作ることができる。
そしてこの受け皿はインスタンス変数が持つことになる。
『状態』は使うのが難しい
Javaだとこの3つのものがおなじクラスで定義されるものなので、ごっちゃになりがち。
ファイルをZIP圧縮するクラスを誰かが作ったとしよう。
このZIP圧縮クラスは ZipArchiverとかいう名前にしておく。
で、たとえばこんなふうに使うクラスだったとする。
ZipArchiver zp = new ZipArchiver();
zp.setFile(file)
zp.execute();
File file = zp.getFile();
このクラスはファイルを『状態』として持たせて、
executeで実行するとその内部のファイルが圧縮されたファイルに置き換わって、
getFileメソッドでファイルを取得できる。
もし圧縮しないままgetFileを実行するとsetFileで渡したFileが戻ってくる。
この例だと、ファイルを『状態』で持っていて、
圧縮する『機能』も持っている。
こういう場合の『状態』の厄介なことは、インスタンスの『機能』によって『状態』が書き換わる可能性があるということだ。
今どのような状態になっているかの見通しが悪くなる。
実際にこのように設計されている物がおおいが、僕は『機能』と『状態』は同居させなくていいのではないかと思う。
もし『機能』と『状態』が同居するクラスがあったとしても『機能』は『状態』に対して不可侵でいるべきだと思う。
そうしないと、同じインスタンスなのに処理どこかで内部が変わっていることが分かりにくくなる。
JavaのStringクラスは内部の文字列をStringクラスが持っているメソッドでは書き換えることをしない。
substringでもconcatでも、戻り値として別のStringのインスタンスを返している。
Javaでは、
String message = “ちっす”;
と宣言されたmessage変数が処理の途中で書き換わって”ちっ”と壊れることはない。
さっきHumanとかMutantなどの例でヒットポイントの話があったが、ヒットポイントを減らす処理をするのはHumanクラス(やそのサブクラス)の内部にあるメソッドではなく、外部の別のメソッドが操作をするか、別のHumanインスタンスを戻り値として戻すか…少なくとも同じクラス内で状態を変化させるべきではないとおもう。
『機能』と『型』は見通しが良い
『型』はJavaではインターフェースとクラスが表現をする。
これは多重継承が可能だ。
『機能』もJava8からデフォルトメソッドという仕組みが追加され、インターフェースもメソッドを持てるようになった。
つまり『機能』も多重継承可能になった。
だが、『状態』は多重継承をすることができない。
これは2つのクラスを継承した場合、どっちのクラス由来の『状態』かを判別することが困難になるからだろう。
インターフェースにはインスタンス変数を持つことはできないので、たとえ『型』や『機能』を多重継承しても
『状態』を多重継承することはJavaではできない。
内部状態を操作しない限り『機能』は見通しが良いもので、インスタンスとして表現されなくてもいいと思う。
Javaでは何でもかんでもクラスの中に記述しなければならないが、Pythonを使っているとその必要もない。
モジュールというもので関数を定義して、必要に応じてクラスをつくったりする。
Pythonにおけるモジュールはインスタンスのように同じ型で複数の個体が作られるというものではなく、一つしか存在しないものになる。
でも関数においてはそのクラス内で定義してインスタンス化するということをしないでも全く問題はないどころか、実体が一つしか作られないので見通しが大変良い。
状態をもたせる場合にはクラスを定義するが、その必要はないものはモジュールにする。
だからPythonにおけるクラスは、僕はC言語の構造体のように『状態』を保持しておくことに主体を持っているように感じる。
『機能』と『状態』の分離
というわけで、今回の記事で言いたいことは
『機能』と『状態』は分離して扱うべきであるということ。
メソッドは自分が属しているインスタンスの内部状態を書き換えない。
インスタンス変数は自分で書き換わらないで、外部からの操作で書き換わるようにする。
処理に必要なデータは、引数で渡すようにする。
メソッドを実行する前段階でセッターメソッドで状態を保持させて実行するようにすると途端わかりづらくなる。
こうやって設計すると、『状態』だけを持つクラスと
『機能』だけを持つクラスに分かれてくると思う。
両方もっているクラスというのは実際あまりないのかも知れない。
こういう考えの究極が関数型言語なんだと思う。
一度イミュータブルの効能に触れると、『状態を持ち変化させる』という書き方がいかに扱いづらいかわかってくると思う。