
问题分析:构造函数中的循环依赖
假设我们有两个模型类 a 和 b,它们之间存在一对多的关系:a 可以拥有多个 b,而 b 属于一个 a。为了方便访问,我们可能希望 a 对象能直接访问其关联的 b 对象列表,同时 b 对象也能直接访问其所属的 a 对象。
考虑以下简化的构造函数实现:
// 模型 B 的构造函数class B extends ParentModel{ protected $a; // 用于存储关联的 A 对象 public function __construct(int $id = null) { parent::__construct($id); $aId = $this->get('a_id'); // 从数据库获取关联 A 的ID if ($aId) { $this->a = new A($aId); // 在 B 的构造函数中实例化 A } }}// 模型 A 的构造函数class A extends ParentModel{ public $B = []; // 用于存储关联的 B 对象列表 public function __construct(int $id = null) { parent::__construct($id); // 假设 CarbonPL 是一个日期处理类 $this->date = new CarbonPL($this->get('date')); $this->initB(); // 在 A 的构造函数中初始化关联的 B 对象 } private function initB() { // 检查实例是否存在于数据库 if (!$this->isReferenced()) { return; } // 构建查询获取所有关联的 B 对象的 ID $query = B::getIDQuery(); $query .= ' WHERe is_del IS FALSE'; $query .= ' AND a_id = ' . $this->id; $ids = Helper::queryIds($query); foreach ($ids as $id) { $this->B[] = new B($id); // 在 A 的 initB 方法中实例化 B } }}登录后复制从上述代码可以看出,当尝试创建一个 A 对象时,其构造函数会调用 initB 方法,而 initB 方法会遍历数据库中的关联 B 对象ID,并为每个ID创建一个新的 B 对象。当 B 对象的构造函数被调用时,它又会尝试根据 a_id 实例化一个 A 对象。这样,A 实例化 B,B 又实例化 A,形成一个无限循环,导致程序崩溃。
解决方案:工厂方法与实例缓存
为了解决这种循环依赖和重复实例化的问题,我们可以采用工厂方法模式结合实例缓存机制。其核心思想是:不直接通过 new 关键字创建对象,而是通过一个静态的工厂方法来获取对象实例。这个工厂方法会维护一个内部缓存,如果某个ID对应的对象已经被创建过,就直接从缓存中返回,否则才创建新对象并加入缓存。
实现步骤
私有化构造函数: 将类的构造函数设为 private 或 protected。这样做可以阻止外部代码直接使用 new 关键字创建对象,强制所有对象创建都通过工厂方法进行。创建静态缓存: 在类内部定义一个静态数组或关联数组,用于存储已创建的对象实例,以对象的ID作为键。实现静态工厂方法: 创建一个公共的静态方法(例如 create_for_id),它接收对象的ID作为参数。在该方法内部,首先检查缓存中是否已存在该ID对应的对象。如果存在,则直接返回缓存中的实例。如果不存在,则通过私有构造函数创建一个新实例,将其添加到缓存中,然后返回新实例。示例代码
以下是 A 类应用工厂方法和实例缓存的示例:
立即学习“PHP免费学习笔记(深入)”;
无涯·问知 无涯·问知,是一款基于星环大模型底座,结合个人知识库、企业知识库、法律法规、财经等多种知识源的企业级垂直领域问答产品
40 查看详情
<?phpclass A extends ParentModel{ private static $cache = array(); // 静态缓存,存储 A 类的实例 public $B = []; // 关联的 B 对象列表 private function __construct(int $id) { parent::__construct($id); $this->date = new CarbonPL($this->get('date')); $this->initB(); } public static function create_for_id(int $id): A { // 检查缓存中是否已存在该ID的实例 if (isset(self::$cache[$id])) { return self::$cache[$id]; } else { // 如果不存在,则创建新实例并存入缓存 $result = new A($id); self::$cache[$id] = $result; // 缓存新创建的实例 return $result; } } private function initB() { if (!$this->isReferenced()) { return; } $query = B::getIDQuery(); $query .= ' WHERe is_del IS FALSE'; $query .= ' AND a_id = ' . $this->id; $ids = Helper::queryIds($query); foreach ($ids as $id) { // 现在通过 B 的工厂方法获取实例,而不是直接 new B() $this->B[] = B::create_for_id($id); } }}// 同样,对 B 类也应用相同的模式class B extends ParentModel{ private static $cache = array(); // 静态缓存,存储 B 类的实例 protected $a; // 关联的 A 对象 private function __construct(int $id) { parent::__construct($id); $aId = $this->get('a_id'); if ($aId) { // 现在通过 A 的工厂方法获取实例,而不是直接 new A() $this->a = A::create_for_id($aId); } } public static function create_for_id(int $id): B { if (isset(self::$cache[$id])) { return self::$cache[$id]; } else { $result = new B($id); self::$cache[$id] = $result; return $result; } }}登录后复制现在,无论何时需要 A 或 B 的实例,都应调用 A::create_for_id($id) 或 B::create_for_id($id)。这样,如果一个ID对应的对象已经被创建并存在于缓存中,它将被重用,从而有效地避免了无限循环的发生。
注意事项与总结
内存管理: 静态缓存会一直持有对象实例,直到脚本执行结束。对于大量不同ID的对象,这可能会占用较多内存。如果对象生命周期较短或数量巨大,需要考虑缓存清理策略或使用更复杂的缓存机制(如弱引用缓存,尽管PHP原生不支持)。缓存失效: 如果对象的数据在数据库中发生了变化,而缓存中的实例没有更新,则可能导致数据不一致。对于需要实时数据更新的场景,可能需要实现缓存失效机制(例如,在数据更新操作后清除对应ID的缓存)。单例模式的变种: 这种模式实际上是单例模式的一种变体,但它不是全局唯一的单例,而是针对每个ID唯一的单例。测试友好性: 私有构造函数可能会对单元测试造成一定挑战,因为直接实例化对象变得困难。可以通过依赖注入或在测试时提供专门的工厂实现来解决。替代方案: 另一种常见的解决方案是使用依赖注入容器(Dependency Injection Container),将对象的创建和依赖关系管理交给容器处理。容器可以配置为单例或每次请求创建新实例,并能更好地管理复杂的依赖图。然而,对于简单的循环依赖问题,工厂方法加缓存是一个轻量级且有效的解决方案。通过采用工厂方法和实例缓存,我们不仅解决了对象循环依赖导致的无限循环实例化问题,还实现了每个唯一ID的对象实例的重用,提高了程序的性能和资源利用率。这种模式在构建复杂对象模型时,尤其是在ORM(对象关系映射)框架中管理关联对象时,非常有用。
以上就是解决PHP对象循环依赖导致的无限循环实例化问题的详细内容,更多请关注php中文网其它相关文章!