web-dev-qa-db-ja.com

Builderパターン:Directorメソッドで「参照渡し」を使用することは許容されますか?

教えるために、私はPHP Builderパターンの概念的な例の実装を作成しようとしています:

まず、いくつかの製品:

class Product1 {
    private string $attribute1;
    private string $attribute2;
    function getAttribute1(): string {
        return $this->attribute1;
    }
    function getAttribute2(): string {
        return $this->attribute2;
    }
    function setAttribute1(string $attribute1): void {
        $this->attribute1 = $attribute1;
    }
    function setAttribute2(string $attribute2): void {
        $this->attribute2 = $attribute2;
    }
}

class Product2 {
    private string $attribute1;
    private string $attribute2;
    function getAttribute1(): string {
        return $this->attribute1;
    }
    function getAttribute2(): string {
        return $this->attribute2;
    }
    function setAttribute1(string $attribute1): void {
        $this->attribute1 = $attribute1;
    }
    function setAttribute2(string $attribute2): void {
        $this->attribute2 = $attribute2;
    }
}

Builderインターフェース:

interface Builder {
    public function createNewProduct();
    public function makePart1($value);
    public function makePart2($value);
    public function getProduct();
}

具体的なビルダー:

class ConcreteBuilder1 implements Builder {
    private Product1 $product;

    function __construct() {
        $this->product = $this->createNewProduct();
    }
    public function createNewProduct() {
        return new Product1();
    }
    public function makePart1($value) {
        $this->product->setAttribute1("variation $value a1"); 
    }
    public function makePart2($value) {
        $this->product->setAttribute2("variation $value a2");
    }
    public function getProduct() {
        return $this->product;
    }

}

class ConcreteBuilder2 implements Builder {
    private Product2 $product;

    function __construct() {
        $this->product = $this->createNewProduct();
    }
    public function createNewProduct() {
        return new Product2();
    }
    public function makePart1($value) {
        $this->product->setAttribute1("variation $value b1");
    }
    public function makePart2($value) {
        $this->product->setAttribute2("variation $value b2");
    }
    public function getProduct() {
        return $this->product;
    }

}

そして、Directorは参照によってビルダーインスタンスを受け取ります。

class Director {
    public function createVariation1(Builder &$builder){
        $builder->makePart1(1);
        $builder->makePart2(2);
    }
    public function createVariation2(Builder &$builder){
        $builder->makePart1(3);
        $builder->makePart2(4);
    }
}

それを使う:

$builder = new ConcreteBuilder2();
$director = new Director();
$director->createVariation1($builder);
var_dump($builder->getProduct());

それで、このアプローチは受け入れられますか?まだ有効なGoF Builderですか?

そうでない場合、それはどのようにPHPの正しい概念ビルダーになりますか?

2
celsowm

その他のビルダーパターン

最初に、同じ名前の下にdifferentパターンがあることを簡単に説明します。JoshuaBlochのBuilderパターン(Effective Javaで説明)(抜粋 を参照) )。このパターンはGoF Builderパターンに触発されました。ただし、その目的/意図は同じではありません-別の問題を解決するため、その記事の内容にかかわらず、別のパターンです。基本的に、Bloch Builderは扱いにくいコンストラクターでオブジェクトを作成できるように設計されています。それは本質的に流暢なインターフェースを持つファクトリーです。記事では、Joshua Blochがオプションの名前付きパラメーターをシミュレートする方法として説明していますが、小さな ドメイン固有の言語 を提供する方法と見なすこともできます。

GoF Builder

それは何のため?

GoFの本は、その意図は「複雑なオブジェクトの構築をその表現から分離して、同じ構築プロセスが異なる表現を作成できるようにすること」であると述べています。進化するコードベースのコンテキストでこれを見ると、構造的にまったく異なるいくつかの表現を生成する必要があるいくつかの(おそらく自明ではない)構築ロジックがあり、これら2つ(構築ロジックと製品の種類)が進化する必要があると想定されています。独立して。それにも関わらず、is構築プロセスを一般化された方法で説明する方法があります(これも、パターンの適用を正当化するための最初の仮定です)。

