xoubaman/builder

一个可复用的构建器库

2.0.2 2020-10-07 19:57 UTC

This package is auto-updated.

Last update: 2024-09-08 05:06:58 UTC


README

一个用于简化构建器实现的PHP库。

构建器模式是一种设计模式,其中一个类知道如何构建另一个类。使用构建器提供了一些好处

  • 为对象实例化添加语义
  • 在实例化大对象时减少杂乱
  • 减少了类实例化的地方,减少了重构类构造函数时需要更改的地方

这里有篇文章更详细地阐述了这些好处。

安装

使用composer在你的项目中引入依赖

composer require --dev xoubaman/builder

测试是使用构建器最常见的地方。如果你打算在测试之外使用库,不要包含--dev标志。

使用构建器

构建器在属性$base中定义了一个“基本”设置,该设置将被用作每次构建的起点。这个初始设置可以被修改来构建自定义实例。

一旦准备好构建器,它将提供以下公共方法

  • build()将使用当前的数据设置来进行构建。一旦调用,设置将被清除,下一次调用将使用默认值。
  • cloneLast()将返回与上次调用build()时相同的构建。这对于获取相同设置的多个构建非常有用。
  • repeatLastSetup()将当前设置更改为与上次构建相同的设置。这对于避免重复使用具有微小差异的大设置非常有用。

使用CLI生成器创建构建器

由于构建器的样板代码是重复且不吸引人的,所以提供了一个生成器脚本来自动为提供的类创建构建器。

该脚本需要一个类的全限定名称作为参数

vendor/bin/builder build "Game\MetalSlugCharacter"

将创建Game\MetalSlugCharacterBuilder

可以使用自定义模板而不是提供的模板来创建构建器。为此,必须传递一个转换器作为参数

vendor/bin/builder build "Game\MetalSlugCharacter" "Path\To\My\Converter"

转换器是一个实现了Converter接口的类。它将由脚本通过传递Xoubaman\Builder\Generator\ClassMetadata\ClassMetadata作为参数来调用。

手动创建构建器

  • 创建一个扩展Xoubaman\Builder\Builder的构建器。
  • 声明以下任何常量以适应构建器需求
    • CLASS_TO_BUILD:要实例化的类的FQN。省略或设置为空,则构建器将返回一个数组。
    • USE_CONSTRUCTOR:在实例化类时调用的构造函数。省略或设置为空以使用常规构造函数。
    • AFTER_BUILD_CALL:在构建新实例后立即调用的方法。它是一个数组,其中第一个元素是方法名,其余的将用作调用参数。
  • $base属性中定义用于构建的默认值,该属性是一个格式为fieldName => value的数组。这可能是一个好地方,例如在构建器构造函数中。该属性是一个数组,它必须具有与用于实例化类的构造函数相同数量的元素和相同的顺序。
  • 添加方法以方便地修改构建设置。

技巧和窍门

以下受保护的方法可用于创建自定义方法以修改构建设置

  • addToCurrent(string $field, $value) 将设置当前数据设置中 $field 的值。
  • removeFromCurrent(string $field) 将从当前数据设置中删除 $field。注意,这可能会在构建类实例时引发错误,因为它将从一个构造函数调用中删除一个参数。
  • currentSetup() 将返回整个当前数据设置,这对于执行比添加或删除内容更复杂的操作很有用。
  • replaceCurrentSetup(array $newSetup) 将替换整个当前设置。

魔法方法 __call 以这种方式实现,即调用 withParameter 方法将尝试用当前设置中的 parameter 键替换。例如,withFooBar('baz') 将设置当前设置中的 fooBar 键的值为 'baz'

为了更好的类型提示,建议在子构建器中重写 build 方法,将返回类型设置为要实例化的类。

$base 属性(或当前设置)中的任何可调用值在构建时会调用。例如,使用 $base['foo' => function() { return 'bar'; }],参数 foo 所使用的值将是 'bar'。这对于在每次构建时产生不同的值很有用,如某些自增 ID。

示例

以下是一个构建器示例,与生成器创建的内容非常相似

//Class to build
final class MetalSlugCharacter {
    private string $name;
    private Weapon $weapon;
    private int $bombs;

    public function __construct(string $name, Weapon $weapon, int $bombs) {...}
    public static function create(): self {}
    public function loadBombs(int $howMany): void {}
}
//The builder
final class MetalSlugCharacterBuilder extends Builder {
    //Class to instantiate, leave empty to return an array
    protected const CLASS_TO_BUILD = MetalSlugCharacter::class;
    //Constructor to use, leave empty to use the default __construct()
    protected const USE_CONSTRUCTOR = 'create';
    //Call this method after instantiation. First element is the method name, the other will be used as parameters to the call 
    protected const AFTER_BUILD_CALL = ['loadBombs', 100];

    public function __construct() {
        //This data setup will be used by default
        //It must hold enough parameters and in the same order as needed by the constructor being used
        $this->base = [
            'name'   => 'Marco Rossi',
            'weapon' => new RocketLauncher(),
            'bombs'  => fn() => rand(10, 50), //Callables will be invoked to get the value
        ];
    }

    //Tip: override build() for type hinting
    public function build(): MetalSlugCharacter {
        return parent::build();
    }

    //We declare methods to modify the build setup 
    public function withName(string $name): self {
        return $this->addToCurrent('name', $name);
    }

    public function withWeapon(Weapon $weapon): self {
        return $this->addToCurrent('weapon', $weapon);
    }
    
    //Be creative with method names to express intention and improve readability
    public function withoutWeapon(): self  {
        return $this->addToCurrent('weapon', new BareHands);
    }
}

现在,在我们的测试中

$builder = new MetalSlugCharacterBuilder();

//A new instance with default values
$character = $builder->build(); //Returns the default Marco + Rocket Launcher

//A new instance with a different setup
$character = $builder
                ->withName('Eri Kasamoto')
                ->withWeapon(new HeavyMachineGun())
                ->build();

//A new instance with the same setup as the last one created
$character = $builder->cloneLast(); //Returns Eri with the Heavy Machine Gun again

$character = $builder->repeatLastSetup() //Repeats the Eri + Heavy Machine Gun setup
    ->withBombs(5) //Methods starting with "with" change the current setup using __call 
    ->build();

添加更多酷炫的东西

对于反复使用的构建器设置,将其封装在静态工厂方法中以消除重复并提高可读性。这是一种称为 Object Mother 的设计模式。

//Instead of doing all the time
$character = $builder
                ->withWeapon(new HeavyMachineGun())
                ->withBombs(99)
                ->build();

//Encapsulate concrete setups in a static factory methods
public static function fullyLoadedWithMachineGun(): MetalSlugCharacter
{
    return (new self())
                ->withWeapon(new HeavyMachineGun())
                ->withBombs(99)
                ->build();
}

public static function emptyHanded(): MetalSlugCharacter
{
    return (new self())
                ->withWeapon(new BareHands())
                ->withBombs(0)
                ->build();
}


//And start to use meaningful one liners that do not create distraction in the tests
$character = MetalSlugCharacterBuilder::fullyLoadedWithMachineGun();
$character = MetalSlugCharacterBuilder::emptyHanded();

如果您的构建器 + 母亲变得过于杂乱,请考虑将它们分开,在母亲内部使用构建器。

许可证

此库受 MIT 许可证许可。更多详细信息请参阅 LICENSE 文件。