23
Help us understand the problem. What are the problem?

posted at

updated at

PHPで読むEffective Java

はじめに

Javaプログラマーではないですが、このごろ良いコードを書くための勉強としてEffective Javaを読んでいます。
あやふやな理解のまま読み進めて「とりあえず読み終えた」という状態になりたくなかったので、普段使っているPHPで項目ごとにサンプルコードを書きました。

もし間違っている点や、不十分な点がありましたらご指摘いただきたいです。

※本記事は、Effective Javaの解説を目的にしたわけではありませんのでその点はご容赦ください。

第2章:オブジェクトの生成と消滅

項目1:コンストラクタの代わりにstaticファクトリメソッドを検討する

要約

あるクラスのインスタンスを作成する時に単にnew クラス名とするのではなく、クラスのインスタンスを返すstaticファクトリメソッドを使用しましょうということ。

メリット

  • 一つのメソッドとして名前を持てるので表現力が増す
  • 必ずしも新しいインスタンスを返さなくて良い
  • 任意のサブタイプのインスタンスを返せる

PHPで書いてみる

value object的なプリミティブなstring型をラップしただけのUserNameクラス。

UserName.php
class UserName
{
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public static function ValueOf(string $name): self
    {
        return new static($name);
    }
}

$userName1 = new UserName("qiita1");
$userName2 = UserName::ValueOf("qiita2");

いいサンプルコードが思いつかなかったけど、new UserName()よりUserName::ValueOfのほうが表現力増してることは伝わると思う。

staticファクトリメソッドを使えばクラスをシングルトンにしたりできる。
メリット2つ目の「必ずしも新しいインスタンスを返さなくて良い」とはこういうことを意味している。
laravelのDI Containerのコードを抜粋して見てみる。

Container.php
class Container implements ArrayAccess, ContainerContract
{
    /**
     * The current globally available container (if any).
     *
     * @var static
     */
    protected static $instance;

    // ============= 省略 ============== //

    /**
     * Get the globally available instance of the container.
     *
     * @return static
     */
    public static function getInstance()
    {
        if (is_null(static::$instance)) { // インスタンスが存在するかどうかチェック
            static::$instance = new static;
        }

        return static::$instance;
    }
}

ここではクラスのインスタンスをstaticな$instanceに格納し、staticファクトリメソッドで最初に$instanceにインスタンスが格納されているかチェックすることでシングルトンを実現している。

項目2:多くのコンストラクタパラメータに直面したときにはビルダーを検討する

要約

staticファクトリメソッドとコンストラクタはどちらも多くのオプションパラメータに対してはうまく対応できない。
普通にphpで多くのオプションパラメータを持つクラスのインスタンスを初期化しようと思ったら下記のようになる。(phpでは引数にデフォルトのパラメータを指定できるが、javaではそれができないためテレスコーピングコンストラクタという手法を使う。)

PHPで書いてみる

普通にコンストラクタで

class Sample
{
    private int $a; // 必須
    private int $b; // 必須
    private int $c; // オプション
    private int $d; // オプション
    private int $e; // オプション
    private int $f; // オプション

    public function __construct($a, $b, $c = 0, $d = 0, $e = 0, $f = 0)
    {
        $this->a = $a;
        $this->b = $b;
        $this->c = $c;
        $this->d = $d;
        $this->e = $e;
        $this->f = $f;
    }
}

$sample = new Sample(1, 1, 0, 0, 3, 3);

このコードだと、このクラスを利用するクライアント側で「コンストラクタのそれぞれの引数が何を意味するのか」に意識を割かないといけない。
さらに上のコードの場合、$c$dはデフォルトの0でいいのに$e$fを指定したいためにわざわざ指定しないといけないというのも良くない。

ビルダーパターン

class Sample
{
    private int $a; // 必須
    private int $b; // 必須
    private int $c; // オプション
    private int $d; // オプション
    private int $e; // オプション
    private int $f; // オプション

    public function __construct(SampleBuilder $sampleBuilder)
    {
        $this->a = $sampleBuilder->a;
        $this->b = $sampleBuilder->b;
        $this->c = $sampleBuilder->c;
        $this->d = $sampleBuilder->d;
        $this->e = $sampleBuilder->e;
        $this->f = $sampleBuilder->f;
    }
}

class SampleBuilder
{
    // 必須
    public int $a;
    public int $b;

    // オプション
    public int $c = 0;
    public int $d = 0;
    public int $e = 0;
    public int $f = 0;

    // 必須パラメータはコンストラクタで設定
    public function __construct(int $a, int $b)
    {
        $this->a = $a;
        $this->b = $b;
    }

    // オプションパラメータを設定
    public function c(int $c): self
    {
        $this->c = $c;
        return $this;
    }

    public function d(int $d): self
    {
        $this->d = $d;
        return $this;
    }

    public function e(int $e): self
    {
        $this->e = $e;
        return $this;
    }

    public function f(int $f): self
    {
        $this->f = $f;
        return $this;
    }

    public function build(): Sample
    {
        return new Sample($this);
    }
}

$sampleBuilder = new SampleBuilder(1, 2);
$sample = $sampleBuilder->c(1)->d(1)->f(3)->build();

クラスを利用するのも読むのも楽になった。
デフォルトの値でいいやつ(上の例だと$e)はわざわざ設定する必要もなくなった。
もうちょっといい書き方はありそう。
(Javaだと内部クラスを使ってもうちょっとかっこよく書ける。)

項目3:privateのコンストラクタかenum型でシングルトン特性を強制する

要約

Javaでシングルトンを強制する場合、下記のどちらかを使う。
- privateなコンストラクタを用意して、staticファクトリメソッドだけでインスタンスを作成できるようにする
- 単一要素を持つenum型を宣言する

2つ目の方法がおすすめらしい。

単一要素のenum型は、たいていシングルトンを実装する最善の方法です。

PHPで書いてみる

PHPには組み込み型としてenum型が用意されていないので1つ目の方法を使う。
さっきも使ったlaravelのコード。

Container.php
class Container implements ArrayAccess, ContainerContract
{
    /**
     * The current globally available container (if any).
     *
     * @var static
     */
    protected static $instance;

    // ============= 省略 ============== //

    /**
     * Get the globally available instance of the container.
     *
     * @return static
     */
    public static function getInstance()
    {
        if (is_null(static::$instance)) { // インスタンスが存在するかどうかチェック
            static::$instance = new static;
        }

        return static::$instance;
    }
}

このままだとやろうと思えばクラスの外部からnew Container()ができてしまうので強制できているとは言えない。明示的にprivateなコンストラクタを定義することでシングルトンを強制できる。

Container.php
class Container implements ArrayAccess, ContainerContract
{
    private function __construct(){} // 追加

    /**
     * The current globally available container (if any).
     *
     * @var static
     */
    protected static $instance;

    // ============= 省略 ============== //

    /**
     * Get the globally available instance of the container.
     *
     * @return static
     */
    public static function getInstance()
    {
        if (is_null(static::$instance)) { // インスタンスが存在するかどうかチェック
            static::$instance = new static;
        }

        return static::$instance;
    }
}

これでもうnew Container()はできなくなり、インスタンス化にはstaticファクトリメソッドを使うしかなくなった。

項目4:privateのコンストラクタでインスタンス化不可能を強制する

要約

staticメソッドとstaticプロパティだけからなるクラスは悪用されがちだが、基本データ型に対する操作をまとめたクラスなどのようにうまく使うこともできる。
このようなクラスの場合、インスタンスには意味がないのでクラスのユーザーに誤解を与えないためにもインスタンス化不可能にするのが良い。

PHPで書いてみる

