はじめに
すみません、タイトルは遊んでいます。
PHPと遅延静的束縛と、オブジェクト指向の継承、委譲について解説します。
前提としてオブジェクト指向の継承を理解しているものとします。
そのため、オブジェクト指向の継承について理解できていない場合は、より混乱すると思われます。
この記事を書いた動機、または背景
弊社のサービス拡張を実装している中で
共通処理をSuperクラスに定義してパラメータをSubクラスから変更する意図をもった既存コードがありました。
今回の拡張ではパラメータだけではなく、SuperクラスのメソッドをSubクラスでオーバーライドして
オーバーライドしたメソッドをSuperクラスの共通処理から呼び出したかったため遅延静的束縛を利用しました。
なお、そもそもの話としてSuperクラスからSubクラスでオーバーライドしたメソッドを呼び出すことの是非、
及び継承関係を利用した実装より委譲を利用した実装にするべきではないか、ということも検討してみます。
サンプルコード1
前提として下記のコードがあります。
<?php
class Super {
/*
* ここはprivateしてconstructorないしはsetterで設定
* そもそもValueObjectにするべきなどなどツッコミどころは多いですが
* それはさておきで進みます...
*/
protected $parameter1 = 'Super1';
protected $parameter2 = 'Super2';
public function initialize(): array
{
return [
'parameter1' => $this->getParameter1(),
'parameter2' => $this->getParameter2(),
'parameter3' => $this->getParameter3(),
];
}
private function getParameter1(): string
{
return $this->parameter1;
}
private function getParameter2(): string
{
return $this->parameter2;
}
private function getParameter3(): string
{
return 'Super3';
}
}
/*
* Subクラスでパラメータのみ上書きします
*/
class Sub1 extends Super {
protected $parameter1 = 'Sub1-1';
protected $parameter2 = 'Sub1-2';
}
/*
* Initialize()実行の戻り値は下記の通りです。
* [
* 'parameter1' => 'Sub1-1',
* 'parameter2' => 'Sub1-2',
* 'parameter3' => 'Super3',
* ]
*/
(new Sub1())->initialize();
getParameter3()
をオーバーライドしたい
ここでSuperクラスで設定している parameter3
の値をSubクラスから変更したいとします。
また、Superクラスを継承しているSubクラスは他にも多数あるものとします。
上記サンプルだけをみると、 getParameter3
の戻り値をSuperクラスのプロパティに変更すればいいのでは?
と思えますが、Superクラスを継承している既存クラスが多数あると考えると、どこかで parameter3
を宣言して使用しているかもしれません。
そのため getParameter3()
をオーバーライドすることでSuperクラスで設定している
parameter3
をSubクラスから変更してみましょう。
<?php
class Sub2 extends Super {
// ここのスコープがprivateだと、この後の遅延静的束縛でエラーになる
protected function getParameter3(): string
{
return 'Sub2-3';
}
}
/*
* Initialize()実行の戻り値は下記の通りです。
* [
* 'parameter1' => 'Super1',
* 'parameter2' => 'Super2',
* 'parameter3' => 'Super3',
* ]
*/
(new Sub2())->initialize();
しかし、 initialize()
では getParameter3()
を this
で呼び出しているため期待する結果は得られません。
※ initialize()
内の this
はクラス解析時に Super
として解釈される
SuperクラスからSubクラスでオーバーライドしたメソッドを実行する
ここで、SuperクラスからSubクラスでオーバーライドしたメソッドを実行するため 遅延静的束縛 を利用します。
Superクラスで遅延静的束縛を利用するよう変更する
<?php
class Super {
/* 〜 省略 〜 */
public function initialize(): array
{
// thisによるメソッド呼び出しをstaticに変更する
return [
'parameter1' => $this->getParameter1(),
'parameter2' => $this->getParameter2(),
'parameter3' => static::getParameter3(),
];
}
/* 〜 省略 〜 */
}
/* 〜 Subクラスの宣言は前述の通りのため省略します 〜 */
// これにより、期待通りにSubクラスでオーバーライドしたメソッドをSuperクラスから呼び出せます
/*
* Initialize()実行の戻り値は下記の通りです。
* [
* 'parameter1' => 'Sub1-1',
* 'parameter2' => 'Sub1-2',
* 'parameter3' => 'Super3',
* ]
*/
(new Sub1())->initialize();
/*
* Initialize()実行の戻り値は下記の通りです。
* [
* 'parameter1' => 'Super1',
* 'parameter2' => 'Super2',
* 'parameter3' => 'Sub2-3',
* ]
*/
(new Sub2())->initialize();
何が起きているのか
PHPはプログラム実行前にコードの解析・解釈を行なっています。
サンプルコード1の Super
クラスを解析・解釈した時に this
は Super
として解釈されます。
そのため、 Sub2
クラスで getParameter3()
メソッドをオーバーライドしても
initialize()
メソッドの実行時に Super
クラスの getParameter3()
が呼び出されます。
しかし、遅延静的束縛を利用した場合は initialize()
メソッドの実行時まで static
の解釈を行いません。
そして、 initialize()
メソッドの実行時に呼び出し元となる Sub2
クラスを static
として解釈します。
static
を Sub2
クラスとして解釈するため、 Super
クラスから Sub2::getParameter3()
を呼び出すため
Sub2::getParameter3()
のメソッドスコープを protected
としておく必要があります。
※PHP公式ドキュメントによるサンプル
この仕組みを利用することにより、Subクラスでオーバーライドしたメソッドを
Superクラスから呼び出すことが可能となります。
継承と委譲について
コチラ でも言及されているように、一般的には継承より委譲を使うことが勧められています。
上記では継承関係を元に遅延静的束縛を利用してみましたが、委譲で行なってみようと思います。
サンプルコード2
<?php
/*
* initialize処理を別クラスに切り出す
*/
class Main {
public function initialize(CommonMethods $executor): array
{
return [
'parameter1' => $executor->getParameter1(),
'parameter2' => $executor->getParameter2(),
'parameter3' => $executor->getParameter3(),
];
}
}
interface CommonMethods {
public function getParameter1(): string;
public function getParameter2(): string;
public function getParameter3(): string;
}
class Super implements CommonMethods {
// オーバーライドを禁止するためスコープをprivateに変更
private $parameter1 = 'Super1';
private $parameter2 = 'Super2';
public function getParameter1(): string
{
return $this->parameter1;
}
public function getParameter2(): string
{
return $this->parameter2;
}
public function getParameter3(): string
{
return 'Super3';
}
}
class Sub implements CommonMethods {
public function getParameter1(): string
{
// Superクラスと同じ値を返したい
return 'Super1';
}
public function getParameter2(): string
{
// Superクラスと同じ値を返したい
return 'Super2';
}
public function getParameter3(): string
{
// Subクラス独自の値を返したい
return 'Sub--3';
}
}
/*
* Initialize()実行の戻り値は下記の通りです。
* [
* 'parameter1' => 'Super1',
* 'parameter2' => 'Super2',
* 'parameter3' => 'Super3',
* ]
*/
(new Main())->initialize(new Super());
/*
* Initialize()実行の戻り値は下記の通りです。
* [
* 'parameter1' => 'Super1',
* 'parameter2' => 'Super2',
* 'parameter3' => 'Sub--3',
* ]
*/
(new Main())->initialize(new Sub());
initialize()
を別クラスに切り出し実行時に委譲先を注入しました。
結果、 Super
と Sub
に親子関係は発生せず、それぞれの変更を柔軟に行うことが可能な実装となりました。
委譲か継承か
上記サンプルを見ると、 Super
と Sub
の親子(依存)関係が解消され、
変更が容易になったため、継承よりも委譲を使う方が良いように思えます。
しかし、委譲を使用したコードの場合は getParameter1()
などのメソッドスコープが public
となってしまい
全く無関係なコードからも呼び出しが可能となり、場合によっては変更による影響範囲が広がることが懸念されます。
結論、一概にどちらが良い、と言えるものではないと思います。
ここはまさしくオブジェクト指向のキモとなる部分であるカプセル化を意識し
- 利用者に何かを強制させたい場合、第3者から処理を隠蔽したい場合などは継承を使用することでその意図を伝える
- オブジェクトの分離に着目し、互いの依存性を下げ、変更による影響を限定的にするため委譲を使用することでその意図を伝える
といった使い分けが非常に重要と考えます。
また、同様に、「なぜ継承を使用しているのか?」「なぜ委譲を使用しているのか?」ということを意識して
他者のコードを読むことにより、設計された意図を汲み取れることが期待されます。
終わりに
今回は私が遭遇した、第3者の記述したコードを修正した経験を元に、遅延静的束縛、継承、委譲について紹介してみました。
遅延静的束縛や継承は、一般に、コードを複雑にし解読を難しくするものとして忌避される傾向にあります。
しかし、これらは適切に、また、しっかりとした意図を持って使用することにより大変威力を発揮するものです。
乱用するものではありませんが、全く使わない、というのも、また、強力な機能を使用せず大変勿体無いと思います。
強力な武器をうまく適切に扱えるよう、これからも研鑽を重ねて行きます。
最後まで読んでいただき、ありがとうございました。