web-dev-qa-db-ja.com

コンストラクターが仕事をしているのではなく、質問しないでください

「コンストラクターは機能してはならない」というフレーズを参照すると、さまざまなブログ投稿に、コンストラクターを機能させないようにというアドバイスがあります。それにもかかわらず、なぜそうなのか理解に苦労しています。さらに、 this の人気のある投稿では、そのようなアドバイスを一粒の塩で行うことを提案しています。

同じ状況の2つの実装の例があります。この状況では、AFactoryにはcreateAを使用するメソッドBがあります。 Aには、Bが生成するクエリ結果が必要です。これを実装するには2つの方法があります。

例1:

class AFactory {
    public function createA(B $b): A {
        return new A($b->getQueryResult());
    }
}

class A {
    private $query_result;

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

    public function doFooWithQueryResult() {
        // Do something with query result
    }

    public function doBarWithQueryResult() {
        // Do something with query result
    }
}

最初の例では、ファクトリはクエリ結果をフェッチし、それをAのコンストラクターに渡します。 Aは、クエリ結果を対応するクラスプロパティに割り当てるだけです。ただし、ここに1つの問題があります。Aは、クエリ結果が有効なデータ構造かどうか、つまりAに適した実際のクエリ結果かどうかを検証しません。それがどこから来たのかわかりません。この検証の責任はAFactoryに漏れており、AAFactoryと非常に密接に結合しています。他の実装はこの問題を解決しますが、コンストラクタは作業を実行します。そして明らかにそれは悪いです。

例2:

class AFactory {
    public function createA(B $b): A {
        return new A($b);
    }
}

class A {
    private $query_result;

    public function __construct(B $b) {
        $this->query_result = $b->getQueryResult();
    }

    public function doFooWithQueryResult() {
        // Do something with query result
    }

    public function doBarWithQueryResult() {
        // Do something with query result
    }
}
3
user2180613

あなたが書いた

Aは、クエリ結果が有効なデータ構造かどうか、つまり、Aに適した実際のクエリ結果かどうかを確認しません。

直接続く

どこから来たのかわからない

しかし、これらは2つの異なるものです! Aは「データの送信元を知る」必要はありませんが、もちろんその入力を検証できます。

class A {
    private $query_result;

    public function __construct(array $query_result) {
        // makes tests, throws an exception if $query_result is not valid
        validateInput($query_result);  

        $this->query_result = $query_result;
    }
    // ...
}

コンストラクターでのIMHO入力検証は、別の場所で実行する必要がある「実際の作業」としてカウントされません。それにもかかわらず、コンストラクタはgetQueryResultを呼び出す必要はありません。これにより、ABから分離され、テストと再利用がはるかに容易になります。

4
Doc Brown

Aとはこれは、OOPでクラスを設計する際の基本的な質問です。

ほとんどの問題は、最初にこの質問をするのではなく、クラスの実装の詳細に焦点を当てているために発生します。あなたの例は私にこの印象を与えます。

質問に戻る:

  • 詳細を行う前に質問してください。つまり、クラスAを定義する必要があります。この質問を完了する前に実装に移らないでください。通常、コーディングを開始すると、クラスはどういうわけか一緒になると考えがちです。この誘惑を避けてください。
  • この質問をするときは、クラスに対して、多くではなく、1つの責任を定義してください。そしてこの責任は明確でなくてはならない。
  • 定義を行うと、プロパティはその定義から派生するため、このクラスがどのようなプロパティを持っているかを簡単に確認できます。たとえば、クラスがVehicleであると決定した場合、クラスにはattributes(ボディ、エンジン、タイヤ、ステアリングなど)とactions(移動、停止など)が必要です。回転、加速、減速など.
  • どんなに魅力的であろうと、コーディングに飛び込む前にこれらのステップを実行することが重要です。

さて、コンストラクタに関するあなたの質問に対しては仕事をすべきではありません:

このルールについての私の理解は(あなたが言ったように絶対的ではありませんが)これです:アプリケーションでは、クラスのオブジェクトを構築するポイントに到達するまでに、その構築のデータはすでに利用可能であり、コンストラクターに渡されているはずです。たとえば、Webアプリケーションで、フォームを介してユーザーが入力したデータを収集し、オブジェクトを構築するとします。

// Gather user information
email = email from form data
...

// Do data validation if necessary
if (name is valid)
if (email is valid) 
if (credit card is valid)
...

// Now ready to construct object
User user = new User(name, email, creditCard, ...)
...

オブジェクトの構築に使用するデータベースからデータを取得する必要がある場合も同様です(例A)。

アイデアは、オブジェクトを作成するコンテキストでは、建設前の作業をそこで行う必要があるということです。たとえば、上記の例では、Webアプリケーションのコントローラーを使用できます。

コンストラクターでより適切に実行する必要があることが1つあります。それは、論理的にはクラスの定義の一部である種類の作業です(クラスの定義に関する以前の説明)。たとえば、Webユーザーの例で、各ユーザーに信用限度に基づいてランクを割り当てることがビジネスロジックの一部である場合、この種の作業はコントローラーではなく、おそらくサービスクラスではありませんが、ビジネスオブジェクト自体。したがって、このようなことはコンストラクターで行うことができます。

0
Nazar Merza

クラスAはBが何をするかを知らないはずです。クラスAは、クエリ結果がどのように見えるかさえ知らないはずです。

改善点は、明示的な名前付きパラメーターを持つことです。つまり、クエリの結果であるかどうかにかかわらず、Aを構築するために必要なデータを正確に把握できます。

クエリ結果で実際に何が問題になるのかを考えます-クエリが間違っている場合、これは例外的な状況であり、そのようなものとして扱う必要があるため、 "A"インスタンスが作成されません。

より一般的に言えば、Doc Brownが示唆するように、それは「カップリング」を減らすこと、またはコードの一部が別の部分にどれだけ依存しているかについてです。

ここを参照 コードを結合するいくつかの異なる方法のクイックガイド。何らかの方法で回避するのは不可能であることがわかります。コードをできるだけ疎結合にして、独立してテストおよび変更できるようにする必要があります。

コンストラクターjustが名前付きパラメーターを取る場合、単純な変数を割り当てることを除いて、コンストラクターで複雑な作業を行う必要はありません。これらのパラメーターの値に到達するために行う複雑な作業は、AまたはAへの依存関係によって無視されるため、Aの単体テストは非常に簡単になります。その複雑な作業は、抽象化の背後に隠れてユニットテストを行う候補にもなります。

0
lgj