如何创建多属性PHP Immutable类?兼顾构造器精简与不可变性
如何设计不可变的PHP Gameworld类(避免构造器臃肿)
嘿,针对你想把Gameworld改成不可变类但又不想让构造函数参数过多的问题,我来聊聊可行的方案,先说说你提到的单次Setter思路,再给你推荐更优雅的替代方案。
你的单次Setter思路:可行但有局限
你想到的每个Setter只能调用一次的方案,确实能实现不可变性,但实际用起来会有几个需要注意的问题:
优点
- 避开了构造函数参数爆炸的问题,能逐步设置属性,灵活性不错
- 严格限制每个属性只能被设置一次,符合不可变类的核心要求
缺点
- 对象会处于部分初始化状态:直到所有Setter都调用完,对象的属性才是完整的,如果中途不小心使用了对象,很可能因为未初始化的属性抛出错误
- 代码冗余:每个Setter都要重复检查
used_setters数组,后续加新属性时维护成本高 - 无法强制必填属性:如果忘记设置某个必要属性,编译器不会提醒你,容易出现遗漏
推荐方案:建造者模式(Builder Pattern)
建造者模式简直是为这种场景量身定做的——既能灵活设置多个属性,又能保证最终生成的对象是完全初始化且不可变的,代码可读性和维护性都很高。
实现思路
- 先写一个
GameworldBuilder类,它包含和Gameworld一样的属性,并且提供流畅的Setter方法(返回自身,支持链式调用) Gameworld的构造函数设为私有,只能由Builder类调用,确保外部无法直接创建未完成的实例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
相关产品推荐
相关产品推荐

