はじめに
自分なりに考えている、オブジェクト指向プログラミングとはどういったものかを、小難しいオブジェクト指向用語は使わずに説明する。また犬や猫が鳴いたり車が走ったりするような抽象的なものではななく、具体的な機能を持つコードを使って説明する。
ここではオブジェクト指向言語の機能については説明しない。(例: クラスとは何か、インスタンスはどうやって作るのかなど)
オブジェクト指向プログラミングの特徴
「オブジェクト指向プログラミングの特徴は、カプセル化、継承、多態性、ダイナミックバインディングである」とかいう、専門用語を専門用語で説明した定義があるがとりあえずこれは忘れよう。
オブジェクト指向プログラミングの特徴は、大きく分けると2つある。
1つは、処理ではなく「機能」を1つのカタマリとして切り分けられるということ。
1つは、機能のカタマリを「交換できる」ということ。
以下でその具体例を出して説明する。
処理ではなく「機能」を1つのカタマリとして切り分けられる
ここでは非オブジェクト指向とオブジェクト指向の差を示すための例として、1~100の数について、それが3の倍数だったときは"fizz"グループだと判断、それが5の倍数なら"buzz"グループだと判断して、それぞれのグループの数字が何個存在するか数えるという処理を作る。
非オブジェクト指向で普通にコードを書くと、こうなる。(コードはjavascriptで書いている)
function main() {
var fizzCount = 0; // 初期化
var buzzCount = 0;
for(var num=1; num<=100; num++) {
if (num % 3 == 0) { fizzCount++; } // カウント
if (num % 5 == 0) { buzzCount++; }
}
var result = {}; // 結果組み立て
result["fizz"] = fizzCount;
result["buzz"] = buzzCount;
console.log(result);
}
このコードは少しごちゃごちゃしているので「fizz,buzzを数える機能」を切り分けたいと考える。
このとき「処理をサブルーチンに切り分ける」という素朴な手法で機能を切り分けようとしても、うまくいかない。(「fizz,buzzを数える機能」は数を記憶する変数の初期化や、実際に数を数えるところや、結果を組み立てるところにコードが分かれているため)
しかしオブジェクト指向であれば「fizz,buzzを数える機能」を1つのオブジェクトにまとめて、切り分けることが可能になる。このような切り分けができれば、プログラマはメイン処理を書いてるときにfizzとbuzzをどうやって数えればいいのかを考える必要がなくなる。また、登場する変数が減るので覚えておかなければならないことが少なくなって楽になる。
function main() {
var counter = new FizzBuzzCounter()
for(var num=1; num<=100; num++) {
counter.count(num);
}
console.log(counter.getCountResult());
}
class FizzBuzzCounter {
constructor() {
this.fizzCount = 0; // 初期化
this.buzzCount = 0;
}
count(num) {
if (num % 3 == 0) { this.fizzCount++; } // カウント
if (num % 5 == 0) { this.buzzCount++; }
}
getCountResult() {
var result = {}; // 結果組み立て
result["fizz"] = this.fizzCount;
result["buzz"] = this.buzzCount;
return result;
}
}
機能のカタマリを「交換できる」
次に、fizzとbuzzだけではなく偶数と奇数の数を数える処理も必要になったとしよう。mainに渡す引数が"fizzbuzz"ならfizzとbuzzを数えて、そうでない場合は偶数と奇数を数えることにする。
何か処理を共通化ができそうな気がするが、非オブジェクト指向ではどうもうまくいかない。無理にコードを使いまわそうとすると、以下のような悲惨なコードになる。
function main(args) {
var count1 = 0; // fizzbuzzのときはfizz、偶数奇数のときは偶数カウント
var count2 = 0; // fizzbuzzのときはbuzz、偶数奇数のときは奇数カウント
for(var num=1; num<=100; num++) {
if (args == "fizzbuzz") {
// fizzbuzz
if (num % 3 == 0) { count1++; }
if (num % 5 == 0) { count2++; }
} else {
// 偶数奇数
if (num % 2 == 0) { count1++; }
else { count2++; }
}
}
var result = {};
if (args == "fizzbuzz") {
// fizzbuzz
result["fizz"] = count1;
result["buzz"] = count2;
} else {
// 偶数奇数
result["even"] = count1;
result["odd" ] = count2;
}
console.log(result);
}
しかしオブジェクト指向であれば「fizz,buzzを数える機能」と「偶数,奇数を数える機能」を交換しても使えるようにすることで、以下のようなスマートな共通化ができる。
処理が綺麗に書けるということだけではなく「FizzBuzzCounterクラスやmain処理の後半部分は、機能追加前からまったく変化しない」というメリットもある。
function main(args) {
var counter;
if (args == "fizzbuzz") {
counter = new FizzBuzzCounter();
} else {
counter = new EvenOddCounter();
}
// mainの、ここ以降は変化していない
for(var num=1; num<=100; num++) {
counter.count(num);
}
console.log(counter.getCountResult());
}
class FizzBuzzCounter { // 機能追加前のFizzBuzzCounterクラスと完全に同じコード
constructor() {
this.fizzCount = 0; // 初期化
this.buzzCount = 0;
}
count(num) {
if (num % 3 == 0) { this.fizzCount++; } // カウント
if (num % 5 == 0) { this.buzzCount++; }
}
getCountResult() {
var result = {}; // 結果組み立て
result["fizz"] = this.fizzCount;
result["buzz"] = this.buzzCount;
return result;
}
}
class EvenOddCounter {
constructor() {
this.evenCounter = 0; // 初期化
this.oddCounter = 0;
}
count(num) {
if (num % 2 == 0) { this.evenCounter++; } // カウント
else { this.oddCounter++; }
}
getCountResult() {
var result = {}; // 結果組み立て
result["even"] = this.evenCounter;
result["odd" ] = this.oddCounter;
return result;
}
}
まとめ
処理ではなく「機能」を1つのカタマリとして切り分けられるというのは、カプセル化にあたる。この特徴を活用すると何が嬉しいかというと、「サブルーチン」という素朴な手法では切り分けられなかった機能を切り分けられるようになることが嬉しい。
機能のカタマリを「交換できる」というのは、多態性にあたる。この特徴があると何が嬉しいかというと、機能のカタマリをまるっと入れ替えることで、機能を呼び出す側がifで処理を分けなくても(場合によって全く修正しなくても)何種類もの動作をさせることができることが嬉しい。
これを踏まえた上で「オブジェクト指向とは何か」という禅問答に答えるなら、以下のようになるだろうか。
従来よりも機能を上手くまとめて扱えるようにするための手法。
そのために、「データ」と「データを使った処理」をまとめたオブジェクトというものを使う。