2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ROXXAdvent Calendar 2019

Day 5

PHPの型宣言とReflectionでif文消してみた

Posted at

こんにちはみなさん

以前にPHPでelseを書きたくない時に使える回避手段っていう記事を書いたことがあります。
個人的にはelseは極力消したいですが、ifはまあ、あってもいいかなって思います。
true or false で判別とかしますし。

ただ、際限なくifが増えていくとき、やたらと長いスパゲッティが出現し、我々の胃腸を痛めつけてくるのです。

今回は、際限のないifを、Laravelのイベントディスカバリの仕組みを参考にして消し飛ばそうという企画になります。

#三行で

  • 際限なく増えるif文が嫌い
  • 役割に従い、クラスを分割
  • 自動紐付けでifを消す

課題

true or false のif文であれば、大したことないです。

if ($test) {
    // some process
}

しかし、複数の値を取りうるパラメータに対する分岐が出てくると、複雑度が増します。
単純な例として、$statusの値に対して分岐処理が発生するとしましょう。

if ($status === 1) {
   //some process
}

if ($status === 2) {
   // some process
}

もしくは

switch ($status) {
    case 1:
        // some process
        break;
    case 2:
        // some process
        break;
}

このstatusがどんどん増え続けた場合、このif文やcase文がどんどん増えるということです。

if ($status === 1) {
   //some process
}

if ($status === 2) {
   // some process
}

// ....

if ($status === 101) {
   //some process
}

if ($status === 102) {
   // some process
}

こうして、スパゲッティが容易に出来上がっていくわけですな。

$statusをオブジェクトにすれば?

コードをスッキリさせるためには、$statusをオブジェクトにしてしまう方法があります。

$obj = Factory::create($status);
$obj->run();

これでどんなにステータスが増えても、行数が多くなることはありません。
なんと、これで解決してしまいましたね!

増えゆくステータスクラスの役割

ステータスによる分岐処理が全体で一つだけなら、ステータスをオブジェクト化するだけでいいでしょう。
しかし、ステータスによる分岐処理が増えたらどうでしょう。
例えば

  • ステータスの表示
  • ステータスによってユーザに通知する内容を変える
  • ステータスによって使えるサービス数が変わる
  • ステータスによって使用するDBが変わる

などなど。
これらがステータスオブジェクトにどかどかぶち込まれていくわけです。

$obj->present();
$obj->notificate();
$obj->getService();
$obj->getDB();

こうしてせっかくifを消せたのに、代わりにステータスオブジェクトはブクブク太っていきます。
なにか分岐処理が追加されるごとに、ステータスクラスを更新する必要があるため、一つのクラスの中に複数種類の異なる処理が入るので、結局このクラスは何がしたいんだってなります。

役割分担と自動紐付け

複数の毛色の違う処理が混在するクラスの中では、ある修正を加えるだけで、クラス内の他の処理に影響を与えてしまう可能性が高くなります。
新しい処理を加えるにもこのクラスの中に入れるとなれば、更にクラスが肥大化していきます。

役割分担

メンテコストを下げるのなら、各クラスごとに役割を制限し、限定された処理に集中できるようにしておくべきでしょう。処理を追加するときは、新しくクラスを作り、処理の修正をするときは、それが書かれたクラスのみを修正すればいいようにしておけば、考慮すべき部分が減らせて思考が楽になります。
先のステータスの例で言えば、こんな感じです。

  • ステータスが何なのかを定義するクラス
  • ステータスを受け取ってユーザに通知するクラス
  • ステータスを受け取って使用サービスを選択するクラス

しかし、いざ役割分担しても、どうやって紐付けするかを知らないと以下のようになります。


$obj = Factory::create($status);

if ($obj instanceof OneStatus) {
    $process = new OneStatusProcess;
}

if ($obj instanceof TwoStatus) {
    $process = new TwoStatusProcess;
}

$process->run($obj);

これはこれで大変ですね。
結局if文が復活しちゃってます。
そこで、単純には別のFactoryクラスを作って、そいつに処理クラスを作らせたり、その中で処理をさせちゃえばいいんじゃないかって考えます。

$statusObject = Factory::create($status);
ProcessFactory::run($statusObject);

とりあえず、今見ているスコープからはifが消えました。
ProcessFactoryの中が単純にif文の連続になっている可能性があるので、まだ油断できません。

if文を設定値にする

ステータスに対する処理クラスの作成や、処理の実行をFactoryに出したあとは、Factoryがどのように処理をするかに注目します。

<?php

