Laravelの長所について

前回Laravelの最低限の環境構築をしたので今回もLaravelについて書こうと思います。

前の会社でもLaravelはほんの少しだけ使ったのですが、メインで使っていたフレームワークCakePHPでした。

Laravelについて社内に詳しい人がいなかったこともあり、イマイチその良さを感じられませんでした。

が、最近ようやく少しずつ実感できてきたのでその辺りをまとめてみたいと思います。

依存性の注入が簡単にできる

結論から言うとこれがLaravelの一番の長所のように思います。

クラスAがクラスBを読み込む時、クラスAはクラスBに依存していると表現します。これはBに変更があればAの処理内容にも影響が及ぶことを意味します。

その為、できる限り特定のクラスやモジュールに依存しないように設計することが望ましいと考えられます。上記の例でいうと、クラスBを変更してもクラスAへの影響が無い、またはできる限り少なくすることが理想です。

それを実現する為にはどうすれば良いかと言うと、必要以上の知識を持たせないようにすることが重要になります。つまりクラス同士の関係はできるだけ抽象的にし、実際の処理や必要な知識については外部から注入してあげることで依存度を下げることができます。これが 依存性の注入 (Dependency Injection / DI)と呼ばれるものです。

Laravelはサービスコンテナという仕組みを使ってこの依存性の注入が簡単に実現できます。例えば、

class Hoge
{
    /**
     * @var Fuga
     */
    private $fuga;

    /**
     * @param Fuga $fuga
     */
    public function __construct(Fuga $fuga)
    {
        $this->fuga = $fuga;
    }
}

とすることでHogeクラスにFugaクラスを注入することができます。タイプヒントのみで自動的に依存解決してくれます。

こうすると仮に違うクラスやモジュールを使うことになっても簡単に交換可能になります。HogeクラスでFugaクラスの知識を持っている必要はありません。

更に抽象に依存させることでLaravelの恩恵をより受けることができます。

interface HogeInterface
{
    /**
     * @param FugaInterface $fuga
     * @return Piyo
     */
    public function hogehoge(FugaInterface $fuga): Piyo;
}

interfaceを注入することで依存度はグッと下がります。実際の処理内容は↓のように実装クラスに書くことで注入される他のclassやinterfaceについての知識を持つ必要が無くなります。

class Hoge implements HogeInterface
{
    /**
     * @var PiyoRepositoryInterface
     */
    private PiyoRepositoryInterface $piyo;

    /**
     * @param PiyoRepositoryInterface $piyo
     */
    public function __construct(PiyoRepositoryInterface $piyo)
    {
        $this->piyo = $piyo;
    }

    /**
     * @param FugaInterface $fuga
     * @return Piyo
     */
    public function hogehoge(FugaInterface $fuga): Piyo
    {
        return $this->piyo->piyopiyo($fuga);
    }
}

ここではPiyoRepositoryInterfaceというリポジトリを使ってデータを取得するんだなということはわかりますが、どんな方法でデータを取ってくるのかについての知識はありません。DBから取ってくるのか、外部APIから取ってくるのか等はPiyoRepositoryInterfaceに結合される実装クラスによって変わります。が、それをHoge及びHogeInterfaceが知っている必要はありません。

同様に引数のFugaInterfaceもどのような型の値が渡ってくるかはFugaInterfaceに結合されるクラスに依ります。引数を修正する必要が出た場合でもFugaInterfaceに結合するクラスを変えるだけでOKです。

また、interfaceに実装クラスを結合するにはServiceProviderを使います。

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(HogeInterface::class, Hoge::class);
    }
}

こうすることでHogeInterfaceを呼び出した時に自動的にHogeを結合してくれます。

このように、依存性の注入が簡単に実現できるところがLaravelの最大の長所だと思います。

単体テストが書きやすい

上記の依存性の注入に伴うメリットの1つとして単体テストが書きやすいということが挙げられます。

抽象に依存させ、しっかりと責務を分離した状態を維持できればそれぞれのクラスが持つ責務についてのみテストを行うことができるからです。

そうすることで各クラスの質を担保することができ、バグを減らすことにも繋がります。

class HogeTest extends \Tests\TestCase
{
    /**
     * @return HogeInterface
     */
    public function test__construct(): HogeInterface
    {
        $hoge = $this->app->make(HogeInterface::class);
        $this->assertInstanceOf(Hoge::class, $hoge);
        return $hoge;
    }

    /**
     * @depends test__construct
     * @param HogeInterface $hoge
     * @return void
     */
    public function testHogehoge(HogeInterface $hoge): void
    {
        $fuga = new FugaInterface('fugafuga');
        $result = $hoge->hogehoge($fuga);
        $this->assertInstanceOf(Piyo::class, $result);
    }
}

こんな感じでテストが書けます。僕は実務で何かクラスを実装するときはまずinterfaceを作り、対応するclassのガワを作った時点で単体テストを作成することを意識しています。ガワさえ作れば上記でいうtest__constructの部分は書けるからです。この辺はTDD(テスト駆動開発)に関わるところなので改めて記事にできればと思います。

まとめ

以上、Laravelの最大の長所とも言える依存性の注入について書いてみました。

まだ知識が浅いので間違っている箇所があればご指摘いただけると幸いです。

冒頭で書いた通り前の会社ではCakePHPを使っており、その時はこうした概念すら知らなかったのですが、設計を考える際に非常に強力な武器になるなと実感しています。

引き続き実践を重ねながら少しでも良い設計・実装ができるように努めていきたいと思います。