LoginSignup
2
1

More than 3 years have passed since last update.

PHPでゴッドオブジェクトを作らないためにできる3つの対策

Last updated at Posted at 2020-11-15

はじめに

この記事はLaracastsという動画学習プラットフォームの講座シリーズ「Whip Monstrous Code Into Shape」の下記エピソードをまとめたものです。
God Object Cleanup #1: Pass-Through
God Object Cleanup #2: Traits and Socks
God Object Cleanup #3: Value Objects

この講座シリーズの内容がめちゃくちゃ素晴らしくて、動画だと見返すのが面倒なので文章にまとめようと思ったのがきっかけです。すべての権利はLaracastsおよび動画作者のJeffery Wayさんに帰属します。また、以降の記述はJeffery Wayさんの主張を自分なりに解釈したもので、私個人の考えではないということにご注意ください。

ゴッドオブジェクトとは

wikipediaによると

In object-oriented programming, a God object is an object that knows too much or does too much. The God object is an example of an anti-pattern.
訳:オブジェクト指向プログラミングにおけるゴッドオブジェクトとは、多くを知りすぎていたり、多くのことをやりすぎているオブジェクトのことで、アンチパターンの一例である。

例えば1つのクラス、よくあるのがUserクラスにビジネスロジックを全部書いちゃってメソッドやプロパティが膨大に膨れ上がっているようなやつですね。

前提例

Laracastsのように、登録したユーザーが動画を視聴できる有料システムを開発中だとします。

ケース1

「よし、ユーザーがお気に入りした動画数や、視聴完了済みの動画数、視聴途中の動画数を取得できるようにしよう」

User.php
<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    .
    .
    .

    public function favoritesCount()
    {
        return 15;
    }

    public function completionsCount()
    {
        return 100;
    }

    public function experience()
    {
        return 123456;
    }
}

上記3つのメソッドで終わるなら問題ありませんが、そんなはずはなく…。ロジックをUser.phpに集約させようとすると、このクラスはすぐに膨れ上がってしまいます。

対処法1. Pass-Throughパターン

上記メソッドはすべて統計に関するものなので、新たにStatsクラスを作成し、それを返すメソッドを作ります。元々あったメソッドはStatsクラスにまとめて移しましょう。

User.php
<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    .
    .
    .
    public function stats()
    {
        return new Stats($this);
    }
}

移動したメソッド名の...Countは冗長になるので消します。

Stats.php
<?php


namespace App;


class Stats
{