前項の最後のサンプルコードでprivateなコンストラクタを定義しているのでここでは省略。

自分が見たlaravelのDIコンテナのコードも、FuelPHPのStrクラスのコードも、privateなコンストラクタを定義しているものはなかった。
インスタンス化目的でないことが明らかな場合はあえて定義しないのが普通なのかもしれない。(あくまで自分の予想。)
プロジェクト内で自分でユーティリティクラスみたいなのを定義するときは、他のメンバーがわかりやすいようにprivateなコンストラクタを定義したほうがいいのかも。

項目5:資源を直接結びつけるより依存性注入を選ぶ

要約

オブジェクトの内部で別のオブジェクトを生成するのではなくて、インスタンス化したオブジェクトを外部から注入しましょう。
DIの詳しい解説は省略。

PHPで書いてみる

// 悪い例
class A
{
    private $b;

    public function __construct()
    {
        $this->b = new B(); // クラスの内部でBクラスをインスタンス化している
    }

    // 何かしらのメソッド
}

class B
{
    // 何かしらのメソッド
}

// Aクラスを使う
$a = new A();

// 良い例
class C
{
    private $d;

    public function __construct($d)
    {
        $this->d = $d;
    }

    // 何かしらのメソッド
}

class D
{
    // 何かしらのメソッド
}

// Cクラスを使う
// クラスDをクラスCの外部でインスタンス化して注入する
$c = new C(new D());

この方法はコンストラクタで依存注入しているのでコンストラクタインジェクションと呼ばれる。
他にセッターインジェクションやインターフェースインジェクションがある。

項目6:不必要なオブジェクトの生成を避ける

要約

オブジェクトの生成コストは高いので繰り返し使う不変なオブジェクトや、可変だけど変更されないオブジェクトは再利用するほうが良い。
※オブジェクトの生成コストはオブジェクトによって違うので一概には言えない。
自動ボクシングに気をつける。(PHPでは気にする必要はなさそう)

PHPで書いてみる

2020-03-01と2021-03-01の差を求めるという全く意味のないコードを100万回繰り返す。

毎回オブジェクトを生成する場合

class CompareTo20200301 // 2020年3月1日との差分を計算するクラス
{
    public static function diff(DateTimeImmutable $arg)
    {
        // 毎回DateTimeImuutableインスタンスが生成される
        $target = new DateTimeImmutable("2020-03-01 12:00 Asia/Tokyo");
        return $target->diff($arg);
    }

}


$datetime = new DateTimeImmutable("2021-03-01 12:00 Asia/Tokyo");

$time_start = microtime(true);

for ($i = 0; $i < 1000000; $i++) {
    CompareTo20200301::diff($datetime);
}

$time = microtime(true) - $time_start;
echo "{$time} 秒"; // => 9.305095911026 秒

一回しかオブジェクトを生成しない場合

class CompareTo20200301
{
    private static $target;

    public static function diff(DateTimeImmutable $arg)
    {
        if (is_null(static::$target)) { // 一度インスタンスを生成して、二回目からは同じものを使い回す。
            static::$target = new DateTimeImmutable("2020-03-01 12:00 Asia/Tokyo");
        }
        return static::$target->diff($arg);
    }

}


$datetime = new DateTimeImmutable("2021-03-01 12:00 Asia/Tokyo");

$time_start = microtime(true);

for ($i = 0; $i < 1000000; $i++) {
    CompareDatetime::diff($datetime);
}

$time = microtime(true) - $time_start;
echo "{$time} 秒"; // => 0.900290966033 秒

100万回繰り返したときこのような結果になった。
毎回インスタンスを生成する場合   : 9.305095911026 秒
一度だけインスタンスを生成する場合 : 0.900290966033 秒

今回は10倍程度の差にしかならなかったが、オブジェクトの生成コストが高い場合と、繰り返しの回数が多い場合は気をつけたほうが良さそう。

項目7:使われなくなったオブジェクト参照を取り除く

要約

JavaやPHPのようにガベージコレクションを備えている言語でもオブジェクト参照を意図せず保持してしまい、それがメモリリークの原因になることがある。

意図せずにオブジェクトを参照保持してしまう可能性がある場合として本書では以下の3つが挙げられている。

  • クラスが独自にメモリ管理しているとき
  • オブジェクト参照をキャッシュに入れたとき
  • リスナーやコールバック

下の2つはJavaではWeakHashMapを使って弱い参照を保持することで解決できるようである。
PHPでも7.4から導入されたWeakReferenceクラスをで解決できそう。
WeakReferenceの解説はこの記事がわかりやすかった。
PHP・GCの話-8話)GC 関連機能紹介2(Weak Reference, Weak Map)(END)

PHPで書いてみる

簡単なStackの実装。
Stackクラスが独自にメモリ管理しているので、使われなくなった参照にはnullを指定する必要がある。

class Stack
{
    private array $elements;
    private int $size = 0;

    public function __construct()
    {
        $this->elements += new stdClass();
    }

    public function push(stdClass $e): void
    {
        $this->elements[$this->size++] = $e;
    }

    public function pop(stdClass $e): stdClass
    {
        if ($this->size = 0) {
            throw new Exception("スタックが空です");
        }

        $result = $this->elements[$this->size--];
        $this->elements[$this->size] = null; // 使われなくなった参照を取り除く

        return $result;
    }
}

スタックが一度大きくなってその後小さくなった場合、$this->elements[$this->size] = null;この1行がないと、スタックから取り出されたオブジェクトはガベージコレクトされない。

項目8:ファイナライザとクリーナーを避ける

要約

ファイナライザやクリーナーは実行のタイミングをプログラマがコントロールできない上に、実行される保証もないので以下のような処理をファイナライザやクリーナーで実行してはいけない。

  • 時間的な制約があること
  • 永続的な状態の更新

ファイナライザやクリーナーの有効な使い方

  • AutoClosableなオブジェクトで、クライアントがclose()を呼び出してリソースを開放し忘れた時の保険として、close()を呼ぶクリーナーを実装するという使い方
  • ネイティブピアがパフォーマンス上重要なリソースを持っている時にそれを回収するために使う。

PHPで書いてみる

PHPにファイナライザは存在しないので省略。

余談

PHPのオブジェクトは参照がなくなった時点でデストラクタが確実に走り、リソースが開放される。
PHPとJavaではガベージコレクションの仕組みが違い、それぞれの仕組みにメリットがある。
PHPのようなタイプは参照の数をカウントしてそれが0になったタイミングでメモリを開放するので、参照がなくなれば確実に開放されるが、循環参照がある場合には開放されないなどがデメリット。
ガベージコレクションの仕組みの違いはこちらの記事を参照。
ファイナライザには頼らない

項目9:try-finallyよりもtry-with-resourcesを選ぶ

要約

リソースの開放をしなければならないオブジェクトがある時に、クラスを使う人はリソースの開放を忘れやすい。項目8で述べたようにファイナライザをセーフティーネットに使う方法もあるがうまく働く確証はない。
try-finallyは歴史的にはリソースの開放を保証する最善の方法だったが次のようなデメリットがある。

  • 開放する必要があるリソースが増えた時に可読性が落ちる。
  • finallyブロックでも例外が発生する可能性があり、同時にtryブロックで例外が発生していても、tryブロックでの例外は覆い隠されてしまい、stack traceに記録されない。

このような問題をtry-with-resourceでは解決している。

PHPで書いてみる

PHPにはないので省略。

最後に

サンプルコードを書くために色々調べる過程でPHPとJavaの違いなども勉強でき、思ったよりも収穫は大きかったように思います。
今後も読み進めながら3章以降も書いていきます。

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
23
Help us understand the problem. What are the problem?