2つを分離するために、抽象化が導入されています。Builderインターフェース(または抽象クラス)です。それは、すべての異なる種類の製品にわたる、建設プロセスについてのこの一般的な考え方の本質を捉えています。基本的に、これは依存関係逆転原理のアプリケーションです。

Component A        Component B
[Director]-------->[Product]

becomes

/------- Component A ------\           /-------  Component B --------\
[Director]-------->[Builder]<|---------[ConcreteBuilder]---->[Product]

これが機能するためには、コードが進化してもビルダーインターフェイスが比較的安定している必要があります。

さて、GoF Builderが実際にどれほどの頻度で登場するかはわかりません。 Bloch Builderがもっと使われているようです。いずれにしても、GoF Builderは知っておく価値があり、教えるという文脈では、抽象化について議論するために使用できます。

もう少し具体的にする必要があります

特定のポイントについてより適切に議論できるように、あなたの例をもう少し具体的にします。ただし、これはかなりの工夫がされたままです。私は通常PHPでコードを作成しないので、見落としがあった場合は自由に修正してください。ここでの私の変更は、私がしたいポイントを実証することを目的としていますが、教育目的でコードを簡略化/変更する必要があるかもしれません-その決定はあなたにお任せします。

私のバージョンでは、製品は「動物農場」です。このフォームの文字列からそれの異なる表現を構築する必要があるとしましょう(これを「ファーム文字列」と呼びます):

"Dogs: Fido, Apollo, Molly, Hunter, Daisy; Cows: Daisy, Rosie, Thor; Cats: Leo, Nala, Roxy"

これは、Directorクラスを持つ動機をよりよくするために、いくつかの重要な構築ロジックが含まれているためです。文字列自体は、データベース(またはその他のソース)から取得される場合があります。これはカスタム形式ですが、より現実的なシナリオでは、JSONなどの標準形式の場合があります。

基本的に、オブジェクトを「動物農場」の表現に再構成するために、Directorはこの文字列を解釈する必要があります。しかし、それは一般化された方法で行う必要があります-構築アルゴリズムは製品の構造から独立している必要があります。

(ちなみに、このファームは牛、犬、猫のみをサポートしています。これは意図的なものです。これらは概念的な製品(「農場」)のさまざまな「部分」を表しています。任意の種類の動物をサポートする機能は、焦点またはポイントではありません。むしろ、焦点は、一般化された構築プロセスを各製品の実装の詳細からどのように分離するかにあります)。

ここで、このシステムを開発していて、以下に示すビルダーインターフェイスを考え出したとします。その設計は、構築の問題と、(クライアントから要求される)可能性が最も高い/最も低い可能性のある変更の種類の理解を反映しています。そのため、これは、最も可能性の高い種類の将来の変更をサポートするのに十分一般的であると想定されています。つまり、インターフェイス自体を変更する必要があることはほとんどありません。代わりに、ConcreteBuilder-sやDirectorを変更することで要件を満たすことができます。

interface FarmBuilder {
    public function makeCow(string $name);
    public function makeDog(string $name);
    public function makeCat(string $name);
}

Directorは入力「ファーム文字列」を解釈し(さまざまな区切り文字を使用してそれを分割するだけです)、FarmBuilderインターフェイスを介して次のように製品を構築します。

class Director {

    private string $farmString;

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

    public function createAnimalFarm(FarmBuilder &$builder) {
        $groups = explode(";", $this->farmString);

        foreach ($groups as $groupString) {
            // Identify group (Cows, Dogs, Cats)
            $temp = explode(":", $groupString);
            $groupKey = strtolower(trim($temp[0]));
            $groupAnimalsString = $temp[1];

            // Get the individual animals in the group
            $groupAnimals = explode(",", $groupAnimalsString);
            $groupAnimals = array_map(function($str) { return trim($str); }, $groupAnimals);

            switch ($groupKey) {
                case "cows":
                    foreach ($groupAnimals as $animalName) $builder->makeCow($animalName);
                    break;
                case "dogs":
                    foreach ($groupAnimals as $animalName) $builder->makeDog($animalName);
                    break;
                case "cats":
                    foreach ($groupAnimals as $animalName) $builder->makeCat($animalName);
                    break;
            }
        }
    }
}

