はじめに
この記事ではオブジェクト指向の基礎について紹介したいと思います。オブジェクト指向はいろんなアーキテクチャ等の基礎になっている部分ですのでこれを機に是非ご覧ください。
オブジェクト指向とは
オブジェクト指向(オブジェクトしこう、英: object-oriented)は、ソフトウェア開発とコンピュータプログラミングのために用いられる考え方である。元々は特定のプログラミングパラダイムを説明するために考案された言葉であり、その当時の革新的技術であったGUI(グラフィカル・ユーザーインターフェース)とも密接に関連していた。明確な用語としては1970年代に誕生し、1981年頃から知名度を得て、1986年頃からソフトウェア開発のムーブメントと化した後に、1990年頃にはソフトウェア開発の総合技術としての共通認識を確立している。ソフトウェア開発における一つの標語のような扱い方もされている。
オブジェクトとは、プログラミング視点ではデータ構造とその専属手続きを一つにまとめたものを指しており、分析/設計視点では情報資源とその処理手順を一つにまとめたものを指している。データとプロセスを個別に扱わずに、双方を一体化したオブジェクトを基礎要素にし、メッセージと形容されるオブジェクト間の相互作用を重視して、ソフトウェア全体を構築しようとする考え方がオブジェクト指向である。
難しいことを色々と言っていますが、要は システムを作る上での登場人物を「オブジェクト」という一つのデータ構造として定義する考え方と思っていただくとわかりやすいのかなと思います。
オブジェクト指向の例
例えば、犬と猫が戦うゲームを作るとします。
このとき、Dogというクラス(オブジェクトを作る設計図のようなもの)とCatというクラスを用意します
詳細は理解しなくていいので雰囲気で追ってください
<?php
class Dog
{
// メンバ変数: 犬の名前
public $name;
// メンバ変数: 犬の体力(HP)
public $health;
// メンバ変数: 犬の攻撃力
public $attackPower;
//コンストラクタといって、オブジェクトが生成されるとまず呼び出される
public function __construct()
{
$this->name = 'Dog';
$this->health = 100;
$this->attackPower = 15;
}
//攻撃するメソッド
public function attack($opponent)
{
$damage = $this->attackPower;
$opponent->takeDamage($damage);
echo "$this->name attacks $opponent->name for $damage damage!\n";
}
//ダメージを受けるメソッド
public function takeDamage($damage)
{
$this->health -= $damage;
if ($this->health < 0) {
$this->health = 0;
}
}
//生きているか判定するメソッド
public function isAlive()
{
return $this->health > 0;
}
//ステータスを表示するメソッド
public function displayStatus()
{
echo "$this->name: $this->health HP\n";
}
}
<?php
class Cat
{
public $name;
public $health;
public $attackPower;
public function __construct()
{
$this->name = 'Cat';
$this->health = 80;
$this->attackPower = 20;
}
public function attack($opponent)
{
$damage = $this->attackPower;
$opponent->takeDamage($damage);
echo "$this->name attacks $opponent->name for $damage damage!\n";
}
public function takeDamage($damage)
{
$this->health -= $damage;
if ($this->health < 0) {
$this->health = 0;
}
}
public function isAlive()
{
return $this->health > 0;
}
public function displayStatus()
{
echo "$this->name: $this->health HP\n";
}
}
そしてその戦いの様子をmain関数で表現するとこんな感じです
<?php
// 犬と猫のオブジェクトを生成
$dog = new Dog();
$cat = new Cat();
echo "勝負開始!\n\n";
//勝負が終わるまでループ
while ($dog->isAlive() && $cat->isAlive()) {
//犬が攻撃する
$dog->attack($cat);
//猫が倒れたか判定する
if (!$cat->isAlive()) {
echo "Dogのかち!\n";
break;
}
//猫が攻撃する
$cat->attack($dog);
//犬が倒れたか判定する
if (!$dog->isAlive()) {
echo "Catのかち!\n";
break;
}
// ステータスを表示する
$dog->displayStatus();
$cat->displayStatus();
echo "\n";
}
こんな風にプログラムに現実世界の物事をそのまま表現することができます。これがオブジェクト指向のわかりやすい一例です。実際にはプログラムを構成する要素をクラスとして表現して、それぞれのパーツとして捉えやすくします。
オブジェクト指向のメリット
細かく言えば色々あるのですが、データと振る舞いを適切な単位に区切られていてわかりやすい ことが何よりのメリットであると言えます。システムを作る上での登場人物を適切にクラスにすることによってその動作をイメージしやすくなります。そのため、処理を理解したりメンテナンスしやすくなります。
オブジェクト指向のデメリット
デメリットとしては難しいことです。これだけ言うと雑なので以下の二つに絞って説明します。
学習コストが高い
難しいということは学習コストが当然高いです。なので初学者がオブジェクト指向を用いたプロジェクトに参加したときに、すぐ活躍するのが難しくなってしまいます。
オブジェクト指向のメリットを活かすための設計が難しい
オブジェクト指向のメリットは様々あるんですが、それを活かすためにはいろんな観点を持った設計が必要です。また、時としてトレードオフになる部分も多いのでそれなりに熟練者であっても正しく活用するには難しい側面があります。
オブジェクト指向における重要要素
ここからはオブジェクト指向の理解を深めるための重要な要素について紹介します。
継承(extends)
継承とはオブジェクト指向において共通化をするためのテクニックです。継承では親クラスと子クラスという概念を用意します。この時、子クラスでは親クラスで定義されたものをそのまま使うことができます。
例えば動物を示す、Animalクラスがあるとします。このときに動物として鳥のクラスと犬のクラスを用意したいとします。鳥は鳴けて空が飛べます。一方犬は鳴けますし走れますが飛べません。このとき、どの動物でも鳴くという動作は共通しています。
そこで、Animalには共通する鳴くための関数を定義して、それぞれにしかできない動作は子クラスに定義することで共通化をすることができます。
このとき、共通化されているAnimalクラスに該当するクラスを親クラス。BirdやDogを子クラスといいます。
<?php
// 親クラス: 動物一般
class Animal
{
// どの動物でも「鳴く」機能は共通なので、親クラスに定義
public function cry()
{
echo "動物が鳴きます。<br>";
}
}
// 子クラス: 鳥
class Bird extends Animal
{
// 鳥に固有の「空を飛ぶ」機能
public function fly()
{
echo "鳥が空を飛びます。<br>";
}
}
// 子クラス: 犬
class Dog extends Animal
{
// 犬に固有の「走る」機能
public function run()
{
echo "犬が走ります。<br>";
}
}
// 実際に使ってみる
$bird = new Bird();
$bird->cry(); // 動物が鳴きます。
$bird->fly(); // 鳥が空を飛びます。
$dog = new Dog();
$dog->cry(); // 動物が鳴きます。
$dog->run(); // 犬が走ります。
委譲
委譲とは、クラスのメンバ変数として他クラスのオブジェクトを所持してそのメソッドを使うことです。例えばRPGで人間のクラスと武器のクラスがあります。人間は武器をもつ、つまりメンバ変数として所持しています。
攻撃したいときに攻撃するメソッドを呼び出す際に武器の攻撃メソッドを呼び出すことで攻撃できます。
<?php
// 剣クラス
class Sword
{
public function attack()
{
echo "剣で斬りつけた!\n";
}
}
// 人間クラス
class Human
{
// 人間は武器をメンバ変数として保持
private $weapon;
// コンストラクタで任意の武器を受け取り、メンバ変数にセット
public function __construct($weapon)
{
$this->weapon = $weapon;
}
// 攻撃メソッド。攻撃処理は武器の attack() メソッドに委譲
public function attack()
{
$this->weapon->attack();
}
}
// 剣を装備した人間
$heroWithSword = new Human(new Sword());
$heroWithSword->attack(); // 「剣で斬りつけた!」と表示
インターフェース
インターフェースは、クラスの外側から見えるクラスの形を定義するもの です。クラスに対してどのようなメソッドを持っているのか定義します。インターフェースを実装したクラスは実装クラスと呼ばれます。
<?php
// 乗り物が共通して持つ機能を定義するインターフェース
interface VehicleInterface
{
// エンジンをかける
public function startEngine();
// 加速する
public function accelerate($speed);
}
// 上記インターフェースを実装した「車」クラス
class Car implements VehicleInterface
{
private $speed = 0;
// インターフェースで定義されているメソッドを必ず実装する
public function startEngine()
{
echo "エンジンを始動しました。\n";
}
public function accelerate($speed)
{
$this->speed += $speed;
echo "車の速度が " . $this->speed . "km/h になりました。\n";
}
}
// 実行例
$myCar = new Car();
$myCar->startEngine(); // 「エンジンを始動しました。」と表示
$myCar->accelerate(20); // 「車の速度が 20km/h になりました。」と表示
一体これが何の役に立つのかと思った方もいるでしょう。しかしそれもカプセル化とポリモーフィズムが理解できると理解が進むのでもう少しお待ちください。
カプセル化
カプセル化とは特定のメンバ変数やメソッドを他クラスから隠匿することによって他クラスが目的のクラスやメソッドの内部を考慮しなくてよくなるテクニックのことです。
主なメリットとしては以下が挙げられます。
- 余計なデータやメソッドに干渉することを防ぐ
- メソッドの場合、開発者はメソッドの処理の内部を気にしなくて済む
オブジェクト指向では、オブジェクトが人間の考えうる動作をすることが大切です。例えばRPGでレベルアップすることでしかステータスが上がらないとします。このときステータスのメンバ変数の変更はレベルアップするメソッドでしかできなくなる必要があります。ですのでprivate関数とgetter(メンバ変数を取得するメソッドの総称)を駆使することによって、ステータス変更はステータス変更のメソッドでしか起こらなくなり、バグを起こす可能性を減らせます。
<?php
class Player
{
// ステータスをすべて private にすることで、直接アクセスを制限
private $level;
private $hp;
private $mp;
private $attack;
private $defense;
// コンストラクタで初期ステータスを設定
public function __construct()
{
$this->level = 1;
$this->hp = 100;
$this->mp = 50;
$this->attack = 10;
$this->defense = 5;
}
/**
* レベルアップするメソッド
* ステータスの上昇はここでのみ行われる
*/
public function levelUp()
{
$this->level += 1;
$this->hp += 20; // HPを20増加
$this->mp += 10; // MPを10増加
$this->attack += 5; // 攻撃力を5増加
$this->defense += 2; // 防御力を2増加
echo "レベルアップしました! 現在のレベル: {$this->level}\n";
}
/**
* 現在のステータスを表示する
*/
public function showStatus()
{
echo "レベル: {$this->level}\n";
echo "HP: {$this->hp}\n";
echo "MP: {$this->mp}\n";
echo "攻撃力: {$this->attack}\n";
echo "防御力: {$this->defense}\n";
}
/**
* ステータスを取得するためのメソッド (getter)
*/
public function getLevel()
{
return $this->level;
}
public function getHp()
{
return $this->hp;
}
public function getMp()
{
return $this->mp;
}
public function getAttack()
{
return $this->attack;
}
public function getDefense()
{
return $this->defense;
}
// 必要に応じて、メソッド経由でのみステータスを操作する形にできる
// (例:特殊なアイテムでHPを増やすメソッドなど)
}
// -------------- 実行例 --------------
// 新しいプレイヤーを生成
$player = new Player();
// レベルアップ前のステータスを表示
echo "[レベルアップ前]\n";
$player->showStatus();
// レベルアップ
$player->levelUp();
// レベルアップ後のステータスを表示
echo "\n[レベルアップ後]\n";
$player->showStatus();
// 下記のような直接代入はできない(private のためエラーになる)
// $player->hp = 9999;
インターフェースとカプセル化
実はインターフェースを用いることで、そのクラスでの処理をカプセル化をすることができます。インターフェースはクラスを呼び出す外部に対してメソッドとその引数、返り値を提示します。一方、外部クラスからはインターフェース越しにのみわかる情報しか参照できません。
interface WeaponInterface
{
public function attack();
}
例えばこの武器のインターフェースを実装したクラスがあるとします。ですが、このクラスに対して外部からわかることは attack()メソッドを持っていることだけです。
つまり処理の内部は隠匿できます。このように、インターフェースを定義して実装クラスを作ることはカプセル化の手段の一つです。少し難しい話なのでよくわからない方は読み飛ばして下さい。
ポリモーフィズム
ポリモーフィズムはインターフェースのメリットであるカプセル化を活かしたテクニックです。まずは以下のコードをご覧ください
<?php
// 武器のインターフェイス
interface WeaponInterface
{
public function attack();
}
// 剣クラス
class Sword implements WeaponInterface
{
public function attack()
{
echo "剣で斬りつけた!\n";
}
}
// 弓クラス
class Bow implements WeaponInterface
{
public function attack()
{
echo "弓矢を放った!\n";
}
}
// 人間クラス
class Human
{
// 人間は「武器(WeaponInterface)」をメンバ変数として所持
private $weapon;
// コンストラクタで武器を受け取る
public function __construct(WeaponInterface $weapon)
{
$this->weapon = $weapon;
}
// 攻撃メソッド。実際の攻撃処理は weapon の attack() に委譲する
public function attack()
{
$this->weapon->attack();
}
}
// ----------- 実行例 -----------
// 剣を装備した人間
$hero = new Human(new Sword());
$hero->attack(); // 「剣で斬りつけた!」と表示
// 弓を装備した人間
$hero = new Human(new Bow());
$hero->attack(); // 「弓矢を放った!」と表示
この処理では武器クラスの実装クラスの剣と弓と人間が登場します。ここで$hero->attack();
の部分に着目して下さい。この部分では剣と弓でそれぞれ別の攻撃をしているはずです。しかし呼び出すメソッドは一見同じように見えます。このようにポリモーフィズムとは処理の見かけは一緒になのに、参照するオブジェクトによってその挙動を変更できる テクニックなのです。
オブジェクト指向を正しく使うには
今回紹介したのはあくまでもオブジェクト指向を理解する上での基本概念であり、道具の説明書のようなものです。ここからオブジェクト指向を正しく使いこなすにはもっと学習を深めていく必要があります。
例えばSOLID原則と呼ばれる原則はオブジェクト指向というのはこうあるべきだという考え方を提唱しています。またデザインパターンと呼ばれるオブジェクト指向のベストプラクティス集のようなものもあります。こういったものを教材にオブジェクト指向のあり方をインプットすることを強くお勧めします。というのもこれらの技術は組み合わせ方次第で多様なことができるのですが、それ故に間違ったことをしてしまうケースも多々あるからです。詳しくは「オブジェクト指向 アンチパターン」などで検索してみて下さい。
オブジェクト指向の正解は諸説あり
ここまで学習してわかった方も多いと思うのですがオブジェクト指向は基礎と呼ばれつつも結構難しい概念です。 それ故に世間ではどういう風にオブジェクト指向を用いるべきかという議論は今も活発であり、日々進化しているといっても過言ではないでしょう。また、その難しさからエンジニア同士で意見が衝突することも多い分野です。ですので、自分の持っているオブジェクト指向の見解は正しいのか?という観点をもって学習していくのがいいんじゃないかなと思います。
まとめ
以上、自分の観点でまとめたオブジェクト指向の基礎でした!改めて言語化してみると難しい概念だということを再認識させられました。正直、少し言語化が上手くできてないと思う点もあるので随時更新するかもしれません。少しでも皆様の学習に役立てていただければと思います!