クラスを使ったコードを書いてはいるものの、どうにもオブジェク指向プログラミング(OOP)というものがわからない。クラスやオブジェクトはわかる。ではオブジェクト指向プログラミングとは何だ?と聞かれると言葉に詰まってしまう。
ちょっと前にバズった「オブジェクト指向と10年戦ってわかったこと」と、それに対するツッコミなどを読んでいるうちに、自分なりに納得できる説明を思いつたので書き残すことにしてみました。
慣れ親しんだPHPを使って説明してみます。
オブジェクトとは何か?
むかーし、昔。OOPとは何か?みたいな話を同僚のエンジニアと話をしたことがあります。彼が言うには、「オブジェクトってのは、要はメモリ空間のことだよ」と。(※コメント参照)
オブジェクトとはメモリ空間のこと
クラスは設計図
一方、クラスというのは、オブジェクトの元になる設計図のようなもの。
まずはサンプルとしてクラスを考えます。すごく簡単なクラス。
class MyFile {
public $name;
public $fp;
function __construct($name) {
$this->name = $name;
$this->fp = fopen($name, 'rb');
}
}
これで設計図が出来ました。
この設計図を元にして、何個でもオブジェクトを作ることができます。
オブジェクトをメモリに展開する
設計図、つまりクラスからオブジェクトを作成するにはnew
を使います。これは「インスタンス化する」とも言われます。
$object = new MyFile('test1.txt');
echo $object->name; // test1.txt
これでMyFileというクラスを元にしたオブジェクトが、メモリ上のどこかに作られました。
メモリ上のどこか、では使えないですね。そこで、$object
にはオブジェクトを指し示す情報が入ってます。PHPの場合はオブジェクトのIDが変数の中に入っています。
微妙な違いですが、$object
自体はオブジェクトではなく、単に__オブジェクトを指し示す普通の変数__である、ということです。
オブジェクトの変数と値渡し
PHPは関数に変数を受け渡すとき、普通の変数は値渡し、オブジェクトの場合は参照渡し、と言われますが、少し違います。
次のサンプルを見ると、無名関数に$object
が受け渡されて$object->name
が書き換えられています。確かに参照渡しのようにみえます。
function resetName($object2) {
$object2->name = 'tested';
};
$object = new MyFile('test1.txt');
resetName($object);
echo $object->name; // tested
ただし、受渡しているのはオブジェクトではなくて、オブジェクトを表す$object
という普通の変数に過ぎません。次のサンプルだとオブジェクトの参照では説明がつかなくなります。
function resetMyFile($object2) {
$object2 = new MyFile('tested.txt');
};
$object = new MyFile('test1.txt');
resetMyFile($object);
echo $object->name; // test1.txt
関数内で$object
は上書きされたはずなのに…
これは$object
も$object2
は違う変数で、たまたま同じオブジェクトを指し示しているからおこります。関数内で$object2
の指し示しているオブジェクトを変えても、元の$object
は変更されずに、元のオブジェクトを指し示したままになります。
では、最後に「オブジェクトの参照渡し」をしてみます。これで、$object2
と$object
が同じ変数になったので$object
を上書きできました。
function replaceMyFile(&$object2) {
$object2 = new MyFile('tested.txt');
};
$object = new MyFile('test1.txt');
replaceMyFile($object);
echo $object->name; // tested.txt
staticについて
今回は、省略!
カプセル化
そもそも何故オブジェクトを使うのでしょう?
オブジェクトを利用する大きなメリットとしては、
- 内部構造や処理を知らなくても__簡単に利用できる__。
- 外部と情報が隔離されていて__安全に使える__。
これを 「カプセル化」と言ます。こちらの「カプセル化と隠蔽」もよくまとまっています。
簡単に使う(≒モジュール化)
内部の処理を隠すことで、簡単に使えるようになります。
処理に良い名前をつけると、さらに使い勝手が向上します。
こんな例を考えてみました。先のMyFile
というクラスにread
というメソッドを追加してみました。
class MyFile {
public $name;
public $fp;
function __construct($name) {
$this->name = $name;
$this->fp = fopen($name, 'rb');
}
function read() {
return fread($this->fp, 1024);
}
}
使うときは、こう。
$file = new MyFile('test.txt');
echo $file->read();
う〜ん、何が便利になったのか分からない…
他に良い例がないか考えてみたところ、fopen
こそ、内部のロジックを隠したケースだと思いつきました。ファイルを開くには、OSの関数をコールして、OS内部のファイルシステムはHDDかSSDのドライバを使って… 実際、自分はfopen内部で起きていることをよく知りませんが、それでも使えるのは内部動作を知らなくても使えるようになっているからです。
安全に使う(≒情報の隠蔽)
カプセル化は、別名「情報の隠蔽」とも言われます。つまり、内部の構造にアクセス出来なくすることです。MyFileクラスでは、メンバー変数がpublicになっていて、いつでも誰でも変更可能な状態で危険なことが分かります。
$open = new MyFile('test1.txt');
$open->name = 'test2.txt'; // 出来る。
こうなると、$open->fp
が指しているファイル名と$open->name
が異なるという状態になってしまいます。一体、どちらの情報が正しいのかわからない、不整合な状態です。
そこで、先のクラスを修正して、情報を隠蔽してみましょう。
class CapsuleFile {
private $name;
private $fp;
public function __construct($name) {
$this->name = $name;
$this->fp = fopen($name, 'rb');
}
}
メンバー変数の$name
と$fp
のスコープをpublicからprivateに変更しただけですが、もう内部の変数は隠蔽されています。
$closed = new CapsuleFile('test2.txt');
$closed->name = 'test3.txt'; // エラーで出来ないよ!!!
となります。
このように情報を隠蔽することで、内部の情報に一貫性を持ったオブジェクトを作ることが可能になります。
大事なことは、privateを使うとか、getterやはsetterでアクセスすればOK、という話ではないことです。オブジェクトが安全に使えるよう、この例では整合性を保てるように、設計するということです。
カプセル化は良いプログラミングの基本
fopen
の例のように、OOPが登場する以前からサブルーチン(いわゆる関数)というのが存在していて、内部の実装をよく知らなくても利用でき、外部から隔離されていて安全に使うことが可能でした。
関数だと変数を内部に持てない(もたせにくい)ため、状態を管理したり、ちょっと違う値を保持しつつ利用するのが難しいという問題がありました。そこでオブジェクトを導入することで、状態をもたせつつ、簡単に利用できるようになった、と考えることが出来ます。
こう考えると、オブジェクトにおけるカプセル化といういうのは、プログラミングの正常な進化に思えます。
一方で、カプセル化することがオブジェクト指向プログラミングなのか?と言われると違うのではないかと感じてしまうのです。
ポリモーフィズム
オブジェクトとクラス、そしてカプセル化については理解したと思いつつ、ではオブジェクト指向プログラミングとは何でしょう?
そんな時に読んだのが、このツイート。
言いたいことはわかるしカプセル化は重要だが、あくまでもモジュール化。オブジェクト指向の本質ではない。3原則からひとつだけ選ぶなら「ポリモーフィズム」を選ぶ。link: オブジェクト指向と10年戦ってわかったこと - Qiita: https://t.co/XFwnBEZ2pi
— Yukihiro Matsumoto (@yukihiro_matz) 2016年5月2日
今までバラバラだった理解が、いきなり一つにまとまった気がしたのでした。
上記のツイートをもとに、自分なりの理解を長々と書いてみたのが、この内容です。
ポリモーフィズムってなんだ?
たくさんあるプログラミング用語の中でも、名前を見ただけでは意味がわからない言葉がポリモーフィズムではないでしょうか。
英語だと「polymorphism」。分解すると「poly」と「morph」から構成されています。
poly
はたくさんという意味(ポリネシアとかポリエチレンとかのポリ)、そしてmorph
が形とか変形という意味になります。だから一緒にすると「たくさんの形」で、__多態性__とか__多相性__と訳されるわけですが…
「プログラミング言語の各要素(定数、変数、式、オブジェクト、関数、メソッドなど)についてそれらが複数の型に属することを許すという性質を指す」
とあります。
確かに、関数などはグローバルに定義するので、同じ名前の関数を複数作れません。違う処理をしようと思うと、別の関数名を付けざるを得ません。
インターフェースを使ったポリモーフィズム
インターフェースを使ってポリモーフィズムを実現してみます。
今まで例に使っていたMyFile
クラス用にインターフェースMyFileInterface
を定義してみました。
interface MyFileInterface {
public function read();
}
まずは普通に実装してみます。
class Utf8File implements MyFileInterface {
private $name, $fp;
public function __construct($name) {
$this->name = $name;
$this->fp = fopen($name, 'rb');
}
public function read() {
return fread($this->resource, 1024);
}
}
次は、Shift-JIS用のクラスを作ってみます。
class SjisFile implements MyFileInterface {
private $file_name, $resource;
public function __construct($file_name) {
$this->file_name = $file_name;
$this->resource = fopen($name, 'rb');
}
public function read() {
return mb_convert_encoding(
fread($this->resource, 1024), 'UTF-8', 'SJIS');
}
}
SjisFile
はUtf8File
とコードは似ていて、read
内で読む度にSJISをUTF-8に変換しているだけです(筋の悪そうなコードですね)。
このインターフェースを使った関数を考えて見ます。例えば、次の関数はMyFileInterface::read
というメソッドを利用しています。
function echoFirstLine(MyFileInterface $file) {
echo $file->read();
}
先のポリモーフィズムの定義に戻って解釈すると、この場合は、Utf8File::read
もSjisFile::read
も、MyFileInterface::read
というメソッドでもあります。これが、どちらのオブジェクトを渡されても、使えるようになる理由です。
継承を使ったポリモーフィズム
次は、クラスの継承を使ったポリモーフィズムをみてみます。
class Utf8File {
/* コードは前のサンプルと同じなので省略 */
}
class SjisFile extends Utf8File {
public function read() {
return mb_convert_encoding(fread($fp, 1024), 'UTF-8', 'SJIS');
}
}
function echoFirstLine(Utf8File $file) {
echo $file->read();
}
SjisFile
はUtf8File
を継承したうえで、read
メソッドを上書き(オーバーライド)しています。ちょっとコードが減りました。
この場合、read
メソッドには
-
Utf8File::read
と -
SjisFile::read
の
の2つ実装が存在してます。これにより、同じ名前で別の処理を実行できるようになりました。
ちなみに、継承を使ったポリモーフィズムの場合は、
- 型の継承(
SjisFile
とUtf8File
はis-a
の関係にある)、 - コードの共有、
の両方を一度に行うことになります。二兎を追う者は一兎をも得ず、のごとく、継承は混乱の元になりやすいので、最近は継承を使わずにコードを書くよう言われています。
Duck Typing
今までさんざん使ってきたサンプルのクラスですが、インターフェースも継承も使わずに作ってみます。
まずは普通に…
class Utf8File {
/* コードは前のサンプルと同じなので省略 */
}
class SjisFile {
/* コードは前のサンプルと同じなので省略 */
}
今までとはimplements
もextends
も使わずにUtf8File
とSjisFile
の2つのクラスを作りました。。それぞれオブジェクトを作ったら、同じと判断されるのでしょうか?
でも、同じように使えるのだからいいじゃないか、と割り切ったのが「Duck Typing」と言う考え方。「クワッと鳴けばアヒル」から来た名前。Rubyで有名になりました。
function echoFirstLine($object) {
return $object->read();
}
$objects = [new Utf8File('test.txt'), new SjisFile('test.txt')];
foreach($objects as $obj) {
echo readFirstLine($obj);
}
あら、動く。
read
を利用する必要があるなら、read
が存在するオブジェクトだったら全部OKだよ、というわけですね。
先のポリモーフィズムの定義からみて、どういう意味なのでしょう?read
というメソッドに対して、様々なクラスにおける定義が存在している状態、と言えるのかなぁ。
交換可能なオブジェクト
だんだんと説明が微妙になってきましたが、ポリモーフィズムの自分なりの解釈を書いてみます。
処理について一意に名前をつけていた状態では、コードに書かれた名前の通りに処理が決定されます。一方、ポリモーフィズムでは条件によって実際に走らせる処理を変えることが出来ます。
オブジェクトで言えば、異なるクラスのメソッドやメンバー変数にアクセスできるようになります。これにより、交換可能なオブジェクトを作ることができるようになります。
オブジェクト指向プログラミング
結局のところオブジェクト指向プログラミングとは、__ポリモーフィズムを上手に利用したプログラミングスタイルのことを言う__のだと思います。従来はコードで表してた処理を、オブジェクトを入れ替えることで表す、と理解してます。
これにより、
- 複雑な機能を実装する、
- 新しい仕様を追加する、
ことが「比較的簡単にできる」ようになる、というのがメリットだと考えています。必ずしも開発が早くなる、ということではないと理解しています。
Strategy Patternの例
分かりやすい例として、ファイルを開く際に文字コードがUTF-8なのかSJISかどうかを判断して、どちらのクラスを使うかを決めます。例えば、こんな関数。
/**
* @param string $filename
* @return MyFileInterface
*/
function openFile($filename) {
$encode = find_file_encode_type($filename); // 文字コードを判定するマジック
$class = $encode . 'File';
return new $class($filename);
}
find_file_encode_type
という便利な関数があると仮定してですが…
この利点としては、
- 文字コードを判断するコードが一箇所になること、
- 各MyFileクラスで、文字コードを限定して考えられること、
- 利用する側としては、文字コードを考える必要がないこと、
- 対応する文字コードが増えても変更が限定されること、
などがあると思います。
これ以降の処理では、ファイルの文字コードを考える必要がなくなり、全体のコードがすっきりするのがわかると思います。また、ある文字コードのみでエラーが起きた場合、その文字コードのクラスにバグがあるだろう、という判断ができます。
最後に
オブジェクト指向プログラミングには、色々な流派があるようです。おそらく自分の解釈は、そのうちの一つを元にしただけで、あくまで限定的な解釈でしょう。
さらに言えば、これで「OOPについて開眼した」と思っても、プログラミングの能力がいきなり向上することは絶対にない、とはっきり言えます。
そもそも現実が複雑すぎるのです。それを簡略化してモデルに落とし込んで、やっとプログラムとして表しているだけで、OOPですら非力なぐらい。ちょっと油断すると、たまたま動いている状態になってしまいます。
逆に、プログラミングは人類には早過ぎるのかもしれません。すぐに複雑になって人間には理解できないコードになってしまいます。仕方なくOOPとか構造化とかで上手にコードを制限することでプログラミングしているのです。
プログラミングを上達させるには、コードを書く以外にないと思います。
とはいえ、闇雲に書くだけでは上達が遅いのも確かでしょう。自分が思いつく上達方法は、こんな感じです。
- SOLIDについて学ぶ。
- オブジェクト指向エクササイズを試してみる。
- デザインパターンを学ぶ。
- 良いソースコードから学ぶ。
- リファクタリングする。
コードを書いた後、上で学んだことを元にコードをリファクタリングするのが効果的に思います。ともあれ上達するのに近道はなくて、基本からガッツリ勉強するしかないんでしょうね。