したがって、Directorはいくつかの構築アルゴリズムをカプセル化します。プログラマーが選択できる他のDirectorクラスが存在する可能性があることに注意してください。たとえば、JSON入力またはバイナリデータのストリームを使用するものがあるかもしれません。原則として、Directorの一部は元のコードと同じようにハードコード化できますが、これは禁止されていません。ただし、パターンによって導入された複雑さを正当化するために、構築プロセスを製品から切り離しておくという要件が実際に存在しない場合、製品自体にいくつかの静的なファクトリーメソッドを提供して、方法を提供することをお勧めします。定義済みインスタンスを作成します。

「Directorがパラメーターを指定せずにビルダーを受け取るJavaの例をいくつか見つけました。一方、Directorのコンストラクターを使用してビルダーオブジェクトを受け取る例も見ました」

ここでは、さまざまな具象ビルダーでディレクターを再利用できるようにしたかったので、パラメーターとしてビルダーをcreateAnimalFarmメソッドに渡すことを選択しました。コンストラクタを介してそれを渡すことは別のオプションです。それでもパターンは同じです-正確な実装に関しては、かなり余計な余地があります。重要なのは、さまざまな要素の役割とそれらの間の全体的な関係、そしてそれらがすべて連携してパターンの意図をサポートする方法です。 (これが、パターンの具体的なrealizationという概念がある理由です。)コンストラクターを介して「ファーム文字列」を渡すことを選択したという事実は、パターンが定義されています。

つまり、デザインパターンはすべての詳細を規定しているわけではありません。それらを実装するための "one true way"はありません。

プロダクト

製品は通常、複雑な構造を持っていますが(必ずしもそうである必要はありません)、何らかの形で他のオブジェクトで構成されている可能性があります。製品は非常に異なる可能性があり、目的が異なるため、インターフェースが異なる可能性があります。
これらを構築する必要がある製品であるとしましょう:

製品1:ファームクラスのインスタンス

class Animal { 
    public string $species; 
    public string $name; 
    function __construct($species, $name) {
        $this->species = $species;
        $this->name = $name;
    }
}

class Farm {
    private $animals = [];   // elements are Animal instances

    function __construct(array $animals) {
        foreach ($animals as $animal) {
            $this->animals[] = $animal;
        }
    }

    public function getRandomAnimal() : Animal {
            $count = count($this->animals);
            return $this->animals[Rand(0, $count - 1)];
    }

    public function petRandomAnimal() : string {
        // Produces a string of the form:
        // "We're on the farm, and we're petting Hunter the Dog! Leo the Cat is jealous."
        // (omitted...)
    }
}

これはかなり工夫されていますが、アイデアは、製品の構築および/または変更インターフェイスが、ビルダーインターフェイスと同じレベルの粒度を提供する必要がないことを示すことです。ここで、Farmクラスは不変で、1回限りのコンストラクターがあります。ただし、ビルダーの抽象化は増分構築インターフェースを提供します。この製品のビルダーを以下に示します。

class DefaultFarmBuilder implements FarmBuilder {
    private $animals = [];

    public function makeCow(string $name) {
        $this->animals[] = new Animal("Cow", $name);
    }

    public function makeDog(string $name) {
        $this->animals[] = new Animal("Dog", $name);
    }

    public function makeCat(string $name) {
        $this->animals[] = new Animal("Cat", $name);
    }

    public function getFarm(): Farm {
        return new Farm($this->animals);
    }
}

この特定のビルダーは基本的に、Farmコンストラクターのパラメーターを増分的に構築し、getFarmメソッドが呼び出されたときにのみファームオブジェクトを構築します(このビルダーの「getProduct」メソッド)。このビルダーは、実装されると常に新しいインスタンスを返しますが、それが理にかなっている場合は、毎回同じインスタンスを返すように実装を変更できます。

これで、クライアントコードがFarmオブジェクトを作成して何らかの方法で操作する必要がある場合、次のように実行できます。

// Construct a Farm using the algorithm represented by the Director class
$farmBuilder = new DefaultFarmBuilder();
$director->createAnimalFarm($farmBuilder);
$farm = $farmBuilder->getFarm();

// Use the farm object
echo $farm->petRandomAnimal();

// Output: 
// We're on the farm, and we're petting Hunter the Dog! Leo the Cat is jealous.

製品2:AnimalOwnerインスタンスの配列

class AnimalOwner {
    private string $name;
    private $animals = [];

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

    public function addAnimal(string $species, string $name) {
        $this->animals[] = $name . " (" . $species . ")";
    }

    public function toString() : string {
        // Produces a string of the form:
        // "[Owner] owns [animals]."
        // (omitted...)
    }
}

前の例とは異なり、このクラスは個々の動物を追加する方法を提供し、そのビルダーはそれを利用できます。ただし、これらの配列を作成するには、いくつかの特別な目的のデータ(所有者名、所有権関係)の処理が必要です。つまり、これはこの使用法に固有のものであり(他のタイプの製品は所有権データを必要としない)、抽象ビルダーインターフェースはサポートを提供しません。これは具象ビルダーによって処理されます。

class OwnerArrayBuilder implements FarmBuilder {
    private $ownershipMap = [];
    private $owners = [];
    private int $animalIndex = -1;

    function __construct(array $ownershipData) {
        // $ownershipData example: 
        // Given: "Dogs: Fido, Molly; Cats: Nala"  (F, M, N)
        // $ownershipData value of                           F  M  N
        // ["Owners" => ["Alice", "Bob"], "OwnershipMap" => [0, 1, 0]]
        // means that Fido and Nala belong to Alice (the owner at index 0), 
        // and that Molly belongs to Bob (the owner at index 1)

        $owners = $ownershipData["Owners"];
        foreach($owners as $owner) {
            $this->owners[] = new AnimalOwner($owner);
        }

        $this->ownershipMap = $ownershipData["OwnershipMap"];
    }

    public function makeCow(string $name) {
        $this->animalIndex++;
        $ownerIndex = $this->ownershipMap[$this->animalIndex];
        $this->owners[$ownerIndex]->addAnimal("Cow", $name);
    }

    public function makeDog(string $name) {
        $this->animalIndex++;
        $ownerIndex = $this->ownershipMap[$this->animalIndex];
        $this->owners[$ownerIndex]->addAnimal("Dog", $name);
    }

    public function makeCat(string $name) {
        $this->animalIndex++;
        $ownerIndex = $this->ownershipMap[$this->animalIndex];
        $this->owners[$ownerIndex]->addAnimal("Cat", $name);
    }

    public function getOwners(): array {
        return $this->owners;
    }
}

したがって、ここでは、ビルダーに追加データが提供されています(おそらく「ファーム文字列」と同じデータベースから取得されたものです)。この場合、結果の配列とAnimalOwnerオブジェクトがすぐに作成され、各ビルダー関数(makeCow、makeDog、makeCat)がそれらのインスタンスでaddAnimal()関数を呼び出します。ここではgetOwnersと呼ばれる「getProduct」関数は、完全に異なる型を返します。これが、抽象ビルダーインターフェイスの一部ではない理由です-製品は統合されていません。とにかくクライアントコードは具象ビルダーを作成するので、それを使用して製品を取得できます。

// Ownership data imported from somewhere:
$ownershipData = ["Owners" => ["Alice", "Bob"], "OwnershipMap" => [0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1]];

// Construct the AnimalOwner array using the algorithm represented by the Director class
$ownerArrayBuilder = new OwnerArrayBuilder($ownershipData);
$director->createAnimalFarm($ownerArrayBuilder);
$owners = $ownerArrayBuilder->getOwners();

// use the owners array:
$ownerStrings = array_map(function($owner) {return $owner->toString();}, $owners);
echo join("\n", $ownerStrings);

