ピカチュウから学ぶオブジェクト指向の第2弾はオブジェクトの継承についてです。
前回作成したピカチュウクラスを使用するので、もし基礎的な内容を学習したい人は、以下の記事を参考にしてください。
オブジェクトの継承が理解できれば、複雑で規模の大きなシステムを構築することができるようになります。また、重複する記述が減らせることで、保守性なども格段に向上して作業負担も軽減されるため、ぜひこの機会に覚えておきましょう。
オブジェクトの継承とは
オブジェクトの継承(PHP.net)
継承するということは、子となるクラスが親クラスのメソッドやプロパティを使用できるようにすることを意味します。前回作成したピカチュウクラスで考えてみましょう。
<?php
// ピカチュウ
class Pikachu
{
private $name = 'ピカチュウ';
public $nickname;
private $move = [
'でんきショック',
'なきごえ',
];
// クラス呼び出し時(インスタンス作成時)に自動で最初に実行される処理
public function __construct($nickname=null)
{
$this->nickname = $nickname ?? $this->name;
}
// 正式名称を取得する
public function getName()
{
return $this->name;
}
// 覚えている技の一覧を取得する
public function getMove()
{
return $this->move;
}
}
ニックネームを付ける機能や、名前や技の一覧を取得するというメソッドは、他のポケモン(ヒトカゲやゼニガメ等)でも同じくできなければなりません。しかし、これをポケモンを作るたびに準備するのは大変ですね。初代だけでも151匹分必要になります。
現在はクラス内の記述も少ないので問題ありませんが、よりゲームに近づけるとなれば更に多くの機能が必要になりますし、もし一部変更しなければならないようなことになれば、151匹分修正をかける必要があるのです。
そうならないためにも、共通して使える機能は親クラス上に記述して、そのクラスを継承して各ポケモンで使用できるようにします。
「ポケモン」クラスを作成(親クラス)
ピカチュウの親となるのはどういったクラスでしょうか?
細かくカテゴライズしていけば、より良い継承先が見つかるかも知れませんが、大きな区分で見れば「ポケモン」というカテゴリに属しています。
では、Pokemonというクラスを新しく作成しましょう。
<?php
// ポケモン
class Pokemon
{
protected $nickname;
protected function setNickname($nickname)
{
$this->nickname = $nickname ?? $this->name;
}
// 正式名称を取得する
public function getName()
{
return $this->name;
}
// ニックネームを取得する
public function getNickname()
{
return $this->nickname;
}
// 覚えている技の一覧を取得する
public function getMove()
{
return $this->move;
}
}
他のポケモンでも共通する、ニックネームのプロパティ($nickname)、正式名称を取得するメソッド(getName)、技の一覧を取得するメソッド(getMove)をピカチュウのクラスから移動してきました。
ピカチュウのコンストラクタで実行していたニックネームを付ける処理は、setNicknameとして新しくメソッドを作成しました。ニックネームのプロパティもポケモンクラスに移動させたので、こちらも取得用のメソッドとしてgetNicknameを作成しています。
※アクセス修飾子のprotectedについては後ほど解説します
クラスを継承する
ポケモンというクラスを継承するためには、子に当たるPikachuクラスにその旨を記述する必要があります。ピカチュウクラスまでを含めたコード全体を見てみましょう。
<?php
// ポケモン
abstract class Pokemon
{
protected $nickname;
protected function setNickname($nickname)
{
$this->nickname = $nickname ?? $this->name;
}
// 正式名称を取得する
public function getName()
{
return $this->name;
}
// ニックネームを取得する
public function getNickname()
{
return $this->nickname;
}
// 覚えている技の一覧を取得する
public function getMove()
{
return $this->move;
}
}
// ピカチュウ
class Pikachu extends Pokemon
{
protected $name = 'ピカチュウ';
protected $move = [
'でんきショック',
'なきごえ',
];
// クラス呼び出し時(インスタンス作成時)に自動で最初に実行される処理
public function __construct($nickname=null)
{
$this->setNickname($nickname);
}
}
$pikachu = new Pikachu;
echo $pikachu->getNickname();
$pikachu1 = new Pikachu('ピカイチ');
echo $pikachu1->getNickname();
echoでの出力結果は以下の通りです。
# 出力結果
ピカチュウ
ピカイチ
引数でニックネームを指定しなければ「ピカチュウ」、指定すれば「ピカイチ」と問題なく出力されていますね。
では、1つずつ確認してみましょう。まずはピカチュウのクラスからです。
class Pikachu extends Pokemon
クラス名の横に、新しくextends Pokemonという記述が追加されました。これがPokemonというクラスを親として継承するという意味になります。
次にコンストラクタ内の部分を確認してみましょう。
public function __construct($nickname=null)
{
$this->setNickname($nickname);
}
親(Pokemon)が持つニックネームの書き込み用メソッド(setNickname)を使用しています。親の持つメソッドは、自クラスが持つメソッドを使用するように$this->メソッド(orプロパティ)と記述すれば呼び出すことができます。
次にポケモンのクラスを見てみましょう。
abstract class Pokemon
classの前に新しくabstractと追加されました。これはクラスの抽象化です。
クラスの抽象化(PHP.net)
親クラスであるポケモン(Pokemon)を、ピカチュウのようにインスタンス化(実体化)されてしまっては困りますよね。ポケモンというポケモンは存在しないので当然です。そうならないために用意されているのがクラスの抽象化(abstract)です。これを付与することで、継承元(親クラス)としてだけ使用できるようになります。ポケモンのクラスは、あくまでピカチュウなどに共通するメソッドやプロパティを管理するためだけの親クラスでなければならないのです。
アクセス修飾子(protected)
他にも変更されたポイントがあります。それがアクセス修飾子であるprotectedです。
ピカチュウクラス内のnameプロパティについて見てみましょう。
protected $name = 'ピカチュウ';
こちらは前回privateにしていました。理由は、外部からアクセスされて別のものに変更されてしまっては困るからです。ですがprivateにしてしまうと、ピカチュウクラス内からでしかアクセスが出来ません。
ポケモンクラスを見てみましょう。
public function getName()
{
return $this->name;
}
正式名称を取得するための「getName」というメソッドでピカチュウクラス内のnameプロパティにアクセスしています。もしprivateであれば、親クラスのポケモンクラスからもアクセスが出来ません。
外部からアクセスされては困る、しかし親クラスなど継承先や継承元からはアクセス出来なければ困る、そんな都合の良い制限をかけるために準備されたのが「protected」なのです。
他にも、setNicknameなどのメソッドに対してもprotectedが用いられています。メソッドについても、外部からのアクセスは困るが、クラス内では有効可したい場合にはprotectedをつけて同じような制限を設けることが可能です。
クラスの継承を使うようになれば、publicとprotectedとprivateの3つを使い分ける必要があります。それぞれどのような時に使用するかを考えながら、適切なアクセス制限を設けるようにしておきましょう。
-
public
クラス内、クラス外のどこからでもアクセスが可能 -
protected
同じクラス及び子クラスからアクセスが可能 -
private
同じクラス内からのみアクセスが可能
技を覚えさせる
それでは新しく増設した継承クラスであるPokemonに新しいメソッドを追加してみましょう。今回は「技を覚えさせる」という機能を増設します。どのポケモンでも技を新しく覚えていくことができますね。そのため、これはピカチュウのクラスなどではなく、親となるポケモンのクラスへ記述します。
// 技を覚えさせる
public function setMove($move)
{
$this->move[] = $move;
// 技が4つを超過していれば、最初の技を削除
if(count($this->move) > 4){
unset($this->move[0]);
// 採番
$this->move = array_values($this->move);
}
}
setMoveというメソッドを新しく追加しました。これは外部からアクセスして使用するためpublicになります。
引数で技を指定して、それを新たにmoveプロパティ内にセットします。ポケモンのゲームでは技を覚えていられる最大数が4つなので、それを超過していれば一番上から消していきます。
それでは新しく「10万ボルト」「でんこうせっか」「かみなり」という3つの技を追加してみましょう。いかがコード全体図になります。
<?php
// ポケモン
abstract class Pokemon
{
protected $nickname;
protected function setNickname($nickname)
{
$this->nickname = $nickname ?? $this->name;
}
// 正式名称を取得する
public function getName()
{
return $this->name;
}
// ニックネームを取得する
public function getNickname()
{
return $this->nickname;
}
// 覚えている技の一覧を取得する
public function getMove()
{
return $this->move;
}
// 技を覚えさせる
public function setMove($move)
{
$this->move[] = $move;
// 技が4つを超過していれば、最初の技を削除
if(count($this->move) > 4){
unset($this->move[0]);
// 採番
$this->move = array_values($this->move);
}
}
}
// ピカチュウ
class Pikachu extends Pokemon
{
protected $name = 'ピカチュウ';
protected $move = [
'でんきショック',
'なきごえ',
];
// クラス呼び出し時(インスタンス作成時)に自動で最初に実行される処理
public function __construct($nickname=null)
{
$this->setNickname($nickname);
}
}
$pikachu = new Pikachu;
$pikachu->setMove('10万ボルト');
$pikachu->setMove('でんこうせっか');
$pikachu->setMove('かみなり');
var_export($pikachu->getMove());
$this->getMove()の出力結果は以下の通りです。
# 出力結果
array (
0 => 'なきごえ',
1 => '10万ボルト',
2 => 'でんこうせっか',
3 => 'かみなり',
)
順番に3つの技が追加されました。4つを超過したので、一番上にあった「でんきショック」が消えています。期待通りの結果ですね。
しかしこのままでは、ピカチュウが覚えられないような技を追加出来てしまいますし、技が重複してしまう可能性があります。
これでは困りますので、重複チェックと覚えられる技の制限を加えましょう。まずは覚えられる技のリストをピカチュウのクラスに追記します。
// ピカチュウ
class Pikachu extends Pokemon
{
protected $name = 'ピカチュウ';
protected $move = [
'でんきショック',
'なきごえ',
];
protected $move_list = [
'でんきショック',
'なきごえ',
'10万ボルト',
'かみなり',
'でんこうせっか',
];
// クラス呼び出し時(インスタンス作成時)に自動で最初に実行される処理
public function __construct($nickname=null)
{
$this->setNickname($nickname);
}
}
覚えられる技はポケモンによって異なります。なので、これはポケモンクラスではなく各ポケモンのクラスに対して割り振ります。また、外部からアクセスして変えられてしまっては困るので、外部からは不可、継承クラスからはアクセスが可能なprotectedを付与しておきましょう。
次に、覚えられる技と重複チェックをポケモンクラス内のsetMoveメソッドへ追記します。
// 技を覚えさせる
public function setMove($move)
{
// 重複チェック
if(in_array($move, $this->move, true)){
echo $move.'はすでに覚えています';
return;
}
// 覚えられる技かチェック
if(!in_array($move, $this->move_list, true)){
echo $move.'は覚えられません';
return;
}
$this->move[] = $move;
// 技が4つを超過していれば、最初の技を削除
if(count($this->move) > 4){
unset($this->move[0]);
// 採番
$this->move = array_values($this->move);
}
}
まずは重複チェックです。
if(in_array($move, $this->move, true)){
echo $move.'はすでに覚えています';
return;
}
in_arrayを使用して、現在覚えている技($this->move)に覚えようとしているものが存在しているかをチェックしています。もしあればエラーメッセージを出力して、その時点で処理を終了させるためにreturnを使います。
次に覚えられる技かチェックをします。
if(!in_array($move, $this->move_list, true)){
echo $move.'は覚えられません';
return;
}
同じようにin_arrayを使用して$this->move_list内に技があるかどうかをチェックしています。今回はリスト内に技がなければエラーメッセージを出す必要があるので、!in_arrayで逆の判定をしています。こちらも同じように、エラーメッセージを出せばreturnで処理を終了させます。
それでは確認してみましょう。
<?php
// ポケモン
abstract class Pokemon
{
protected $nickname;
protected function setNickname($nickname)
{
$this->nickname = $nickname ?? $this->name;
}
// 正式名称を取得する
public function getName()
{
return $this->name;
}
// ニックネームを取得する
public function getNickname()
{
return $this->nickname;
}
// 覚えている技の一覧を取得する
public function getMove()
{
return $this->move;
}
// 技を覚えさせる
public function setMove($move)
{
// 重複チェック
if(in_array($move, $this->move, true)){
echo $move.'はすでに覚えています';
return;
}
// 覚えられる技かチェック
if(!in_array($move, $this->move_list, true)){
echo $move.'は覚えられません';
return;
}
$this->move[] = $move;
// 技が4つを超過していれば、最初の技を削除
if(count($this->move) > 4){
unset($this->move[0]);
// 採番
$this->move = array_values($this->move);
}
}
}
// ピカチュウ
class Pikachu extends Pokemon
{
protected $name = 'ピカチュウ';
protected $move = [
'でんきショック',
'なきごえ',
];
protected $move_list = [
'でんきショック',
'なきごえ',
'10万ボルト',
'かみなり',
'でんこうせっか',
];
// クラス呼び出し時(インスタンス作成時)に自動で最初に実行される処理
public function __construct($nickname=null)
{
$this->setNickname($nickname);
}
}
$pikachu = new Pikachu;
$pikachu->setMove('かえんほうしゃ');
$pikachu->setMove('でんきショック');
$pikachu->setMove('10万ボルト');
var_export($pikachu->getMove());
出力結果は以下の通りです。
# 出力結果
かえんほうしゃは覚えられません
でんきショックはすでに覚えています
array (
0 => 'でんきショック',
1 => 'なきごえ',
2 => '10万ボルト',
)
最初にセットしようとした「かえんほうしゃ」はピカチュウが覚えられる技のリストに含まれていませんので、エラーメッセージが返ってきました。次にセットしようとした「でんきショック」はすでに覚えているので、同様にエラーメッセージが返ってきました。
最後にセットした10万ボルトは、覚えられるリスト内にも含まれており、現在は覚えていないので無事覚えることができました。
このようにして、共通して使いたい機能は継承クラスとして準備することで、他のポケモンを用意した際には独自の設定を追加するだけで同じように使用することができるのです。
まとめ
いかがだったでしょうか。
今回は「【PHP】ピカチュウから学ぶオブジェクト指向 〜クラス継承編〜」をご紹介しました。
オブジェクト(クラス)の継承ができるようになれば、管理はよりしやすくなり、大幅な変更などが生じても比較的対応がしやすくなります。また、ポケモンのように種類が多いようなものを管理するためには向いています。
PHPを学習中の方で、クラスを使ったシステムやサイトづくりを考えている人は、ぜひ参考にしてくださいね。
第3回はコチラ(レベルシステムの導入)