PHP中的组合和聚合

3

我正在阅读一篇有关面向对象编程中关系的文章,内容包括关联、组合、聚合等。有些内容令人困惑,我在网上发现了许多矛盾的信息,希望有人能够为此提供一些解释。

因此,在PHP中,以下代码被称为组合,许多文章和教程都指出应该使用组合来代替继承。

class A
{
  
}

class B
{
    public function __construct(protected A $a)
    {
    }
}

读了一些关于组合和聚合的文章后,似乎上述代码示例实际上是聚合而不是组合。因为在组合中,类A的对象不能独立存在于类B之外,当类B的对象被销毁时,类A的对象也应该被销毁。但在上述代码中,显然不是这种情况,因为类A的对象可以存在于类B之外,所以它的生命周期并不依赖于类B。

以下是组合的示例:

class B
{
    public A $a;
  
    public function __construct()
    {
        $this->a = new A();
    }
}

据我理解,聚合指的是A类对象可以存在于B类之外,而组合则表示A类对象的生命周期依赖于B类,且不能在B类之外存在。
我理解正确吗?
1个回答

6
请注意,这些是用于关联类和对象的模式。
我要提出的第一个观点是,像“使用组合代替继承”这样的概括性规则反映了一些缺失的上下文。如果适当的话,请使用继承。
这些其他模式是用于将类相互关联。
聚合通常用于层次结构/父子关系。
经典的例子是员工和部门之间的关系。
存在Employee对象
Employee是Department的“拥有者/成员”
你可以说“Department Foo”聚合了0-N个员工。
因此,为了建模这种情况,Department通常会有一个内部employees[]数组。
相反,员工可以在内部存储department对象。这本质上与两个类之间的关系匹配为Many >-< Many。
组合反映了更严格的父/子关系,暗示着ObjectA由1-N个Objects B组成。
组合关系的经典例子是购物车,其中包含0-N个产品行项目。
假设Cart和Lineitem的类,Cart将存储Lineitems的数组。
主要区别在于聚合和组合的关系,如果使用组合,当一个购物车被销毁时,所有相关的行项目对象都应该被销毁。
而在聚合示例中,如果一个部门被取消,不一定意味着所有聚合的员工都会被取消,而是删除部门仅会打破员工与部门之间的关系,并可能需要将存储员工部门对象的变量为空,直到员工被分配到另一个部门或者他们的雇佣结束。
实际上,由于PHP是页面作用域,变量和对象组成的生命周期很短,因此除了ORM之外,你不会真正发现许多情况下使用这些UML模式。数据需要持久化,通常通过某种类型的RDBMS或文档数据库来进行持久化,其中ORM将模拟表之间的关系,或者可能是层次结构。
对于您的示例:
这些类似foo/bar A/B的示例没有语义意义或价值。示例#1中唯一的机制是,必须先有ObjA才能构造ObjB。您提供的语法是PHP8中的新功能,我一开始没有注意到。
看起来这个例子更有可能是:
class A
{
    private bSet = array();

    public function addB(B $b) {
       $this->bSet[] = new B($this);
    }  
}

class B
{
    private $a;

    public function __construct(A $a)
    {
        $this->a = $a;            
    }
}


// B shouldn't be made without a Parent A
$objA = new A();
$objA->addB();
$objA->addB();

然而,从技术上讲,没有任何阻止你制作一个B的方法,因为构造函数始终是公共的。

$objB = new B($objA);   

使用聚合,这将是一个更有可能的情况:
class A
{
    private bSet = array();

    public function addB(B $b) {
       $this->bSet[] = $b;
       $b->setA($this);
    }

    public function delB(B $b) {
        for ($i=0; $i < count($this->bSet); $i++) {
            if ($b === $this->bSet[$i]) {
                unset($this->bSet[$i];
                break;
            }
        }
    }  
}

class B
{
    private $a;

    public function setA(A $a) {
        if ($this->a) {
            $this->a->delB($this);
        }
        $this->a = $a;
    }

    public function getA() {
        return $this->a;
    }

}

$objA1 = new A();
$objA2 = new A();

$objB1 = new B();
$objB2 = new B();
$objB3 = new B();

$objA1->addB($objB2);
$objA2->addB($objB1);
$objA2->addB($objB3);
//Change $objB3's parent from A1 to A2.
$objB3->setA($objA1);

A和B彼此独立,但仍然拥有所有权。
在PHP开发中,我发现更实用的是依赖注入作为主要PHP框架(Symfony,Laravel)的基础,以及其他常见的面向对象设计模式的实现,这些模式可以在Gang of 4 bookDomain Driven Design或其他bookswebsites中找到。

感谢您的详细解释。这一切都很有道理,但根据您的答案,如果我们遵循正式定义,我的第一个示例就是聚合示例而不是组合示例。我知道这取决于上下文,而且示例非常简单且不实用,但在实践中,每当您通过构造函数接受依赖项时,几乎99%的PHP教程和文章都称之为组合,这就是我感到困惑的原因,因为组合意味着类负责创建其依赖项的对象(由...组成)。 - Fuze
是的,你的第一个例子会表明组合,但是你提供的语法不是有效的PHP。 - gview
你的意思是什么?就语法而言,它是一个有效的PHP。至少在PHP 8.0+中是有效的。此外,我认为我的第一个例子根据正式定义应该表明聚合,而不是组合。除非我再次误解了它。 - Fuze
我的错,我忽略了 PHP 8 中参数声明的简写。第一个示例指示了 A -> B 的组合,因为不能创建 B 而不使用 A,因为在 B 的构造函数中需要一个 A 对象。当您拥有这样的骨架类时,您可以根据自己的理解进行解释。PHP 无法强制执行组合规则,因为您无法嵌套类。您可以使用匿名类来更严格地约束子对象。 - gview
明白了,谢谢。 - Fuze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接