为什么我们需要构造函数?

Why do we need a constructor?

假设:(1) 有一个 class 有一个构造函数,其方法使用注入对象,如:

class SomeClass
{
    protected $object1;
    protected $object2;

    public function __construct(
        Object1Interface $object1,
        Object2Interface $object2
    ) {
        $this->object1 = $object1;
        $this->object2 = $object2;
    }

// methods that use Object1 and Object2 classes by $this->object1 and $this->object2.
}

和 (2) 有相同的 class,没有构造函数,但是 class 方法接受 Object1Object2 作为依赖项,如下所示:

class SomeClass
{
    public function doStuff1(Object1Interface $object1)
    {// do the stuff}

    public function doStuff2(Object2Interface $object2)
    {// do the stuff}
}

互联网上有许多提倡第一种变体的例子。

但是这些有什么区别呢?

它们是不同的,并不是真正提倡的。

(1) 被广泛称为构造函数依赖注入。它被提倡为比 setter 依赖注入更好的首选形式,而不是 (2)。这些依赖项来自消费者 "hidden",通常在对象生命周期内不会更改。

考虑抽象的 http 客户端:

$httpClient = new HttpClient(new CurlAdapter());
// or
$httpClient = new HttpClient(new SocketAdapter());

切换适配器不会影响客户端的使用方式:

$response = $httpClient->get($url);

构造函数 DI 被提倡优于 setters,因为它强制注入依赖项。此外,setters 通常允许在对象生命周期内更改依赖项,从而改变其行为并打开出现棘手且难以调试的错误的可能性。

$dataContainer = $serviceLocator->get('SomeDataContainer');
$dataContainer->setStorage(new ArrayStorage());
$dataContainer->set('a','b');
$dataContainer->get('a'); // => 'b'
    // called somewhere else
    {
    $serviceLocator
        ->get('SomeDataContainer')
        ->setStorage(new RedisStorage());
    }
$dataContainer->get('a'); // => 'foobar' WAT

(2) 用于不同的用例,通常不与 DI 重叠。 以这种方式传递依赖关系有多种原因。它们可能是交互界面的一部分,在对象生命周期中经常更改或在调用时决定。依赖项在概念上可能不属于对象,但对于该特定操作仍然需要。重要的是它们不应存储在对象中并以可能导致副作用的方式使用!

class SomeClass
{
    protected $dep;

    public function doSomething(DepInterface $dep)
    {
        // do the stuff
        // store dep for later
        $this->dep = $dep;
        // This is similar to issue with setters but much worse.
        // Side effect is not obvious
    }

    public function doSomethingElse()
    {
       $this->dep->increment();
    }
}

for ($i = 0; $i < 100; $i++) {
   $foo->doSomething($bar);
   if (rand(0, 100) == $i) {
       // represents conditions out of direct control.
       // such as conditional or timing errors, alternative flows,
       // unanticipated code changes
       $foo->doSomethng($baz);
   }
   $foo->doSomethingElse();
}
// what will be the result?

考虑这个解决一些不切实际问题的 DDD 示例:

class Bus
{
    public function board(Passenger $passenger, TicketingService $tservice)
    {
        // @todo check bus have seats
        // throws exception if ticket is invalid
        $tservice->punchPassengerTicket($this, $passenger);
        $this->passengers[] = $passenger;
    }
}

if ($bus->isIntercity()) {
    $ticketingService = new BookingService();
} else {
    $ticketingService = new PrepaidCityTransportCardsService();
} 
$bus->board($passenger, $ticketingService);

在这个例子中,我们明确强制登车巴士需要门票,并且在上车时打票。但是,如果我们要在 Bus 实例化时注入 TicketingService,即使在根本没有上车的情况下,我们也会得到更复杂的依赖图。通过服务来执行操作但从不存储它会大大降低复杂性,并在这种情况下显着提高可测试性。

这种技术在领域驱动设计上下文中称为双重分派(不要与 Double Dispatch 混淆)并被广泛使用。
您可能认为我们可以在登机前检查机票并完全消除对票务服务的依赖。好吧,这完全是另一个话题。

第一个最大的好处是你总是处于对象处于"correct"

的状态

如果你有一个点 class 包含 2 个变量 x 和 y

第一种方法确保 x 和 y 始终存在于 class 中(不为空),因此方法只能使用它们。

第二种意味着每个方法在使用之前都必须检查 x 和 y 是否设置正确

两者各有优缺点