こんにちは、バックエンドエンジニアの米山です。
突然ですがコンストラクタ、使っていますか?
特に、ビジネスロジックをまとめるクラスを作った時や、似たような処理を行うクラスの初期化に役立つのがコンストラクタです。
今回は、そんなコンストラクタを複数作る手法と、私なりのオススメ度も併せてご紹介しようと思います。
それではいってみましょう〜!
そもそも、「コンストラクタ」って何?
Wikipediaにはこうあります。
コンストラクタ(英: constructor)は、オブジェクト指向のプログラミング言語で新たなオブジェクトを生成する際に呼び出されて内容の初期化などを行なう関数あるいはメソッドのことである。
JavaやPHP、C#に代表されるようなオブジェクト指向言語では、クラスのインスタンスを作成する時に初期化処理を行うことができます。
この初期化処理のことを「コンストラクタ」と呼んでいます。
具体的にPHPを例に、コードで表すとこんな感じになります。
class SomeClass { // これがコンストラクタ public function __construct() { logger()->debug('初期化処理だよん'); } } class Hoge { public function fuga() { // クラスのインスタンスを作ると、初期化処理が呼ばれる // ログに「初期化処理だよん」が出力される $instance = new SomeClass(); } }
コンストラクタを複数実装する方法
それでは本題に入りましょう。
コンストラクタを複数実装し、引数の数や内容で処理を分岐させます。
いくつか方法があるので、私なりのオススメ度も併せて記載しておきます。
【オススメ度:★★★☆☆】方法その1:コンストラクタをオーバーライドさせる
似たようなことをやるためには、abstractクラスを用意し継承した先のクラスで「自分自身のコンストラクタを呼ぶ」もしくは「親クラスのコンストラクタを呼ぶ」という分岐になります。
コンストラクタだけ、オーバーライドすることができます。
【余談】オーバーロードとオーバーライド
さて、上記で説明した通りなのですが「オーバーロード」「オーバーライド」は混同しやすい用語なので、解説を挟みます。
私なりに理解した内容で説明すると
- オーバーロード:同名の、類似メソッド(または関数)の作成
- オーバーライド:メソッド(または関数)の上書き
です。オーバーロードを実際にコードで表すとこうです。
class SomeClass { // 元のメソッド public void SampleMethod() { } // オーバーロードしたメソッド(返却値が異なる) public String SampleMethod() { return "hoge"; } // これもオーバーロードしたメソッド(引数が異なる) public void SampleMethod(int value) { } }
オーバーライドは下記のようになります(同じくJavaで表現します)
class AbstractSampleBase { // 親クラスのメソッド public String SampleMethod(String value) { return "hoge" + value; } } class ChildSample extends AbstractSampleBase { // 継承先で再定義する public String SampleMethod(String value) { return "fuga"; } }
【閑話休題】結局、PHPでコンストラクタをオーバーライドすると・・・?
話を戻して、PHPでコンストラクタをオーバーライドさせると、こうなります。
abstract class SampleBase { public $output; // 親クラスのコンストラクタは引数なし public function __construct() { $this->output = 'これは親クラスのコンストラクタです'; } } class ConstructorSampleA extends SampleBase { // 子クラスのコンストラクタは引数あり public function __construct($value) { $this->output = 'これは子クラスのコンストラクタです' . $value; } }
これを、例えばControllerクラスなどで呼び出す場合、以下のようになります。
class SampleController extends Controller { public function index() { // 親クラスのコンストラクタがよばれる $data = new ConstructorSampleA(); // 下をコメントアウトすると子クラスのコンストラクタが呼ばれる // $data = new ConstructorSampleA('hello world'); return view('sample', ['output' => $data->output]); } }
【オススメ度:★★★☆☆】方法その2:コンストラクタをオーバーライドさせる(1メソッドで分岐)
方法その1と似ていますが、こちらのやり方では必ず子クラスのコンストラクタを呼び、引数の内容によって親クラスのコンストラクタを呼ぶ、というやり方です。
abstract class SampleBase { public $output; // 親クラスのコンストラクタは引数なし public function __construct() { $this->output = 'これは親クラスのコンストラクタです'; } } class ConstructorSampleB extends SampleBase { // 子クラスのコンストラクタは引数あり(デフォルトはnull) public function __construct($value = null) { if (empty($value)) { // 親クラスのコンストラクタを呼び出す parent::__cunstruct(); } else { // 引数の値があった時に子クラス側の処理を行う $this->output = 'これは子クラスのコンストラクタです' . $value; } } }
【オススメ度:★☆☆☆☆】方法その3:func_num_argsとfunc_get_argsでコンストラクタを分岐する
この方法は、可読性の観点からオススメはしません。また、PHPのバージョンにより挙動が変わる可能性があります。
func_num_args関数でコンストラクタに渡した引数の数を取得・チェックし、引数の数に応じて呼び出すコンストラクタを変える方法です。
分岐先のメソッドに渡す引数はfunc_get_args関数で取得できます。
class ConstructorSampleC { public $output; public function __construct() { $argCount = func_num_args(); $argList = func_get_args(); // 受け取った引数の数によって、呼び出すメソッドを分岐 switch($argCount) { case 1: $this->construct1($argList[0]); break; case 2: $this->construct2($argList[0], $argList[1]); break; case 3: $this->construct3($argList[0], $argList[1], $argList[2]); break; } } private function construct1($value) { $this->output = 'これは引数1つのコンストラクタです' . $value; } private function construct2($value1, $value2) { $this->output = 'これは引数2つのコンストラクタです' . $value1 . $value2; } private function construct3($value1, $value2, $value3) { $this->output = 'これは引数3つのコンストラクタです' . $value1 . $value2 . $value3; } } class SampleController extends Controller { public function index() { // 引数1つの時のコンストラクタ $data = new ConstructorSampleC('hello'); // 引数2つの時のコンストラクタ // $data = new ConstructorSampleC('hello', 'world'); // 引数3つの時のコンストラクタ // $data = new ConstructorSampleC('hello', 'world', 'hoge'); return view('sample', ['output' => $data->output]); } }
【オススメ度:★★★★☆】方法その4:interfaceを使ってクラスで分岐する
方法その1では継承を使いました。「同じような処理を行う」が「初期化処理が異なる」場合はinterfaceが有効です。
この例ではfactory関数を用意し分岐条件に応じてそれぞれの初期化を行う、別々のクラスを返すようにします。
継承を使う場合、継承元・継承先でコンストラクタの分岐を行うことで少々複雑さが増します。
主処理が同じ場合は
- 初期化を行ってから主処理を行うクラス
- 初期化を行わず、主処理を行うクラス
にクラスそのものを、分けてしまった方がわかりやすくなるケースがあります。
interface ISample { // 共通で行う主処理 public function execute(); } class ConstructorSampleD implements ISample { public function construct() { logger()->debug('引数が0の時のコンストラクタです'); } public function execute() { // クラスごとに処理を分けても良いし、共通的な処理を作って呼び出しても良い } } class ConstructorSampleE implements ISample { public function __construct($value) { logger()->debug('引数が1の時のコンストラクタです' . $value); } public function execute() { // クラスごとに処理を分けても良いし、共通的な処理を作って呼び出しても良い } } class ConstructorSampleF implements ISample { public function __construct($value1, $value2) { logger()->debug('引数が2の時のコンストラクタです' . $value1 . $value2); } public function execute() { // クラスごとに処理を分けても良いし、共通的な処理を作って呼び出しても良い } } // クラスのインスタンスを作り出すfactoryクラス class ConstructorSampleFactory { public static function createSample($mode) { if ($mode === 1) { // 引数が0の時はConstructorSampleDクラスとしてコンストラクタを実行 return new ConstructorSampleD(); } elseif ($mode === 2) { // 引数が1つの時はConstructorSampleEクラスとしてコンストラクタを実行 return new ConstructorSampleE('hoge'); } elseif ($mode === 3) { // 引数が2つの時はConstructorSampleFクラスとしてコンストラクタを実行 return new ConstructorSampleF('hoge', 'fuga'); } } }
この場合、呼び出し元では下記のようになります。
class SampleController extends Controller { public function index() { // 引数0の時のコンストラクタ $data = ConstructorSampleFactory->createSample(1); // 引数1つの時のコンストラクタ // $data = ConstructorSampleFactory->createSample(2); // 引数2つの時のコンストラクタ // $data = ConstructorSampleFactory->createSample(3); return view('sample', ['output' => $data->output]); } }
この例ではcreateSample関数の中で、直接コンストラクタに引数を渡していますが、呼び出し元(Controller)から引数を繋げたい場合は、createSample関数へ渡す引数を可変長引数リストにする等で対応可能です。
まとめ
他の言語のようにオーバーロードができないので、少し不恰好になってしまいますが
- 継承を使い、親クラスと子クラスでコンストラクタを分ける
- func_num_args関数、func_get_args関数を使って、処理パターンを分岐させる
- interfaceを使ってクラスそのものを分岐し、それぞれで初期化させる
まとめるとこの3パターンになります。
是非試してみて下さい。
Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい。