はじめに
こんにちは、MakeIT AdventCalendar2018 19日目を担当する @itti1021 です。
明日は @yozakura121 さんが投稿してくださいます。
簡単な自己紹介をしますと、僕は約半年前にJavaScriptからプログラミングの勉強を始めて、Javaの入門書を読み、最近PHPに興味が出てきた言語ふらふらマンです。(今年度中に絞りたい)
当初はJavaでコード書いて云々しようと思っていたのですが、時間と面白くできる自信が無かったので止めました。この記事が面白くできたとは言ってない。
じゃあ何するの?ということでオブジェクト指向についての今の認識を書き留めておきたいと思います。
記事にするため文章化していますが、あくまで現時点における個人の見解なのでお手柔らかに。
内容
まずオブジェクト指向って何?
オブジェクト指向とは、システム開発で用いる部品化の考え方。
つまり、現実に存在する物をオブジェクトという部品に分割または置き換えをすることで、プログラムを人間が把握しやすいようにしよう、という考え方です。
現実世界を模して作り、現実世界で変化があった時には対応する部品を修正・交換する。
こうすることでプログラミングに頭を捻る時間を削減したり、多人数開発での齟齬を減らすことができます。
オブジェクト指向における基本要素
オブジェクト指向にはクラス、フィールド、メソッド、インスタンス等の要素があり、今回は上記の4つについて触れようと思います。
- クラス
クラスとはプログラマが定義した、対象物の設定をまとめて固有の名前を付けたもので、人間が見やすく、扱いやすくするための設計図のようなものです。
クラスには後述するフィールドとメソッドがあり、対象についてそれがどのようなものなのか、どのように動くのかを記述します。
(フィールド、メソッドには他の名称もありますが今回は省略)
人間クラス・飛行機クラス・たこ焼きクラス 等
- フィールド
そのクラスが持っているもの、特性を記述します。
例えば人間クラスなら、目フィールド・鼻フィールド・口フィールド等を設定することになります。
- メソッド
そのクラスの動き、反応を記述します。
同じく人間クラスなら、走るメソッド・寝るメソッド・食べるメソッド等を設定します。
- インスタンス
クラスから生み出されるプログラミングで実際に扱う実体のことです。
例に挙げている人間クラスからは人間インスタンスを作ることができます。
オブジェクト指向でしたいこと
どんなものなの(フィールド)
何ができるの(メソッド)
を合わせた金型(クラス)
を使って実体(インスタンス)を作り、
これを動かすことでプログラムという仮想世界に現実世界を再現することです。
実際にJavaのコードをちょっぴり
とりあえず一通り要素を紹介しましたが文章だけではつまらないのでここからは内容に即したJavaのコードを載せていきます。
(コンパイルエラーやインスタンス化等の細かい誓約、ゲームの仕様は考慮しないものとします。また、適宜省略致します。)
書くの大変なので 本題から逸れるのでご容赦ください。
皆さんご存知(?)
某有名ゲームの看板モンスターです。
上記4つの要素でスライムを表すためのJavaのコードがこちらです。
// クラス
public class Slime {
// フィールド
String name = "スライム"; // スライムA,スライムB,スライムC 名前を宣言
int hp = 5; // HPを宣言
int mp = 0; // MPを宣言
// メソッド
void attack(Hero heroInstance) {
heroInstance.hp = hp - 1;
System.out.println(heroInstance.name + " に 1のダメージ!");
}
void run() {
// にげる処理
System.out.println(this.name + " は にげだした!");
}
}
// 呼び出し元からインスタンス化
Slime slimeInstance = new Slime();
これでスライムクラスは仮想世界でスライムインスタンスという実体になりました。
オブジェクト指向の三大機能
さらに、オブジェクト指向プログラミングをより楽に、エラーを減らす仕組みがあるのです。
それは**隠蔽・継承・多態性**です。
隠蔽 (カプセル化)
フィールドへの読み書きやメソッドの呼び出しを制限する機能です。
現実世界でも財布やパソコンの中身等、使われて困るもの、見られて困るものは持ち歩いたり制限することで管理しますよね。
オブジェクト指向ではクラスにアクセス制御をかけようということです。
では、隠蔽しないとどうなるのか?
以下のコードのようになります。
// 勇者クラス
public class Hero {
String name = "勇者"; // 名前宣言
int hp = 100; // HP100の勇者を宣言
int mp = 50; // MP宣言
String state = default; // 状態異常無し
// 略
void attack(Slime slimeInstance) {
// たたかう処理
}
void run() {
// にげる処理
}
void useItem(Item itemInstance) {
// どうぐを使う処理
}
// 略
}
// 僧侶クラス
public class Priest {
String name = "僧侶";
int hp = 70;
int mp = 100;
String state = default;
// 略
// ベホマ 勇者のHPを全回復
void healAll(Hero heroInstance) {
if (this.mp > 9) {
// 自分のMPが10以上なら
heroInstance.hp = -100;
this.mp = this.mp - 10;
System.out.println(heroInstance.name + " の HP は 全回復した!");
} else {
System.out.println("MP が 足りない!");
}
}
}
HP100の勇者を作成し、その勇者にベホマ(HP全回復の呪文)を使用するためのコードです。
ところが、これを実行すると勇者のHPは-100になってしまいます。
healAllメソッドでタイピングミスをしているからです。
void healAll(Hero heroInstance) {
heroInstance.hp = -100; // heroInstance.hp = 100; をタイピングミス
}
このようにアクセス制御を行わないと現実ではありえない状態ができてしまうのです。
そのため、まずはフィールドにprivateという外部から扱えないようにするアクセス修飾子を付けます。
public class Hero {
// int hp = 100; に追加
private int hp = 100;
}
しかし、このままではhealAll等の外部からのメソッドが使えなくなってしまうため、自クラスが持っているフィールドの情報を他クラスに渡すgetterメソッドと、他クラスから受け取った値を自クラスのフィールドに反映させるsetterメソッドを追加します。
public class Hero {
// HP宣言
private int hp = 100;
>
// getterメソッド
public int getHp() {
return this.hp; // 呼び出し元にHPを渡す
}
>
// setterメソッド
public void setHp(int hp) {
this.hp = hp; // 呼び出し元から受け取ったHPをセット
}
このgetter/setterメソッドはpublicというアクセス修飾子を設定しているのでどこからでも呼び出すことができます。
また、PriestクラスはHeroクラスのフィールドを直接扱うことができなくなったのでhealAllメソッドを書き換えます。
public class Priest {
void healAll(Hero heroInstance) {
if (this.mp > 9) {
// heroInstance.hp = -100; を変更
int putHp = -100
heroInstance.setHp(putHp);
this.mp = this.mp - 10;
System.out.println(heroInstance.getName() + " の HP は 全回復した!");
} else {
System.out.println("MP が 足りない!");
}
}
// 実行
Hero heroInstance = new Hero(); // 勇者インスタンス
Priest priestInstance = new Priest(); // 僧侶インスタンス
priestInstance.healAll(heroInstance); // ベホマを勇者に使う
System.out.println("HP: " + heroInstance.getHp());
// 実行結果
HP: -100
実行結果は変わっていませんがこれで一応隠蔽はできました。
そして、この段階で既に2つのメリットがうまれています。
-
Read Only/Write Onlyのフィールドが実現できる
setterメソッドを設定しなければRead Only (外部から読めるが書き換えられない)
getterメソッドを設定しなければWrite Only (外部から書き換えられるが読めない) -
フィールド名等のクラス内部の情報を自由に書き換えることができる
例えば、今回設定しているHPというフィールド名をcurrentHp(現HP)というフィールド名に変更したくなったとします。
もし、getter/setterメソッドを準備せず、外部のクラスで直接フィールドを読み書きしていた場合、そのクラスの内部まですべて書き換えなければなりません。
// 書き換え例
public class Priest {
void healAll(Hero heroInstance) {
// heroInstance.hp = -100; を変更
heroInstance.currentHp = -100;
}
1つ2つのクラスならまだしも大規模の開発でこれをすることになったら大変ですよね。
正常に動く所を間違えて改変したり、タイピングミスを誘発します。
もともとの問題の解決
隠蔽しただけでは、結局勇者のHPに-100が設定できてしまうので、最後にsetterメソッドに妥当性チェックを加えます。
public class Hero {
setHp(int hp) {
// this.hp = hp を変更
if (hp < 0 || hp > 100) { // 0 ~ 100 以外
throw new IllegalArgumentException("HPに設定されようとしている値が異常です。");
}
this.hp = hp;
}
これで先程のコードを実行します。
// 実行
Hero heroInstance = new Hero(); // 勇者インスタンス
Priest priestInstance = new Priest(); // 僧侶インスタンス
priestInstance.healAll(heroInstance); // ベホマを勇者に使う
System.out.println("HP: " + heroInstance.getHp());
// 実行結果 エラー文
"HPに設定されようとしている値が異常です。"
いくら気を付けていてもミスは起こります。
丁寧に準備して未然に防ごうということですね。
クラスを外部から勝手に見られたり書き換えらたりしない1つの**オブジェクト(対象物)**として作り、これを集めてプログラムにして、現実と矛盾しないようにするため、隠蔽が重要なのです。
隠蔽のまとめ
- 読み書きを限定できる
- クラス内部を改変しやすい
- フィールドを外部から直接扱えないため安全
継承 (インヘリタンス)
その名の通り、クラスからクラスへ、持っている要素を引き継ぐことができる機能です。
例えばスライムから、毒を持っているバブルスライムを新しく作るとします。
こんな時一からまたクラスを書くのでしょうか?
バブルスライムはスライムと同じフィールドとメソッドを持っていて、攻撃に毒効果が付与されています。
違う箇所は攻撃の内容ということですね。
バブルスライム is スライム (バブルスライムはスライムの一種である) が成り立ちます。
このis-aの関係の時オブジェクト指向では継承を使います。
public class BubbleSlime extends Slime { // スライムクラスを継承
private String name = "バブルスライム";
private int hp = 10; // HPを10で再設定
public void attack (Hero heroInstance) {
// ダメージ部
heroInstance.setHp(heroInstance.getHp() - 1);
System.out.println(heroInstance.getName() + " に 1のダメージ!");
// 毒判定
boolean result = new java.util.Random().nextBoolean(); // 真か偽を生成
if (result) {
heroInstance.setState(poison); // resultが真なら毒状態にする
System.out.println(heroInstance.getName() + " は どくにおかされた!");
}
}
クラス宣言の後にextends class (継承したいクラス) を記述して、継承元クラスとの差分を書きます。
これだけでいいんです。
// 実行
BubbleSlime bubbleSlimeInstance = new BubbleSlime(); // バブルスライムインスタンス
bubbleSlimeInstance.run(); // バブルスライムにrunメソッドを使わせる
// 実行結果
"バブルスライム は にげだした!"
スライムクラスを継承しているので、バブルスライムクラスでは定義しなくてもrunメソッドが使えます。
このように同じ部分は記述を避けることができ、コードが見やすくなります。
さらに、継承を使うともう1つ恩恵を受けることができます。
スライムクラスのrunメソッドを"にげだした!"から"すべって にげた!"に変更するとします。
public class Slime {
run() {
// にげる処理
System.out.println(this.name + " は すべって にげた!");
}
}
もし、バブルスライムクラスを継承を使わず一から作成していると、スライムクラスを修正した後バブルスライムクラスも修正しなくてはなりません。
これが1つ2つと増えるにつれ、記述量・修正忘れ・タイピングミスも増えていき、数十、数百になるに至っては管理できなくなるかと思います。
継承を使っていれば、元のクラスを修正するだけですべての継承後のクラスに反映することができるのです。
継承のまとめ
- コードが見やすく簡潔になる
- プログラマー側のミスを削減できる
- エラーの修正箇所の特定が容易になる
記述量が減る上に管理もしやすくなるって良いですよね。僕は特に好きです。
多態性 (ポリモーフィズム)
インスタンスを曖昧にとらえる機能です。
非常に言語化しづらいため一言でいうとこうなりました。
今度はスライムクラスの元クラスを考えます。
スライムといえば敵ですよね。ということでエネミークラスを定義します。
public abstract class Enemy {
private String name; // 名前を宣言
private int hp; // HPを宣言
private int mp; // MPを宣言
abstract public void attack(Hero heroInstance) {}
abstract public void run() {}
attackメソッドやrunメソッドは継承するモンスターで記述が変わるので空欄にします。
メソッドを空欄にするときにはクラス宣言の前にabstractを付け、抽象クラスにします。
抽象クラスとはインスタンス化 (実体化) できないクラスのことです。
attackメソッド (攻撃命令) を出しても何もせず、説明文すら出ない敵なんて実体化させても扱いようがないですよね。
public class Bat extends Enemy { // エネミークラスを継承
private String name = "ドラキー";
private int hp = 10;
private int mp = 3;
public void attack (Hero heroInstance) {
// ダメージ部
heroInstance.setHp(heroInstance.getHp() - 3);
System.out.println(heroInstance.getName() + " に 3のダメージ!");
}
public void run() {
// にげる処理
System.out.println(this.name + " は とんで にげた!");
}
継承の流れ
ごちゃごちゃしてきたのでまとめますと、
(継承元←継承先)
スライム
Enemy class (実体化できない抽象クラス)
↑
Slime class
↑
BubbleSlime class (attackメソッドに毒判定を追加)
ドラキー
Enemy class (実体化できない抽象クラス)
↑
Bat class
となっています。
ここまでは準備です。
では実際に、曖昧にとらえるとはどういうことなの?というと、下記を例とします。
public class Hero {
public void attack(Slime slimeInstance) {
slimeInstance.setHp(slimeInstance.getHp() - 5); // スライムに5ダメージ
}
public void attack(Bat batInstance) {
batInstance.setHp(batInstance.getHp() - 5); // ドラキーに5ダメージ
}
もし、インスタンスをすべて厳密にとらえると、スライムに対するattackメソッド、ドラキーに対するattackメソッド、のように対象とするインスタンスに合わせてそれぞれのメソッドを用意しなくてはいけません。
スライムもドラキーも厳密には型が違いますが、同じ敵モンスターです。
なので、敵として曖昧に扱おうということです。
(Javaではなんの変数なのかintやString等の型宣言が必要で、基本的にはSlime classならSlimeの型、Bat classならBat型にインスタンスを代入するのですが、抽象クラスの型に継承されているクラスを入れることは許されています。)
// 実行
Enemy slimeInstance = new Slime(); // スライムインスタンス
Enemy batInstance = new Bat(); // ドラキーインスタンス
public class Hero {
public attack(Enemy enemyInstance) {
enemyInstance.sethp(enemyInstance.getHp() - 5); // 敵に5ダメージ
}
このように同じEnemy型のインスタンスとすることでまとめて処理できるようになります。
また、敵モンスターのrunメソッドでも効果があります。
// 実行
Enemy[] enemies = new Enemy[3]; // 出現した敵
enemies[0] = new Slime(); // スライム
enemies[1] = new BubbleSlime(); //バブルスライム
enemies[2] = new Bat(); // ドラキー
for (Enemy enemyInstance : enemies) { // 出現した敵を一匹ずつEnemy型に入れる
enemyInstance.run(); // Enemy型に入っているインスタンスのrunメソッドを使う
}
// 実行結果
"スライム は すべって にげた!"
"バブルスライム は すべって にげた!"
"ドラキー は とんで にげた!"
呼び出す側はどのモンスターにも同じrunメソッドを使わせたのですが、それぞれのモンスターは自分に決められた動きをします。
1つの命令から多くの状態を生み出す可能性がある、多態性の語源です。
多態性のまとめ
- 厳密には異なるインスタンスをまとめて処理できる
- 個々のインスタンスが各クラスでの定義に従った動作を行う
言語化するのが難しかったです、僕自身もっとメリットあるのでは?と思っています・・・。
総括
ここまで記事を読んでくださりありがとうございます。
今回は省略しましたが、メリットだけでなく色々と誓約もあるので気になった方は是非調べてみてください。
また、有識者の方々、僕自身知識も経験も浅いため、何言ってんだコイツというのがありましたらマサカリを。
☝️☝️☝️きっと明日の僕