Readonly – publiczne pola w klasie

Jak zapewne wiecie, istnieją trzy modyfikatory dostępu: public, protected i private. Początkujący programiści mogą mieć problem, którego z nich użyć w danym momencie. Nie ma się jednak co oszukiwać – w programowaniu występują trudniejsze dylematy do rozstrzygnięcia. Temat ten stosunkowo szybko staje się zrozumiały i w zasadzie, w codziennej pracy kodera, rzadko kiedy pojawia się pytanie, którego modyfikatora użyć.

Jestem fanem reguły, która mówi żeby zacząć od najbardziej restrykcyjnego, a ewentualnie później rozluźniać zakres w miarę potrzeb. Z tego tytułu najlepiej wyjść od prywatnych. Gdy istnieje potrzeba, zmienić na chronione i na końcu publiczne. Spotkałem się też z opiniami, że prywatne rzadko kiedy mają sens, dlatego domyślnie należy używać chronionych – a prywatne tylko wtedy, gdy chronione pozwalają klasie dziedziczącej na zbyt dużo. Ta idea jest mi daleka z uwagi na to o czym wspomniałem wcześniej.

W tym materiale chciałem skupić się na jednym konkretnym przypadku. Chodzi o obiekty, będące tak naprawdę reprezentacją nieskomplikowanych struktur danych – bez żadnych zachowań. Chodzi więc o: DTO (Data Transfer Object), komendy i inne tego typu byty. Do tej pory, natrafić można było na co najmniej dwa obozy:

1. Prywatne pola w klasie i metody gettery (ewentualnie settery).

<?php

declare(strict_types=1);

namespace Koddlo\Application\Command\Client;

final class CreateClientCommand
{
    public function __construct(
        private string $id,
        private string $email,
        private string $firstName,
        private string $lastName
    ) {
    }

    public function getId(): string
    {
        return $this->id;
    }
    
    public function getEmail(): string
    {
        return $this->email;
    }
    
    public function getFirstName(): string
    {
        return $this->firstName;
    }
    
    public function getLastName(): string
    {
        return $this->lastName;
    }
}

2. Po prostu publiczne pola.

<?php

declare(strict_types=1);

namespace Koddlo\Application\Command\Client;

final class CreateClientCommand
{
    public function __construct(
        public string $id,
        public string $email,
        public string $firstName,
        public string $lastName
    ) {
    }
}

W zgodzie z zasadą maksymalnego ograniczenia, ja rozłożyłem swój namiot w obozie numer 1. Gettery i tak generuje się w prosty sposób za pomocą IDE. Co jednak jest prawdą – nie ma mowy o jakiejkolwiek enkapsulacji w przypadku takich obiektów, w których brak logiki i zachowań. Dlaczego więc widoczność prywatna? Niektórzy twierdzą, że DTO powinny być niemutowalne – stąd też publiczne pola odpadały od razu. Ja nie uważam, że obligatoryjnie powinny być niemutowalne, chociaż zdecydowanie lepiej, gdy są. Nie jest to reguła, której dzielnie bym bronił, ale jeśli nie potrzebuję obiektu modyfikować w trakcie cyklu jego życia to na to nie pozwalam. Stąd też prywatne pola dużo bardziej mi odpowiadały. W tym podejściu na siłę doszukałbym się zalet, za to wad nie.

Najnowsza wersja, PHP 8.1 zmieniła reguły gry. Co takiego się stało? Wprowadzone zostało słowo kluczowe readonly, pozwalające określić pola klasy jako tylko do odczytu. Od tej pory wszelkiego rodzaju proste obiekty deklaruję właśnie w ten sposób – pole publiczne tylko do odczytu.

<?php

declare(strict_types=1);

namespace Koddlo\Application\Command\Client;

final class CreateClientCommand
{
    public function __construct(
        public readonly string $id,
        public readonly string $email,
        public readonly string $firstName,
        public readonly string $lastName
    ) {
    }
}

Widzicie jak wygląda taki obiekt. Zdecydowanie to rozwiązanie do mnie przemawia. Istnieje jednak kilka haczyków, na które należy zwrócić uwagę, tak żeby używanie readonly stało się to klarowne.

Pole oznaczone jako tylko do odczytu można ustawić jednokrotnie. W powyższym przykładzie będzie to za pomocą konstruktora. Co więcej, ta konstrukcja przeznaczona jest tylko dla pól publicznych – dla innych zresztą chyba i tak nie miałaby sensu.

Co ciekawe, musi być określony typ pola – nie przejdzie składnia typu: public readonly $id. Zawsze można to obejść używając public readonly mixed $id. Od razu dodam, że nie należy tego nadużywać, ale nie twierdzę, że nigdy się nie przyda. Na przykład reprezentacja żądania opisującego dane wejściowe jest świetnym przykładem, gdzie najpierw trzeba zezwolić na dowolne dane, by następnie sprawdzić ich poprawność.

Jak więc zachowa się ustawienie tylko do odczytu dla konkretnych typów? Skalarne bez problemu. Co ciekawe działają też tablice. Istnieje więc możliwość jednokrotnego przypisania tablicy do pola i następnie nie ma możliwości jej podmiany, ani też modyfikacji w ramach tego obiektu. Inaczej ma się sprawa przy referencjach do innych klas. Tutaj pilnowane jest jednokrotne ustawienie, ale sam obiekt (jeśli na to pozwoli) może zostać zmodyfikowany. To oczywiście za sprawą tego, że w PHP obiekty są obsługiwane przez referencje. Zadziała więc składnia: public readonly Client $client na takiej zasadzie, że nie można przypisać innej instancji Client, natomiast uda się edytować obiekt$client.

Kolejna sprawa jest taka, że nie można do właściwości readonly przypisać wartości domyślnej. Chyba, że dla metody, czyli w ten sposób: public function __construct(public readonly string $id = 'default'). Oczywiście, tej wartości nie będzie można już zmienić, gdyż zostanie jednorazowo ustawiona na default.

Nie da się też ustawić wartości przez bezpośrednie odwołanie – musi to być przez konstruktor lub metodę. Nie zadziała więc $client->firstName = 'Krystian', za to zadziała $client->setName('Krystian'). Nie uda się jednak kolejne wywołanie tego settera – logiczniejsze jest więc zrobienie tego przez konstruktor.

Pola typu readonly nie da się zniszczyć za pomocą unset(). A jak zachowa się tego rodzaju pole w przypadku dziedziczenia? Jeżeli klasa rodzica zadeklaruje readonly, to w klasie potomnej nie da się tego zmienić.

Oczywiście, temat ten nie jest głównym sporem w branży. Chciałem jednak pokazać, dlaczego wprowadzenie pól tylko do odczytu jest bardzo dobrym ruchem oraz gdzie ma to sens. Mam nadzieję, ze ten materiał trochę to rozjaśnił i pozwoli Wam na swobodne używanie tego rozwiązania. Ja je bardzo polubiłem i regularnie używam.

Krystian Żądło
Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan angielskiej piłki nożnej, dobrego humoru oraz podcastów.