class ProcessFactory
{
//...

public function run($object) {
    if ($obj instanceof OneStatus) {
        $process = new OneStatusProcess;
    }

    if ($obj instanceof TwoStatus) {
        $process = new TwoStatusProcess;
   }

   $process->run($obj);
}

結局今度はこのProcessFactoryというクラスのメンテナンスが大変になります。
昔elseを回避するために配列を使った方法を提示しましたが、それを流用して、少し改善させると

<?php

//...

$links = [
    OneStatus::class => OneStatusProcess::class,
    TwoStatus::class => TwoStatusProcess::class,
    //...
];

public function run($obj) {
    $class = get_class($obj);
    $process = new $this->links[$class];
    $process->run($obj);
}

設定値を用意することで、if文はなくなりました。
設定値をlinksというプロパティに出していますので、ステータスに対する処理クラスが発生したら、このプロパティを編集する必要があります。

設定値を自動生成する

設定値をいちいち作っていくと、このFactoryクラスがどんどん膨らんでいきます。
確かに設定値なので、ロジックは増えませんが、クラス自体の行数が増えていくのは精神衛生的に良くないです。
すると、この設定値を自動で生成できるようにすると、楽なのではとなるわけです。
ここでは以前に紹介したLaravelのイベントディスカバリを参考にして、設定値を自動生成する過程を解説します。

処理クラスのインターフェース

自動生成するための第一段階として、処理クラスのインターフェースを定めます。
まず、どの処理クラスもrunというメソッドを持っていることを前提としておけば上述したようにどんな処理も$process->run($obj)と書けるわけです。
ここで注目すべきは、書くステータスに対応した処理クラスであるという特徴により、そのメソッドrunの引数の型を確定できるところです。
つまり、

<?php

class OneStatusProcess
{
    public function run(OneStatus $object)
    {
        //...
    }
}

のように型宣言を書くことができます。
とりあえず、このクラスの中で、runOneStatusのインスタンスのみを引数に取れることが確定しました。

処理クラスが引数にしている型を取得する

処理クラスに型宣言ができたため、外部からこの処理クラスのrunがどのクラスのインスタンスを引数に取るかを、外部から取得できるようになります。


$ref = new \ReflectionClass(OneStatusProcess::class);// ReflectionClass
$method = $ref->getMethod('run');//                     ReflectionMethod
$param = $method->getParameters()[0];//                 ReflectionParameter
$type = $param->getType();//                            ReflectionType
$typeName = $type->getName();//                         'OneStatus'

これはReflectionと呼ばれるものたちで、クラスやメソッドの構造を取得したり、内部の動作を変更したりする仕組みのことです。これを利用して、上述の手順で引数の型を取得することができます。
このようにして引数の型が取得できると、先程のステータスに対する処理クラスの紐付けを作成できます。

$this->links[$typeNAame] = OneStatusProcess::class;

プロセスクラスの配置をルール化して、設定値を自動取得

プロセスのクラス名がわかれば、Reflectionを使って型を取得でき、その型に対してプロセスクラスを紐付ければ良いというわけです。これを実現するために、プロセスクラスの配置と名前をルール付します。

$classFiles = glob(__DIR__ . '/Process/*.php');
foreach ($classesFiles as $file) {
    include_once($file);
}

$classes = array_filter(get_declared_classes(), fn($class) => strpos($class, 'App\\Process\\') === 0);

特定のディレクトリ配下に処理クラスが定義されたファイルを配置しておき、これらをincludeします。
ついで、宣言済みクラスのリストから、特定の名前空間に属する(App\Process)クラスのリストを取得します。

まとめ

まとめると、以下のような感じになります。

public function run($obj)
{
    // ファイルの読み込み
    $classFiles = glob(__DIR__ . '/Process/*.php');
    foreach ($classesFiles as $file) {
        include_once($file);
    }

    // 処理クラスの取得
    $classes = array_filter(get_declared_classes(), fn($class) => strpos($class, 'App\\Process\\') === 0);

    // ステータスと処理クラスの紐付け
    foreach ($classes as $class) {
        $ref = new \ReflectionClass($class);
        $method = $ref->getMethod('run');
        $param = $method->getParameters()[0];
        $type = $param->getType();
        $typeName = $type->getName();
        $this->links[$typeNAame] = $class;
    }

    // 処理
    $class = get_class($obj);
    $process = new $this->links[$class];
    $process->run($obj);
}

微妙に長いですが、この処理は基本的にこれ以上膨らみません。
こうして際限ない分岐処理はなくなり、各ステータスに対応した処理クラスを作るだけでなんとかなる世界になりました。

まとめ

ちょっと長くなりましたが、型宣言とReflectionを組み合わせてif文を消してみました。
今回はコードばっかりだったので、次は絵を使ってみようかなって思いました。

今回はこんなところです。

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?