// Output: 
// Alice owns Fido (Dog), Apollo (Dog), Molly (Dog), Hunter (Dog), Daisy (Cow) and Leo (Cat).
// Bob owns Daisy (Dog), Rosie (Cow), Thor (Cow), Nala (Cat) and Roxy (Cat).

製品3:農場の動物をまとめたHTMLテーブル

この製品は、カスタムのユーザー定義クラスではありません。代わりに、作成する必要があるのは、次の形式のHTMLテーブルを表すDOMDocumentです。

+------+----------+
| Cows | #numCows |
+------+----------+
| Dogs | #numDogs |
+------+----------+
| Cats | #numCats |
+------+----------+

この場合、ビルダーは基本的に、一般的なDOMDocument/DOMElement APIに対して特別な目的の構築メカニズムを提供します。

class HtmlSummaryTableBuilder implements FarmBuilder {
    private DOMDocument $dom;
    private $countCells = [];
    private $groups = ['Cows', 'Dogs', 'Cats'];

    function __construct() {

        $this->dom = new DOMDocument();
        $table = $this->dom->createElement('table');

        foreach ($this->groups as $group) {
            $tableRow = $this->dom->createElement('tr');
            $tableCell_Group = $this->dom->createElement('td', $group);
            $tableCell_Count = $this->dom->createElement('td');

            $this->countCells[$group] = ["Cell" => $tableCell_Count, "Count" => 0];

            $tableRow->appendChild($tableCell_Group);
            $tableRow->appendChild($tableCell_Count);
            $table->appendChild($tableRow);
        }

        $this->dom->appendChild($table);
    }

    public function makeCow(string $name) {
        $this->countCells["Cows"]["Count"]++;
    }

    public function makeDog(string $name) {
        $this->countCells["Dogs"]["Count"]++;
    }

    public function makeCat(string $name) {
        $this->countCells["Cats"]["Count"]++;
    }

    public function getTable(): DOMDocument {
        foreach ($this->groups as $group) {
            $this->countCells[$group]["Cell"]->appendChild(
                $this->dom->createTextNode(strval($this->countCells[$group]["Count"])));
        }
        return $this->dom;
    }

}

余談:テーブルに数のみが含まれていると、ビルダーは動物の名前を受け取っても無視します。アイデアは、デザイナーがビルダーインターフェイスに名前のサポートを含める理由があったということです。名前はほとんどの製品タイプで使用されているようで、おそらくこれまでのプロジェクトでの経験に基づいて、仮想の開発者が将来の変更でも一般に名前を使用すると予想しています。

クライアントコードは、次のようにDOMDocumentを作成できます。

$tableBuilder = new HtmlSummaryTableBuilder();
$director->createAnimalFarm($tableBuilder);

// obtain and use the table
echo $tableBuilder->getTable()->saveHTML();
// Output:
<table>
  <tr><td>Cows</td><td>3</td></tr>
  <tr><td>Dogs</td><td>5</td></tr>
  <tr><td>Cats</td><td>3</td></tr>
</table>

概要

うまくいけば、これは具体的なビルダーがすべて同じ形式に従う必要がないことを示すのに役立ちました。彼らは非常に異なることを行うことができます。ここでの重要な抽象化は、一般化された構築メカニズムです。 Directorと具象Builderはこれに依存しているため、これらは分離されています。このインターフェースが、変化に直面しても安定している一方で、さまざまな製品の構築をサポートするのに十分な表現力がある限りです。一般的に、さまざまな具象ビルダーは、さまざまなモジュールから取得でき、独立して(さまざまな個人/チームによって)開発できます。構築アルゴリズムと製品ビルダーはどちらも独立して変更できます。抽象ビルダーインターフェイスを利用できる限り、異なるDirectorを定義できます。たとえば、より優れた構築アルゴリズム、異なる入力形式をサポートするため、または事前定義された「プリセット」オブジェクトを提供するために、新しいDirectorを追加できます。

追伸 PHPサンドボックス の例全体を示します。

4

あなたのPHPのビルダーパターンの適切な方法ではありません。 PHPでのビルダーパターンの例を次に示します。

