简体   繁体   中英

Why do we need a constructor?

Suppose: (1) there is a class with a constructor whose methods use injected objects, like:

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.
}

and (2) there this same class, no constructor, but class methods accept Object1 and Object2 as dependencies, like this:

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

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

There are numerous examples over the internet that advocate the 1st variant.

But what's the difference between these?

They are different and are not really advocated one over another.

(1) is widely known as constructor Dependency Injection. It is advocated as better, preferred form over setter dependency injection, not over (2). Those dependencies are "hidden" from the consumer and normally do not change during object lifetime.

Consider abstract http client:

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

Switching adapter does not affect how client is used:

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

Constructor DI is advocated as superior to setters as it enforces that dependency was injected. Besides, setters usually allow dependencies to be changed during object lifetime, altering its behavior and opening possibility for tricky and hard to debug errors.

$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) is used in different use cases and normally do not overlap with DI. There are various reason to pass dependencies this way. They might be a part of interaction interface, change often during object lifetime or be decided at call time. Dependencies might not conceptually belong to the object but still be needed for that specific operation. It is important that they should not be stored in the object and used in a way that can cause side effects!

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?

Consider this DDD example solving some unrealistic problem:

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);

In this example we explicitly enforce that boarding bus requires tickets and tickets are punched upon boarding. But if we are going to inject TicketingService at Bus instantiation we will get more complicated dependency graph even for the cases where boarding does not happen at all. Passing service to perform operations but never storing it reduces complexity quite a bit and significantly improves testability in this case.

This technique is called double dispatch in domain driven design context(not to be confused with Double Dispatch ) and used extensively.
You might think that we could have checked tickets before boarding and removed dependency on ticketing service altogether. Well, this is a whole another topic.

The big benifit of the first is that you are always in a state where the object is "correct"

If you had a point class containing 2 variables x and y

The first way makes sure x and y always exist in the class (is not null) so methods can just use them.

The second means each method will have to check if x and y are set correctly before using them

Both have advantages and disadvantages though

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM