You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何创建多属性PHP Immutable类?兼顾构造器精简与不可变性

如何设计不可变的PHP Gameworld类(避免构造器臃肿)

嘿,针对你想把Gameworld改成不可变类但又不想让构造函数参数过多的问题,我来聊聊可行的方案,先说说你提到的单次Setter思路,再给你推荐更优雅的替代方案。


你的单次Setter思路:可行但有局限

你想到的每个Setter只能调用一次的方案,确实能实现不可变性,但实际用起来会有几个需要注意的问题:

优点

  • 避开了构造函数参数爆炸的问题,能逐步设置属性,灵活性不错
  • 严格限制每个属性只能被设置一次,符合不可变类的核心要求

缺点

  • 对象会处于部分初始化状态:直到所有Setter都调用完,对象的属性才是完整的,如果中途不小心使用了对象,很可能因为未初始化的属性抛出错误
  • 代码冗余:每个Setter都要重复检查used_setters数组,后续加新属性时维护成本高
  • 无法强制必填属性:如果忘记设置某个必要属性,编译器不会提醒你,容易出现遗漏

推荐方案:建造者模式(Builder Pattern)

建造者模式简直是为这种场景量身定做的——既能灵活设置多个属性,又能保证最终生成的对象是完全初始化且不可变的,代码可读性和维护性都很高。

实现思路

  1. 先写一个GameworldBuilder类,它包含和Gameworld一样的属性,并且提供流畅的Setter方法(返回自身,支持链式调用)
  2. Gameworld的构造函数设为私有,只能由Builder类调用,确保外部无法直接创建未完成的实例
  3. Gameworld只提供Getter方法,完全不暴露任何修改属性的入口,彻底保证不可变性

完整代码示例

不可变的Gameworld类

<?php

class ImmutableException extends \Exception {}

class Gameworld {
    /** @var string */
    private $name;
    /** @var string */
    private $type;
    /** @var bool */
    private $is_online;
    /** @var int */
    private $online_players;
    /** @var int */
    private $online_players_record;
    /** @var string */
    private $description;
    /** @var string */
    private $location;
    /** @var \DateTime */
    private $created_at;

    // 私有构造函数,仅允许Builder调用
    private function __construct(
        string $name,
        string $type,
        bool $is_online,
        int $online_players,
        int $online_players_record,
        string $description,
        string $location,
        \DateTime $created_at
    ) {
        $this->name = $name;
        $this->type = $type;
        $this->is_online = $is_online;
        $this->online_players = $online_players;
        $this->online_players_record = $online_players_record;
        $this->description = $description;
        $this->location = $location;
        $this->created_at = $created_at;
    }

    // 仅提供Getter方法,禁止修改属性
    public function getName(): string { return $this->name; }
    public function getType(): string { return $this->type; }
    public function isOnline(): bool { return $this->is_online; }
    public function getOnlinePlayers(): int { return $this->online_players; }
    public function getOnlinePlayersRecord(): int { return $this->online_players_record; }
    public function getDescription(): string { return $this->description; }
    public function getLocation(): string { return $this->location; }
    public function getCreatedAt(): \DateTime {
        // 返回DateTime的克隆,防止外部修改内部状态
        return clone $this->created_at;
    }

    // 静态方法获取Builder实例,简化调用
    public static function builder(): GameworldBuilder {
        return new GameworldBuilder();
    }
}

配套的GameworldBuilder类

class GameworldBuilder {
    /** @var string */
    private $name;
    /** @var string */
    private $type;
    /** @var bool */
    private $is_online = false; // 设置合理默认值
    /** @var int */
    private $online_players = 0;
    /** @var int */
    private $online_players_record = 0;
    /** @var string */
    private $description = '';
    /** @var string */
    private $location = '';
    /** @var \DateTime */
    private $created_at;

    public function __construct() {
        // 默认创建时间设为当前时间
        $this->created_at = new \DateTime();
    }

    // 流畅式Setter,返回自身支持链式调用
    public function name(string $name): self {
        $this->name = $name;
        return $this;
    }

    public function type(string $type): self {
        $this->type = $type;
        return $this;
    }

    public function isOnline(bool $is_online): self {
        $this->is_online = $is_online;
        return $this;
    }

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

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

    public function description(string $description): self {
        $this->description = $description;
        return $this;
    }

    public function location(string $location): self {
        $this->location = $location;
        return $this;
    }

    public function createdAt(\DateTime $created_at): self {
        $this->created_at = clone $created_at; // 克隆避免外部修改
        return $this;
    }

    // 构建不可变实例,这里可以做参数校验
    public function build(): Gameworld {
        // 校验必填属性,避免创建不完整的对象
        if (empty($this->name) || empty($this->type)) {
            throw new \InvalidArgumentException('Name and type are required for Gameworld');
        }

        return new Gameworld(
            $this->name,
            $this->type,
            $this->is_online,
            $this->online_players,
            $this->online_players_record,
            $this->description,
            $this->location,
            $this->created_at
        );
    }
}

使用示例

// 链式调用创建不可变的Gameworld实例
$gameworld = Gameworld::builder()
    ->name('Fantasy Land')
    ->type('MMORPG')
    ->isOnline(true)
    ->onlinePlayers(1200)
    ->onlinePlayersRecord(5000)
    ->description('A magical open-world RPG')
    ->location('US East')
    ->build();

// 只能读取属性,完全无法修改
echo $gameworld->getName(); // 输出:Fantasy Land
// 没有任何Setter方法,彻底保证不可变性

为什么建造者模式更优?

  • 绝对不可变:Gameworld实例一旦创建,所有属性(包括引用类型如DateTime)都无法被外部修改
  • 避免部分初始化:只有调用build()后才会生成实例,还能在build()里做参数校验,确保必填属性都已设置
  • 代码更优雅:链式调用的写法可读性极强,后续修改或新增属性也很方便
  • 默认值友好:可以在Builder里给非必填属性设置合理默认值,减少重复代码

如果坚持用单次Setter方案?

如果你的场景特别简单,不想引入额外的Builder类,那单次Setter方案也可以用,但建议补充几点:

  • 加一个isInitialized()方法,检查所有必填属性是否已设置,在使用对象前调用
  • 给所有Setter加上@internal注解,提示外部代码不要直接调用,最好配合工厂方法来确保对象完全初始化
  • 严格遵守“只在Setter里修改属性”的规则,不要在其他方法里偷偷修改

内容的提问来源于stack exchange,提问作者nass

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:26:08