    /**
     * Stats constructor.
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function favorites()
    {
        return 15;
    }

    public function completions()
    {
        return 100;
    }

    public function experience()
    {
        return 123456;
    }
}

これで$user->stats($user)->favorites() // 15というようにアクセスできるようになりました。

ケース2

「ユーザーのビデオ視聴状態に関するロジックを追加したい!」

「リレーションを取得したり、参照して視聴状態を完了、未完了にしたりするメソッドを追加しよう。」

User.php
<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    .
    .
    .

    public function completions()
    {
        // completionsリレーションを取得
    }

    public function complete()
    {
        // リレーションを参照して項目を完了とする
    }

    public function uncomplete()
    {
        // リレーションを参照して項目を未完了とする
    }

}

「あー、あとはビデオ視聴済みかどうかの判定と、動画視聴ページでスレッドを開始する処理、返信をする処理も追加しなきゃ」

User.php
    .
    .
    .
    public function completed(Video $video)
    {
        // ビデオ視聴済みか判定
    }

    public function startConversation(Conversation $conversation)
    {
        // 現在のユーザー向けに新しいスレッドを作成
    }

    public function replyTo(Conversation $conversation, Reply $reply)
    {
        // スレッド内で返信を追加
    }

早速クラスが大きくなる予感がしてきましたね。

対処法2. Traitを使う

同じようなロジックはtraitに移してみましょう。完了状態に関するロジックをまとめるため、Completable traitを作成します。

Completable.php
<?php


namespace App;


trait Completable
{
    public function completions()
    {
        // completionsリレーションを取得
    }

    public function complete()
    {
        // リレーションを参照して項目を完了とする
    }

    public function uncomplete()
    {
        // リレーションを参照して項目を未完了とする
    }

    public function completed(Video $video)
    {
        // ビデオ視聴済みか判定
    }

}

同じように、残り二つのメソッドはフォーラム関係のメソッドなのでParticipatesInForum traitを作成して移しましょう。

ParticipatesInForum.php
<?php


namespace App;


trait ParticipatesInForum
{
    public function startConversation(Conversation $conversation)
    {
        // 現在のユーザー向けに新しいスレッドを作成
    }

    public function replyTo(Conversation $conversation, Reply $reply)
    {
        // スレッド内で返信を追加
    }
}

User.phpで作成したtraitを使用しましょう。

User.php

<?php

namespace App;


use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Completable;
    use ParticipatesInForum;
    .
    .
    .

}

Userクラスがすっきりしましたね!
動画内でJefferyさんは、もしほかの場所で使われないとしてもTraitsを使うのはありだと主張しています。Traitを使って似たようなロジックをまとめることは、物が散らかった部屋でジャンル分けした箱の中に物を振り分けて片付けるようなものだと考えています。

ケース3

「収益関連の処理を追加しよう!」

「収益を出すメソッドと、数値をドル表示に整形するメソッドを追加して、と」

Performance.php
<?php


namespace App;

use Illuminate\Database\Eloquent\Model;


class Performance extends Model
{
    .
    .
    .
    public function revenue()
    {
        return $this->revenue; // 8600
    }

    public function revenueInDollars()
    {
        return $this->revenue() / 100; //86
    }

    public function revenueAsCurrency()
    {
        return money_format('$%i', $this->revenueInDollars()); // $86.00
    }

}

早速revenueに関するロジックが増えてきました。

対処法3. 値オブジェクトを使う

上記のケースでは、全てのメソッドにはrevenueという接頭辞がついており、revenueという値を操作する処理であるということが共通しています。この場合、revenueに振る舞いを持たせることは有効に作用するので、値オブジェクトを使ってみましょう。

ただし、値オブジェクトは作成する意味があるときにだけ使いましょう。DDDでなんでもかんでもプリミティブを値オブジェクトにする人がいますが、それほどばかげたことはありません。

ただしRevenueVOとかRevenueValueObjectとか命名するのはやめましょう。Revenueとするのが本来あるべき姿です。

Revenueクラスを作成したら、先ほどのメソッド(revenue()以外)をこちらに移し、メソッド名も適したものに変更します。

Revenue.php
<?php


namespace App;


class Revenue
{
    private $revenue; // Immutableにする

    /**
     * Revenue constructor.
     */
    public function __construct($revenue)
    {
        $this->revenue = $revenue;
    }

    public function inDollars()
    {
        return $this->revenue() / 100; //86
    }

    public function asCurrency()
    {
        return money_format('$%i', $this->inDollars()); // $86.00
    }

}

さあ、値オブジェクトを作ったら、どうやってアクセスすれば良いでしょうか?

$performance->revenueこんな感じでアクセスしたいわけです。

今のままだと、上記のやり方では8600を取得するだけです。正解はこうです。

Performance.php
<?php


namespace App;

use Illuminate\Database\Eloquent\Model;


class Performance extends Model
{
    public function getRevenueAttribute($revenue)
    {
        return new Revenue($revenue);
    }

}

Eloquentのカスタムアクセサ―によって、revenueプロパティにアクセスしようとしたときに、getRevenueAttribute($revenue)メソッドが定義されている場合、同メソッドを呼び出してくれます。上記の場合、Revenueインスタンスを返してくれるというわけです。

これで、$performance->revenue->asCurrency //$86.00 こんな風にアクセスすることができるようになります。

また、もしecho $performance->revenue->asCurrency とやって出力しようとすると、型が異なるのでエラーになります。文字列として出力した場合は__toString()メソッドを定義してあげましょう。そうすると、文字列としてアクセスしようとした場合の処理を定義することができます。

Performance.php
   .
   .
   .
   public function __toString()
   {
       return (string) $this->asCurrency();
   }

おわりに

以上、いかがでしたでしょうか。
Jeffery Wayさんの「Whip Monstrous Code Into Shape」では、他にもMonstrousなコードを綺麗にするためのテクニックがたくさん紹介されています。時間ができたら都度参考になった他のエピソードもまとめようと思います。

間違っているところなどありましたら是非是非ご指摘お待ちしております!

2
1
1

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
1