Zapewnienie niemutowalności obiektów danych poprzez zastosowanie wzorca wytwórcy

Niemutowalność danych jest bardzo pożądana w szczególności w systemach wielowątkowych. Nie zawsze jednak jest prosta do zapewnienia. Weźmy np. pod uwagę system w którym dane mamy rozproszone pomiędzy wiele źródeł a sposób ich pozyskiwania składa się z kilku kroków.

Najprostszym przypadkiem takiej sytuacji może być konfiguracja aplikacji konsolowej. Załóżmy, że mamy aplikację, która posiada plik konfiguracyjny, ale jednocześnie zachowanie aplikacji może być nadpisane poprzez użycie przełączników podczas wywołania z konsoli. W sytuacji takiej musimy, w pierwszej kolejności wczytać ustawienia z pliku a następnie nałożyć na to opcje przekazane przez argumenty.

W zwyczaju mam enkapsulować konfigurację w obiekt, np:

class Settings {
    /** @var bool */
    private $a = False;

    /** @var int */
    private $ratio;

    public function setA() {
        $this->a = True;
    }

    public function resetA()
    {
        $this->a = False;
    }

    /**
     * @return bool
     */
    public function isA(): bool
    {
        return $this->a;
    }

    /**
     * @param int $ratio
     */
    public function setRatio(int $ratio)
    {
        $this->ratio = $ratio;
    }

    /**
     * @return int
     */
    public function getRatio(): int
    {
        return $this->ratio;
    }
}

/**
 * @param Settings $settings
 */
function read_params(Settings $settings): void
{
    while (!empty($argv)) {
        $arg = array_shift($argv);
        switch ($arg) {
            case '-a':
                $settings->setA();
                break;
            case '--ratio':
                $settings->setRatio((int)array_shift($argv));
                break;
        }
    }
}

/**
 * @param Settings $settings
 */
function read_config(Settings $settings): void
{
    $file_settings = read_json_file($configFile)

    if (isset($file_settings['a'])){
        if ((bool)$file_settings['a'] == True) {
            $settings->setA();
        } else {
            $settings->resetA();
        }
    }

    if (isset($file_settings['ratio'])){
        $settings->setRatio((int)$file_settings['ratio']);
    }
}

/**
 * @return Settings
 */
function read_settings(): Settings
{
    $settings = new Settings();
    read_config($settings);
    read_params($settings);
    return $settings;
}

array_shift($argv);
var_dump(read_settings());

Rozwiązanie takie ma jednak podstawową wadę, każdy kto otrzyma klasę z ustawieniami, może ją zmienić. Nawet jeżeli nie powinien tego robić. Najprostszym rozwiązaniem, wydaje się użycie interfejsu i “schowanie” implementacji.

interface SettingsInterface {
    /**
     * @return bool
     */
    function isA(): bool;

    /**
     * @return int
     */
    function getRatio(): int;
}

oraz zmiana funkcji read_settings:

/**
 * @return SettingsInterface
 */
function read_settings(): SettingsInterface
{
    $settings = new Settings();
    read_config($settings);
    read_params($settings);
    return $settings;
}

Rozwiązanie to jednak nie jest właściwe. Co prawda nie eksponujemy obiektu i w miejscu użycia posługujemy się interfejsem który jest tylko do odczytu, to nadal można zmienić nasz obiekt poprzez wywołanie $settings->resetA() i nasz kod zadziała. Dlatego pomimo ekspozycji interfejcu read-only, nie możemy mówić o zapewnieniu niemutowalności obiektu klasy Settings.

Oczywiście można klasę Settings zmienić tak aby jej parametry były przekazywane poprzez konstruktor:

class Settings {
    /** @var bool */
    private $a = False;

    /** @var int */
    private $ratio;

    /**
     * @param bool $a
     * @param int $ratio
     */
    public function __construct(bool $a, int $ratio)
    {
        $this->a = $a;
        $this->ratio = $ratio;
    }

    /**
     * @return bool
     */
    public function isA(): bool
    {
        return $this->a;
    }

    /**
     * @return int
     */
    public function getRatio(): int
    {
        return $this->ratio;
    }
}

Powoduje to jednak szereg problemów związanych z wytworzeniem naszej klasy i kończy się albo dużą ilością zmiennych i kodem spaghetti, albo operowaniem na tablicach. Żadne z tych rozwiązań nie jest w mojej ocenie dobre. Z pomocą w tej sytuacji, przychodzi wspomniany w tytule wytwórca (ang. Builder):

class SettingsBuilder {
    /** @var bool */
    private $a = False;

    /** @var int */
    private $ratio;

    public function setA() {
        $this->a = True;
    }

    public function resetA()
    {
        $this->a = False;
    }

    /**
     * @param int $ratio
     */
    public function setRatio(int $ratio)
    {
        $this->ratio = $ratio;
    }

    /**
     * @return Settings
     */
    public function build(): Settings {
        return new Settings($this->a, $this->ratio);
    }
}

Trzeba jeszcze zmienić funkcje odczytu ustawień, oraz usunąć zbędny już teraz interfejs:

/**
 * @param SettingsBuilder $settingsBuilder
 */
function read_params(SettingsBuilder $settingsBuilder): void
{
    while (!empty($argv)) {
        $arg = array_shift($argv);
        switch ($arg) {
            case '-a':
                $settingsBuilder->setA();
                break;
            case '--ratio':
                $settingsBuilder->setRatio((int)array_shift($argv));
                break;
        }
    }
}

/**
 * @param SettingsBuilder $settingsBuilder
 */
function read_config(SettingsBuilder $settingsBuilder): void
{
    $file_settings = read_json_file($configFile);

    if (isset($file_settings['a'])){
        if ((bool)$file_settings['a'] == True) {
            $settingsBuilder->setA();
        } else {
            $settingsBuilder->resetA();
        }
    }

    if (isset($file_settings['ratio'])){
        $settingsBuilder->setRatio((int)$file_settings['ratio']);
    }
}

/**
 * @return Settings
 */
function read_settings(): Settings
{
    $settingsBuilder = new SettingsBuilder();
    read_config($settingsBuilder);
    read_params($settingsBuilder);
    return $settingsBuilder->build();
}

I mam w pełni działający kod z niemutowalnym obiektem ustawień oraz z zachowaną strukturą kodu oraz przepływu danych.

Końcowy listing prezentuje się w następujący sposób:

class Settings {
    /** @var bool */
    private $a = False;

    /** @var int */
    private $ratio;

    /**
     * @param bool $a
     * @param int $ratio
     */
    public function __construct(bool $a, int $ratio)
    {
        $this->a = $a;
        $this->ratio = $ratio;
    }

    /**
     * @return bool
     */
    public function isA(): bool
    {
        return $this->a;
    }

    /**
     * @return int
     */
    public function getRatio(): int
    {
        return $this->ratio;
    }
}

class SettingsBuilder {
    /** @var bool */
    private $a = False;

    /** @var int */
    private $ratio;

    public function setA() {
        $this->a = True;
    }

    public function resetA()
    {
        $this->a = False;
    }

    /**
     * @param int $ratio
     */
    public function setRatio(int $ratio)
    {
        $this->ratio = $ratio;
    }

    /**
     * @return Settings
     */
    public function build(): Settings {
        return new Settings($this->a, $this->ratio);
    }
}

/**
 * @param SettingsBuilder $settingsBuilder
 */
function read_params(SettingsBuilder $settingsBuilder): void
{
    while (!empty($argv)) {
        $arg = array_shift($argv);
        switch ($arg) {
            case '-a':
                $settingsBuilder->setA();
                break;
            case '--ratio':
                $settingsBuilder->setRatio((int)array_shift($argv));
                break;
        }
    }
}

/**
 * @param SettingsBuilder $settingsBuilder
 */
function read_config(SettingsBuilder $settingsBuilder): void
{
    $file_settings = read_json_file($configFile);

    if (isset($file_settings['a'])){
        if ((bool)$file_settings['a'] == True) {
            $settingsBuilder->setA();
        } else {
            $settingsBuilder->resetA();
        }
    }

    if (isset($file_settings['ratio'])){
        $settingsBuilder->setRatio((int)$file_settings['ratio']);
    }
}

/**
 * @return Settings
 */
function read_settings(): Settings
{
    $settingsBuilder = new SettingsBuilder();
    read_config($settingsBuilder);
    read_params($settingsBuilder);
    return $settingsBuilder->build();
}

array_shift($argv);
var_dump(read_settings());

Dodatkowo, możemy zmodyfikować klasę wytwórcy, tak aby wszystkie jej metody z wyjątkiem build zwracały jej własną instancję i otrzymujemy wygodny mechanizm “method chaining”:

class SettingsBuilder {
    /** @var bool */
    private $a = False;

    /** @var int */
    private $ratio;

    /**
     * @return $this
     */
    public function setA() {
        $this->a = True;

        return $this;
    }

    /**
     * @return $this
     */
    public function resetA()
    {
        $this->a = False;

        return $this;
    }

    /**
     * @param int $ratio
     * @return $this
     */
    public function setRatio(int $ratio)
    {
        $this->ratio = $ratio;
        
        return $this;
    }

    /**
     * @return Settings
     */
    public function build(): Settings {
        return new Settings($this->a, $this->ratio);
    }
}

(new SettingsBuilder())
    ->setA()
    ->setRatio($ratio)
    ->build();

Podsumowując, aby uzyskać elastyczny sposób budowania obiekty reprezentującego dane, a jednocześnie zapewnić jego niemutowalność, należy rozdzielić odpowiedzialność budowania danych od kontenera je przechowującego.

Comments:0

Leave a Reply

Your email address will not be published. Required fields are marked *