<?php

namespace RefactoringGuru\Builder\Conceptual;

/**
 * The Builder interface specifies methods for creating the different parts of
  * the Product objects.
 */

interface Builder
{
     public function producePartA(): void;

     public function producePartB(): void;

     public function producePartC(): void;
}

/**
 * The Concrete Builder classes follow the Builder interface and provide
 * specific implementations of the building steps. Your program may have several
 * variations of Builders, implemented differently.
 */

 class ConcreteBuilder1 implements Builder
{
    private $product;

    /**
     * A fresh builder instance should contain a blank product object, which is
     * used in further Assembly.
     */
     public function __construct()
    {
        $this->reset();
    }

     public function reset(): void
    {
        $this->product = new Product1;
    }

    /**
     * All production steps work with the same product instance.
     */
    public function producePartA(): void
    {
        $this->product->parts[] = "PartA1";
    }

    public function producePartB(): void
    {
        $this->product->parts[] = "PartB1";
    }

    public function producePartC(): void
    {
        $this->product->parts[] = "PartC1";
    }

    /**
     * Concrete Builders are supposed to provide their own methods for
     * retrieving results. That's because various types of builders may create
     * entirely different products that don't follow the same interface.
     * Therefore, such methods cannot be declared in the base Builder interface
     * (at least in a statically typed programming language). Note that PHP is a
     * dynamically typed language and this method CAN be in the base interface.
     * However, we won't declare it there for the sake of clarity.
     *
     * Usually, after returning the end result to the client, a builder instance
     * is expected to be ready to start producing another product. That's why
     * it's a usual practice to call the reset method at the end of the
     * `getProduct` method body. However, this behavior is not mandatory, and
     * you can make your builders wait for an explicit reset call from the
     * client code before disposing of the previous result.
     */
     public function getProduct(): Product1
    {
        $result = $this->product;
        $this->reset();

        return $result;
    }
}

/**
 * It makes sense to use the Builder pattern only when your products are quite
 * complex and require extensive configuration.
 *
 * Unlike in other creational patterns, different concrete builders can produce
 * unrelated products. In other words, results of various builders may not
 * always follow the same interface.
 */
 class Product1
{
     public $parts = [];

    public function listParts(): void
    {
        echo "Product parts: " . implode(', ', $this->parts) . "\n\n";
    }
}

/**
 * The Director is only responsible for executing the building steps in a
 * particular sequence. It is helpful when producing products according to a
 * specific order or configuration. Strictly speaking, the Director class is
 * optional, since the client can control builders directly.
 */
 class Director
{
    /**
     * @var Builder
     */
     private $builder;

    /**
     * The Director works with any builder instance that the client code passes
     * to it. This way, the client code may alter the final type of the newly
     * assembled product.
     */
    public function setBuilder(Builder $builder): void
    {
        $this->builder = $builder;
    }

    /**
     * The Director can construct several product variations using the same
     * building steps.
     */
    public function buildMinimalViableProduct(): void
    {
        $this->builder->producePartA();
    }

    public function buildFullFeaturedProduct(): void
    {
        $this->builder->producePartA();
        $this->builder->producePartB();
        $this->builder->producePartC();
    }
}

/**
 * The client code creates a builder object, passes it to the director and then
 * initiates the construction process. The end result is retrieved from the
 * builder object.
 */
 function clientCode(Director $director)
{
     $builder = new ConcreteBuilder1;
    $director->setBuilder($builder);

     echo "Standard basic product:\n";
    $director->buildMinimalViableProduct();
     $builder->getProduct()->listParts();

    echo "Standard full featured product:\n";
    $director->buildFullFeaturedProduct();
    $builder->getProduct()->listParts();

    // Remember, the Builder pattern can be used without a Director class.
    echo "Custom product:\n";
    $builder->producePartA();
    $builder->producePartC();
    $builder->getProduct()->listParts();
}

 $director = new Director;
 clientCode($director);

リファレンス: https://refactoring.guru/design-patterns/builder/php/example

0