Pojmenované konstruktory v PHP

Jan Machala
Jun 14, 2017 · 3 min read
Factory method

PHP nabízí pouze jeden kontruktor pro třídu. Tato vlastnost může být velmi limitující a svádějící ke špatnému použití. Podívejme se na níže uvedený kontruktor, který už má pár roků bobtná. Mějme třídu ValueObject\Price, která v sobě drží měnu a částku výrobku:

<?php
use
Money\Money;
use Money\Currency as MoneyCurrency;
use Nette\Utils\Json as NJson;

class Price
{
/** @var Money */
private $money;

/**
*
@param mixed $from
*/
public function __construct($from = null)
{
if (NULL !== $from) {
if (is_string($from)) {
$obj = NJson::decode($from);
} elseif (is_array($from)) {
$obj = (object)$from;
} elseif ($from instanceof \stdClass) {
$obj = $from;
} elseif ($from instanceof Money) {
$obj = $from;
}

if ($obj instanceof Money) {
$this->money = clone $obj;
} else {
$this->money = new Money($obj->amount, new MoneyCurrency($obj->currency));
}
}
}
}

Konstruktor je vysoce univerzální a zvládá načíst přes svůj jediný argument $from hodnoty jako string, json string, pole, object, stdClass a instanci Money (třída z MoneyPHP).

Použitím výše uvedeného konstruktoru s námi třída komunikuje tímto jazykem:

<?php
$price = new Price('{"amount":10,"currency":"CZK"}');
$price = new Price(['amount'=>10, 'currency'=>'CZK']);
$price = new Price(new Money(10, new Currency('CZK')));

Konstruktor bere až 5 různých vstupních kombinací a tím výrazně znemožnuje pochopení a použití této třídy. Co s tím?

Refaktorujeme na pojmenované konstruktory

Přidejme pár veřejných statických funkcí pro vytváření instancí. Tato změna nám umožní se zbavit podmínek uvnitř konstruktoru.

<?php
use
Money\Money;
use Nette\Utils\Json as NJson;

class Price
{
/** @var Money */
private $money;
/**
*
@param Money|\stdClass|null $from
*/
public function __construct($from = null)
{
if (NULL !== $from) {
if ($obj instanceof Money) {
$this->money = clone $obj;
} else {
$this->money = new Money($obj->amount, new MoneyCurrency($obj->currency));
}
}
}

public static function fromJsonString(string $jsonString)
{
return new Price(NJson::decode($jsonString));
}

public static function fromArray(array $array)
{
return new Price((object) $array);
}

public static function fromStdClass(stdClass $stdClass)
{
return new Price($stdClass);
}

public static function fromMoney(Money $money)
{
return new Price($money);
}
}

Každá metoda teď respektuje SRP. Public interface třídy je jasný a srozumitelný, metody dělají jen to co mají. Je tímto refactoring hotový?

Osobně mě vadí fakt, že stále je možné vytvářet nové instance pomocí současného konstruktoru new Price(‘{“amount”:10,”currency”:”CZK”}’).

<?php
use
Money\Money;
use Money\Currency as MoneyCurrency;
use Nette\Utils\Json as NJson;

class Price
{
/** @var Money */
private $money;

/**
*
@param mixed $from
*/
private function __construct($from = null)
{
...
}
}

Konstruktor již není veřejný, lze tedy zrušit jeho obsah a upravit factory metody.

<?php
use
Money\Money;
use Money\Currency as MoneyCurrency;
use Nette\Utils\Json as NJson;

class Price
{
/** @var Money */
private $money;

private function __construct(){}

public static function fromJsonString(string $jsonString)
{
$obj = NJson::decode($jsonString);
$price = new Price;
$price->money = new Money($obj->amount, new MoneyCurrency($obj->currency));
return $price;
}
...
}

Nyní je nutné upravit všechny místa vytváření třídy Price a nahradit je za použití statických metod. Tímto jsme kompletně přešli na pojmenované konstruktory a zrušili magický konstruktor.

Nové rozhraní

<?php
$price = Price::fromJsonString('{"amount":10,"currency":"CZK"}');
$price = Price::fromArray(['amount'=>10, 'currency'=>'CZK']);
$price = Price::fromMoney(new Money(10, new Currency('CZK')));

Novým rozhraním získáváme kontrolu nad vstupními daty a můžeme jednotlivé metody upravit, aby více reflektovaly požadavky.

public static function fromAmountAndCurrency($amount, $currencyCode)
{
$price = new Price;
$price->money = new Money($amount, new MoneyCurrency($currencyCode));
return $price;
}

Statická metoda, pak nabízí nový komunikační jazyk ValueObject\Price čímž jasně dává najevo nový doménový jazyk aplikace.

<?php
$price = Price::fromAmountAndCurrency(10, 'CZK');

Nyní jsme vytvořili první metodu, která není jen pouhým implementačním detailem, ale zapadá do doménového jazyka aplikace. Oba dva případy užití mají své pro i proti a je už jen na programátorovi, kterou metodu použije.


Poznámky:

  1. Uvnitř statické metody je možné přistupovat k privátním proměnným instance vlastní třídy.
  2. Návrhový vzor Factory method

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade