O getterach i setterach – podsumowanie dyskusji

Jakiś czas temu, zainspirowany artykułem Giorgio Sironiego – “How to remove getters and setters” rozpocząłem dyskusję na forum goldenline na temat przydatności getterów i setterów. Dyskusja była dość burzliwa, jak to zawsze ma miejsce, gdy próbuje się naruszać dogmaty. Jednak wszystko przebiegło w na tyle przyjaznej atmosferze, że nie skończyło się na “flejmłorze” i padło kilka interesujących argumentów. Teraz nadszedł czas by z perspektywy czasu podsumować tą dyskusje i przedstawić wnioski, do jakich doszedłem po jej zakończeniu.

Dla tych, którym się nie chce czytać artykułu, szybki szkic problematyki – Giorgio wysunął śmiałą tezę, że stosowanie “getterów i setterów” psuje enkapsulację, która jest jedną z podstaw programowania zorientowanego obiektowo. W swoim artykule opisuje on sposoby na całkowite pozbycie się tych często używanych artefaktów.

Jako, że zgadzam się z główną tezą artykułu Giorgia, w dalszej części artykułu zamierzam zaprezentować przypadki w których użycie getterów bądź setterów jest szkodliwe, wraz z uzasadnieniem i przykładowym rozwiązaniem problemu. Pokaże również, przypadki w których zastosowanie tych konstrukcji ma sens. Abyśmy się jednak dobrze zrozumieli, najpierw moja definicja – czym są gettery i settery.

Jako getter i setter, rozumiem metody klasy, zwykle nazwane wg wzoru “getFoo()”, “setFoo()”, które “mapują się 1:1” z odpowiadającymi im polami klasy. Przykład:

class Bar {

    private $_foo;

    public getFoo(){
        return $this->_foo;
    }

    public setFoo($foo){
        $this->_foo = $foo;
    }

}

Przypadki problematyczne:

1.Setter, który wpływa na obiekty kolaborujące klasy.

Wyobraźmy sobie, że mamy sobie taką oto klasę do obsługi tabeli w bazie danych:

class Foo_Table {

    private $_db;

    public function setDb($db){
        $this->_db = $db;
    }

    public function getDb(){
        return $this->_db;
    }

    public function fetchAll(){
        //kod
    }

}

Przypomina ona z grubsza klase Zend_Db_Table_Abstract i jest to właściwe skojarzenie (różnica polega na obecności statycznego settera). . Zobaczmy teraz jakie problemy generuje zastosowanie settera:

//gdzies w kodzie tworzymy obiekt $fooTable
$db1 = new PDO($dsn, $username, $passwd, $options);

$fooTable = new Foo_Table;
$fooTable->setDb($db1);

//inne miejsce w kodzie
$fooTable->fetchAll(); //zwraca array(1,2,3);

//inne miejsce w kodzie
$db2 = new PDO($dsn2, $username, $password,$options);
$fooTable->setDb($db2);

//inne miejsce w kodzie
$fooTable->fetchAll(); //zwraca array(4,5,6) - inna baza danych

Jak widać na wyżej załączonym kodzie, z powodu zastosowania settera, można w międzyczasie zmienić obiekt adaptera bazy danych, z którego korzysta klasa tabeli. Konsekwencją tego jest mniejsza przewidywalność klasy – w drugim przypadku dostajemy inny wynik. W przypadku Zend_Db_Table_Abstract sytuację pogarsza fakt, zastosowania statycznego settera, co wielokrotnie zwiększa możliwości zmiany adaptera i nieprzewidywalność takiego kodu. Łatwo usunąć ten problem poprzez likwidację settera i zastosowanie wstrzykiwania obiektu połączenia z bazą danych poprzez konstruktor.

2. Rozpełzanie się kodu

Dziwna nazwa, ale już tłumacze o co chodzi. Zostawmy na chwilę poprzedni przykład z tabelą, żeby nie było, że przyczepiłem się do Zend_Db. Kolejnym przykładem będzie klasa reprezentująca użytkownika, oczywiście dla potrzeb przykładu została skrócona do minimum:

class User {

    private $_birthday; //unix timestamp

    public function getBirthday(){
        return $this->_birthday;
    }

    public function setBirthday($birthday){
        $this->_birthday = $birthday;
    }
}

Załóżmy teraz, że potrzebujemy gdzieś na stronie wyświetlić ile nasz użytkownik ma lat. Jestem przekonany, że wiele osób zrobi tak:


$user = new User;
$yearsDifference = time() - $user->getBirthday();
$yearCount = $yearsDifference/(365* 24 * 3600); // 365*24*3600 - ilość sekund w roku

W czym leży problem ? Problem leży w duplikacji kodu, który ze względu na swoją prostotę jest często kopiowany to tu, to tam. Powoduje to właśnie jego “rozpełzanie” się po całym systemie.

Co więcej, czasami zdarza się, że chcemy zmienić implementację właściwości klasy (timestamp -> DateTime ->string). Teoretycznie getter nas przed tym chroni, bo możemy sobie zrobić konwersję właśnie w nim przy takiej zmianie. Natomiast gdy stosujemy metody w rodzaju zaprezentowanego niżej “howOldAreYou” i nie stosujemy klasycznych getterów, to nie musimy się martwić w ogóle o takie sprawy, bo nie eksponujemy w żaden sposób wewnętrznej implementacji.

Przykład jest dość banalny, natomiast łatwo sobie wyobrazić sytuację, w której chodzi o bardziej skomplikowane rzeczy. Naczelna zasada, jaką kieruję się w takich sytuacjach jest taka, że obiekt powinien skupiać wszystkie funkcjonalności, które są bezpośrednio z nim związane. Przy okazji, dostajemy bardziej semantyczny kod. Refactoring:


class User {

    private $_birthday;

    public function howOldAreYou(){
        $yearsDifference = time() - $this->_birthday;
        return $yearsDifference/(365*24*3600);
    }

    public function areYouMature(){
        if($this->howOldAreYou() >= 18){
             return true;
        }
        return false;
    }
}

Sensowne zastosowania:

1. Setter ustawiający właściwość nie będącą obiektem kolaborującym

Wróćmy do naszej klasy User. Logicznie rzecz biorąc, by metody howOldAreYou i areYouMature mogły działać, potrzebne jest by pole “birthday” miało jakąś wartość. By jej dostarczyć możemy użyć konstruktora i/lub settera. Zastosowanie settera w tym przypadku ma sens, ponieważ jak to zwykle bywa, w większości aplikacji istnieję konieczność edycji danych. Mamy więc:


class User {

    private $_birthday;

    public function __construct($birthday){
        $this->_birthday = $birthday;
    }

    public function changeBirthday($birthday){
        $this->_birthday = $birthday;
    }

    public function howOldAreYou(){
        $yearsDifference = time() - $this->_birthday;
        return $yearsDifference/(365*24*3600);
    }

    public function areYouMature(){
        if($this->howOldAreYou() >= 18){
             return true;
        }
        return false;
    }
}

Patrząc na kod, zwróciliście pewnie uwagę, że nie nazwałem settera klasycznie “setBirthday” ale “changeBirthday”. Jest to taka moja fanaberia – uważam, że jest to bardziej czytelne i lepiej oddaje to, co wykonuje ta metoda. Co do tego przypadku jest jeszcze jedna, kwestia do rozważenia. Załóżmy, że klasa User posiada więcej pól:


class User {

    private $_birthday;
    private $_firstName;
    private $_lastName;

    public function howOldAreYou(){
        $yearsDifference = time() - $this->_birthday;
        return $yearsDifference/(365*24*3600);
    }

    public function areYouMature(){
        if($this->howOldAreYou() >= 18){
             return true;
        }
        return false;
    }
}

Czy pisanie osobnego settera do każdego pola ma sens ? Oczywiście, jeżeli mamy sytuację, gdy rzeczywiście gdzieś w kodzie zmieniamy pojedyncze dane – jak najbardziej. Jednak zwykle odbywa się to tak, że dostajemy z formularza cały array danych, dlaczego więc nie zrobić tak:


class User {

    private $_birthday;
    private $_firstName;
    private $_lastName;

    public function howOldAreYou(){
        $yearsDifference = time() - $this->_birthday;
        return $yearsDifference/(365*24*3600);
    }

    public function areYouMature(){
        if($this->howOldAreYou() >= 18){
             return true;
        }
        return false;
    }

    public function update(array $data){
        $this->_birthday = $data['birthday'];
        $this->_firstName = $data['firstName'];
        $this->_lastName = $data['lastName'];
    }
}

Dzięki dodaniu metody update my oszczędzamy sobie pisania pojedynczych setterów, jak i również osoba korzystająca z tej klasy, nie musi ich wszystkich potem wykorzystywać.

To by było na tyle z “sensownych zastosowań”. Być może wielu z Was, tezy, które zawarłem w artykule wydadzą się herezją. W końcu wiele bibliotek zarówno dla PHP jak i dla innych języków, pisanych jest według filozofii getterowo-setterowej. Niektóre IDE mają nawet różne wspomagacze tej praktyki, jak automatyczna generacja getterów i setterów dla wszystkich pól w klasie. Osobiście uważam, że jest to bezmyślność i pociąga za sobą więcej szkody niż pożytku. Czyniąc nasz kod mniej wewnętrznie spójnym oraz mniej przewidywalnym. Dlatego wszystkim polecam ten sposób kodowania – jeżeli piszecie obiektowo. Pamiętajcie, że sytuacje w rodzaju:


class User {

            private $_birthday;
            private $_firstName;
            private $_lastName;

            public function getBirthday() {
                return $this->_birthday;
            }
}

function howOldIsUser(User $user) {
            $birthday = $user->getBirthday();
            $yearsDifference = time() - $birthday;
            return $yearsDifference / (365 * 24 * 3600);
}

To tak na prawdę kod proceduralny, gdzie User to zwykła struktura danych znana np. z języka C wyrażona tutaj przy użyciu słowa kluczowego “class”. Nie ma oczywiście nic złego w stosowaniu stylu proceduralnego, o ile robi się to świadomie. Więc pamiętaj programisto – najpierw pomyśl, potem koduj :). Zapraszam do komentowania.

  1. Crozin says:

    Jedno co mnie denerwuje w dyskusjach przeciwników i zwolenników getterów i setterów to to że argumenty przeciw bazują na błędnym kodzie. Dlatego też te wszystkie dyskusje powinny być zmienione w “jak poprawnie używać getterów i setterów”.

    Jeżeli ktoś projektuje sobie klasę, dodaje 10 właściwości niepublicznych po czym z automatu w IDE klika “Generate getters and setters” to oczywiście musi prowadzić to złamania zasady enkapsulacji.

    Jeszcze chciałem zwrócić uwagę na to, że część wpisu traktuje o problemie, który sam wytworzyłeś. 😉 Chodzi o nazewnictwo. Stwierdziłeś, że “changeBirthday” jest bardziej czytelne od “setBirthday”. Otóż nie. Bo powinno być “setDateOfBirth”. Podobne problemy są z “areYouMature” i “howOldAreYou”, gdzie powinno być kolejno “isMature” i “getAge”. W dodatku powinno są raczej starać by metody zwracające coś swoją nazwą sugerowały, że coś zwracają. “howOldAreYou” sugeruje, że metoda **sama** coś powie, a nie zwróci odpowiedź.

    Podsumowując, publiczny interfejs dla User:
    getDateOfBirth()
    setDateOfBirth(DateTime dateOfBirth)
    getAge()
    isMature()

    – wszystkie problemy rozwiązane przy pomocy poprawnie wykorzystanych getterów i setterów, które wcale nie muszą ograniczać się do mapowania 1:1. Inaczej nie miałby sensu.

  2. Śpiechu says:

    Dodawanie do wszystkich pól klasy setterów i getterów z automatu uważam za wariactwo. Część pól pełni funkcję wyłącznie pomocniczą, a ich zmiana musi być wywołana pośrednio.
    Ja zazwyczaj robię tak, że jeżeli jakaś klasa nie może w miarę normalnie funkcjonować bez jakichś danych to wymuszam to w konstruktorze. Z kolei jeżeli da się ustawić jakieś wartości domyślne, to wtedy należy zastanowić się nad setterami.
    Marzy się przeciążanie konstruktorów marzy…

  3. Piotr says:

    Wkradł się błąd do obliczeń. Jeden rok to 365 dni * 24h * 3600s. Zapomniałeś o godzinach 😉

  4. Adawo says:

    Będę czepliwy: A w metodzie areYouMature nie wystarczyło by po prostu “return $this->howOldAreYou() >= 18” ?

  5. Artur Świerc says:

    Sam się ostatnio zastanawiałem, jak najlepiej zastąpić akcesory/mutatory. Rozwiązanie wstrzykiwania danych poprzez konstruktor może być, ale co jeśli mamy baaardzo wiele pól? Wtedy trzeba by stosować tablicę.

    Tablica nie jest wg mnie rozwiązaniem eleganckim, idą za tym problemy ze znaczną ilością warunków:
    – mając setter dla jednego parametru, mogę w metodzie umieścić walidację tylko dla tego podawanego parametru – czysto i ładnie.
    – mając metodę do której wrzucam tablicę muszę sprawdzić wszystkie podane w niej parametry (a przynajmniej większość), prócz tego dochodzą warunki na to czy w ogóle parametr podano! Przecież chciałbym np zaktualizować tylko parę wartości, a nie wszystkie.

    Drugi problem jaki napotkałem, to uzupełnianie encji danymi z repozytorium. Jeśli ktoś używa doctrine2 to problem z głowy, schodki zaczynają się gdy używamy Zend_Db. Encja z polami prywatnymi, jak je uzupełnić w prosty sposób nie używając mutatorów?

    Oczywistym jest, że chciałbym aby moja encja, którą chcę pobrać miała uzupełnione wszystkie dane. Najprostszym byłoby skorzystać z NetBeans’owej funkcji towrzenia z automatu metod set/get dla wszystkich pól – jak sam napisałeś, jest to głupie rozwiązanie i narusza enkapsulacje.

  6. Crozin says:

    Jak już @Śpiechu dobrze zauważył w konstruktorze podajemy jedynie argumenty **konieczne** do prawidłowego działania obiektu. Obiekt Kwadrat będzie wymagał podania długości boku, obiekt Obywatel będzie wymagał podania imienia, nazwiska oraz numeru PESEL itp.

    Jeżeli nagle dochodzi do tego, że masz 15 argumentów do przekazania w konstruktorze najprawdopodobniej coś zrypałeś i powinieneś obiekt podzielić na mniejsze. Jednak czasami dochodzi do czegoś takiego i wtedy możesz:

    1. Przekazać tablicę – średnie rozwiązanie, bo trzeba 15 razy dać isset, instanceof itd.
    2. Dodać settery, a we wszystkich metodach obiektu na początku sprawdzić czy aby na pewno jest on w pełni zainiciowany: http://ideone.com/cwmdB
    3. Stworzyć “struktury”, które nieco ułatwiają pracę względem czystych tablic: http://ideone.com/ICF1g (słaby przykład bo powinno to być podzielone na mniejsze, normalne obiekty)

  7. @crozin – słuszna uwaga z dzieleniem obiektu na mniejsze – twoje rozwiązanie nr.3 według mnie jest najczystsze – btw. taki obiekt nazywa się DTO (data transfer object) i tutaj szkoda, że php natywnie nie wspiera takich łysych typowanych struktur danych.

    Jeszcze jedna uwaga co do twojego wcześniejszego komentarza – chodzi o metodę “getAge” – wg mnie zastosowanie prefiksu “get” i traktowanie w ogóle tego jako gettera jest nie na miejscu, ponieważ wiek zawsze obliczamy, a prefix “get” sugeruje, że jest to część stanu obiektu. Poza tym uważam, że getter na samo pole “birthday” jest zupełnie nie potrzebny – tzn. jeżeli rzeczywiście taką rzecz gdzieś prezentujemy to być może, natomiast najczęściej takie informacje pobierane są zbiorczo – wiek, imię, nazwisko etc i dla takich sytuacji tworzy się metodę, która zbiera takie informacje i je zwraca, zamiast tworzenia setek getterow i setterow (zasada minimalnego interfejsu).

  8. Crozin says:

    @Wojciech Soczyński: Co zatem proponujesz zamiast “getAge”? Prefiks “get” wcale nie jest zarezerwowany wyłącznie dla getterów. Używa się go tam gdzie jest on wręcz naturalny. Dla metody zwracającej wiek lub rzucającej wyjątkiem / zwracającej nulla w przypadku gdy nie da się ustalić wieku, taka nazwa jest po prostu idealna.

  9. Artur Świerc says:

    @Crozin rozwiązanie trzecie z DTO wydaje mi się najlepsze, często stosuję jako rozbijanie większych encji. Problem jednak polega na tym, że w efekcie końcowych jakoś do danych trzeba dotrzeć i zwykle zabawa kończy się na getach.

    @Wojtek tutaj zgadzam się z Crozinem, gety nie muszą zwracać suchych danych. W projektach Javowych z powodzeniem stosowane jest nazewnictwo get/set/is/has/add – po “nagłówkach” takiego API od razu widać, których metod mogę używać i do czego służą.

  10. @Artur: w kontekście rozbijania – jest taka koncepcja w DDD jak aggregate root. Co do dotarcia do danych, to najlepsza strategia to poprosić obiekt o wykonanie operacji zamiast pobierać od niego dane. Zresztą polecam artykuł od którego wszystko się zaczęło, jest tam opisane kilka technik. Co do Javy to nie zachwycał bym się tak, ponieważ np. Java Beans są sztandarowym przykładem złej architektury i to właśnie przez nadużywanie get/set.

  11. Crozin says:

    @Wojciech Soczyński: Trzeba pamiętać o tym, że nie można na obiekt zwalać zbyt wiele czyli nie wszystko uda Ci się obliczyć wewnątrz obiektu. Skoro nie da się wszystkiego wyliczyć wewnątrz trzeba udostępnić jakiś sensowny mechanizm pobierania danych z obiektu. Nie muszą to być “surowe gettery”, bo część danych może zostać przetworzona jeszcze w obiekcie, ale coś tam zwrócone zostać musi.

    Trzeba zawsze zachować umiar przy tworzeniu metod dostępowych jak i przy dodawaniu kolejnych zadań do obiektu.

  12. @Crozin – od takich rzeczy mamy m.in strategy pattern. Wiadomo, że jest kwestia segregacji odpowiedzialności obiektów. Nie mniej jednak, czym więcej związanego ze sobą kodu trzyma się obok siebie, tym lepiej dla wewnętrznej spójności.

  13. @Wojciech:
    Sądzę, podobnie jak część przedmówców, że getCokolwiek nie musi zwracać jakiejś już wyznaczonej wartości. Równie dobrze może być ona dopiero wczytana z bazy danych lub obliczona.

    Coś podobnego na temat:
    http://www.yarpo.pl/2011/04/20/gettery-i-settery-w-php/

  14. @patryk: czyli tak naprawdę wg ciebie każda metoda, która coś zwraca jest getterem ? Bez sensu…, proponujesz więc, żeby np w klasach Zend_Db, fetchAll zamienić na getAll etc ?

  15. Każda, która zwraca informacje o stanie obiektu.

  16. @patryk – cytuje “Równie dobrze może być ona dopiero wczytana z bazy danych lub obliczona.” więc ?

  17. Zależy od tego, gdzie trzymasz dane. Jestem w stanie sobie wyobrazić system, choćby prosta księgę gości, gdzie jest możliwość pobrania wpisu: $wpis->getTreść().

    Zakładając, ze może to być wczytywanie pojedyńczo wpisów, można odczytać tę wartość dopiero w momencie wywołania.

    niezbyt rozumiem, o co Tobie chodzi. Będę wdzięczny za jakieś rozjaśnienie co masz dokładnie na myśli :)

  18. Mam na myśli to, że powiedziałeś, że getterem jest każda metoda która zwraca informacje o stanie obiektu, natomiast wcześniej napisałeś, że równie dobrze może być wczytana z bazy lub obliczona, czyli sobie zaprzeczyłeś, bo wczytanie z bazy nie ma nic wspólnego ze stanem obiektu. Co więcej uważam, że myślenie w jakikolwiek sposób o “stanie obiektu” będąc konsumentem jego API, jest błędnym założeniem z perspektywy OOP i łamie enkapsulacje, bo w końcu nie interesuje nas stan obiektu, tylko to co on może dla nas zrobić…

  19. możemy mięć klasę abstrakcyjną:

    abstract class Comment
    {
    public function getContent();
    public function getAuthor();
    static public function getInstance($type) { ... } 
    ...

    oraz dziedziczące po niej np.:

    class CommentTxt extends Comment

    implementująca tamte metody.

    Na poziomie API [Comment] masz rację. tworząc kod CommentTxt warto chyba jednak spojrzeć na to trochę niżej.

    Comment::getContent jest getterem moim zdaniem.

    A może być odczytany z bazy danych czy pliku tekstowego. Choć równie dobrze na dzień dobry można byłoby listę takich obiektów wypełnić wszystkimi komentarzami. Czasem [często IMO] może być to niepotrzebne.

    Jeśli w którymś momencie się mylę, z chęcią usłyszę, w którym :)

    pozdrawiam :)

  20. Hm, chętnie odpowiem na pytanie, o ile wyjaśnisz mi czym jest klasa CommentTxt ? Czy to jest adapter do jakiegoś sposobu zapisywania informacji ?

  21. Jest to klasa, która implementuje metody odziedziczone z Comment, wykorzystując do przechowywania danych pliki txt.

  22. Ok, tutaj widzę pierwszą rzecz w której różnimy się podejściem i która poniekąd też wpływa na kwestię get/set. U mnie jest klasa Comment, która reprezentuje pojedynczy komentarz, natomiast sposób jej zapisu obsługiwany jest przez osobny byt – repozytorium. Takie repozytorium w zależności od implementacji zapisuje dane na odpowiednim medium. Dzięki takiemu podejściu kod jest bardziej elastyczny.

    Co do getterów w twoim przypadku to rzeczywiście wyciągają one dane z bazy. W moim nie muszą tego robić bo obiekt wyciągnięty z repozytorium jest już w tym momencie kompletny.

  23. Podejście ciekawe.

    Jednak czy zawsze potrzebne? Sam mam chwilami obawy, czy nie przesadzam z “pięknem” kodu. Czy do prostego systemu komentarzy potrzeba aż tak dużego skomplikowania? :)

    W “czystym kodzie” dobrze ujął to autor. Gdy ma taką sytuację, zadaje sobie pytanie – czy to będzie trudno przerobić, kiedy okaże się niezbędne? Jeśli odpowiedź brzmi nie, to zachowuje względną prostotę.

    Nie zawsze mi to wychodzi, ale się staram :)

  24. Tzn. wiadomo, że trzeba dostosować środki do problemów. Dlatego na końcu napisałem, że najważniejsze zawsze jest myślenie. Wszystko zależy od skali problemu, ilości koderów, klienta etc. Ja staram się na swoim blogu eksplorować różne podejścia ponad te utarte ścieżki, którymi większość podąża.

  25. filip says:

    Jeżeli geterów i seterów jest sporo, oraz tworzenie jakichś specyficznych metod do powiedzmy wyrzucenia tych danych, lepiej jest zrobić jednego getera, który poprzez parametr mu podany wyświetli stosowne informacje. U siebie w klasie user robię to mniej więcej tak że mam jednego getera i jednego setera, ciągła modyfikacja klasy bo dodałem jedno pole do bazy byłaby bezsensu. W get mogę podać nazwę pola, np “name”, lub tablice array(‘name’, ‘id’, ‘password’) i tym podobne. Niemal identycznie mam w przypadku setera, w którym podaję tablice z polem oraz informacjami które muszą zostać zaktualizowane.

    Robić dziesiątki seterów, jest bezsensu, ale jeżeli ma być jakiś uniwersalny geter/seter to nie widzę problemu żeby taki stosować :))

  26. Po to powstał styl obiektowy, żeby nie martwić się wewnętrzną implementacją obiektu. Wewnętrzną implementacją w tym przypadku są atrybuty, które trzymasz w bazie danych. Przykład o uniwersalnym getterze / setterze jest kanonicznym przykładem przeciwnego podejścia – eksponowania wewnętrznego stanu obiektu. Czemu nie zrobić metod w stylu np. daneDoFormularza(), daneDoCzegosInnego() ? W takim przypadku, jeżeli dodasz jakieś pole do bazy i chcesz, żeby te dane się gdzieś pojawiły to modyfikujesz odpowiednie metody, zamiast składać przy pomocy getterów i setterów te same dane w wielu miejscach w systemie…

  27. P. says:

    Wojciechu!

    Obawiam się, że nie do końca rozumiesz dlaczego stosuje się settery/gettery. A wystarczy czasami wyjrzeć poza świat PHP :)

    Jak Crozin słusznie zauważył, interfejs użytkownika, w języku PHP, będzie wyglądał następująco:
    getDateOfBirth()
    setDateOfBirth(DateTime dateOfBirth)
    getAge()
    isMature()

    Teraz zobaczmy jak ten sam interfejs wyglądałby w C#:

    public interface IUser {
        DateTime DateOfBirth { get; set; }
        int Age { get; }
        bool IsMature();
    }
    

    Oraz implementacja tegoż interfejsu:

    public class User : IUser {
        // Odpowiednia zmienna, przechowująca dane, jest generowana automatycznie
        public DateTime DateOfBirth { get; set; }
        
        // Zamiast tego powyżej można też napisać bardziej tradycyjnie
        private DateTime dateOfBirth;
        public DateTime DateOfBirth {
            get { return this.dateOfBirth; }
            set { this.dateOfBirth = value; }
        }
        
        public int Age {
            get {
                return (DateTime.Today - this.DateOfBirth).Days / 365;
            }
        }
    
        public bool IsMature()
        {
            return this.Age >= 18;
        }
    }
    

    Ojoj, co my tu mamy? Nagle okazuje się, że tradycyjne metody set i get są zbędne!?

    Idea jest prosta, chcemy publicznie udostępnić pewne dane (DateOfBirth, Age), bez jednoczesnego odsłaniania szczegółów implementacji. W ten sposób, możemy dowolnie zmieniać implementację, pod warunkiem tylko, że zachowamy publiczny kontrakt (interfejs), na który się umówiliśmy.
    Klasyczny przykład enkapsulacji!

    PHP czy Java nie posiadają bezpośredniego wsparcia dla getterów i setterów, dlatego sami musimy dodać odpowiednie metody (getDateOfBirth, setDateOfBirth, getAge).
    Podkreślę, metody powstaną tylko dla danych, które chcemy udostępnić publicznie, nie ma tutaj automatycznego “mapowania 1:1”.

    Mam nadzieję, że choć troszeczkę udało mi się rozjaśnić :)

  28. Drogi “P”, doskonale rozumiem dlaczego stosuje się gettery i settery i wyobraź sobie, że znam również inne języki, chociażby Scalę, w której dostępne są podobne mechanizmy, jak ten przedstawiony przez Ciebie z C# – takie podejście nazywa się Uniform Access Principle i wspomniałem o nim przy okazji jednego z artykułów o Scali – http://blog.wsoczynski.pl/2011/03/10/jezyk-scala-dla-programistow-php-pt-2-klasy/ .

    Tak czy inaczej ja widzę, że nie przeczytałeś dogłębnie mojego wpisu – po pierwsze, moja główna obiekcja leży przy stosowaniu getterów i setterów w przypadku manipulacji obiektami kolaborującymi z danym obiektem co wydatnie zmniejsza przewidywalność kodu.

    Po drugie uważam, że gettery dla pojedynczych pól powodują “rozpełzanie się kodu”, czyli to, że bierzemy z jakiegoś obiektu dane za pomocą kilku getterów i później robimy z nimi “coś” w oderwaniu od niego. Prowadzi to do anemicznych obiektów, które są tylko typowanymi kontenerami na dane (typowe podejście proceduralne), zamiast, bytami, które robią konkretne rzeczy.

  29. P. says:

    Drogi Wojciechu!

    Nie rozumiem dlaczego spychasz dyskusję na zupełnie inne tory. Co wspólnego ma podany przeze mnie fragment kodu z Uniform Access Principle? Czy C# jest zgodny z UAP?
    Dla ułatwienia zacytuję fragment Twojego wpisu:

    Przy okazji omawiania klas w Scali warto zauważyć jedną rzecz – metody klasy nie muszą być wywoływane z użyciem nawiasów. Ten fakt wynika z zastosowania w języku zasady „Uniform access principle”, która mówi, że dostęp do pól klasy i jej metod powinien być jednakowy.
    

    Apeluję, nie używajmy pojęć, których do końca nie rozumiemy. W cytowanym fragmencie zdajesz się prawidłowo definiować UAP, ale nie wiedzieć dlaczego, teraz używasz tego pojęcia w kontekście akcesorów C#, co nie powinno mieć miejsca. Słyszysz, że dzwonią, ale nie wiesz w którym kościele 😉

    To tyle tytułem wstępu, mam nadzieję, że nie będziemy musieli do tego wracać :) Chciałbym też podkreślić, że przeczytałem Twoją notkę, a wcześniej udzielona przeze mnie odpowiedź, miała na celu pośrednie odniesienie się do Twoich tez. Jest to bardzo ważne w zrozumieniu dlaczego postawione przez Ciebie problemy są raczej pozorne.

    “Obiekty kolaborujące”

    Pominę sensowność przytoczonego przez Ciebie kodu źródłowego. Jeśli na jakimś etapie uznałeś, że wielokrotne przypisywanie obiektu bazy danych prowadzi do pomyłek, to świetnie! Wykorzystaj do tego celu istniejący setter – sprawdzaj w setDb czy obiekt bazy danych został wcześniej ustawiony. Jeśli tak – wyrzuć wyjątek.

    Alternatywnie, mógłbyś też całkowicie zrezygnować z settera, zostawiając jedynie getter (bo zakładam, że jednak chciałbyś mieć dostęp do obiektu bazy danych). Przecież nie zawsze gettery i settery idą w parze – pokazałem to w moim poprzednim komentarzu!

    Wreszcie trzecia możliwość – co opisałeś w swojej notatce – całkowite pozbycie się akcesorów, i przekazanie (wcale niekoniecznie “wstrzyknięcie” :)) obiektu bazy danych w konstruktorze. To może być akceptowalne rozwiązanie, ale pod warunkiem, że zastosujesz je świadomie, wiedząc, że publicznie nie będziesz miał dostępu do tego co przed chwilą przekazałeś.

    “Rozpełzanie się kodu”

    Jak powinna wyglądać klas User już zaprezentowałem. Nie ma tutaj mowy o żadnym “rozpełzaniu się”.

    W swojej krucjacie przeciwko akcesorom nawet nie zauważyłeś, że podana przez Ciebie implementacja (poniżej), uniemożliwia zapytanie użytkownika o datę urodzenia! No chyba nie to Ci chodziło?!

    class User {
        private $_birthday;
    
        public function __construct($birthday){
            $this->_birthday = $birthday;
        }
    
        public function changeBirthday($birthday){
            $this->_birthday = $birthday;
        }
    
        public function howOldAreYou(){
            $yearsDifference = time() - $this->_birthday;
            return $yearsDifference/(365*24*3600);
        }
    
        public function areYouMature(){
            if($this->howOldAreYou() >= 18){
                 return true;
            }
            return false;
        }
    }
    
  30. Drogi P. – w przypadku UAP ma wiele wspólnego, akcesory z C# to po prostu jedna z jego implementacji. UAP nie sprowadza się tylko do wywoływania metod obiektu bez nawiasów. Polecam chociażby artykuł w angielskiej wikipedii.

    Co do reszty, jest fundamentalna różnica w naszym podejściu do programowania. U mnie obiekt wykonuje jakieś konkretne zadania. Dlatego też, załóżmy, że mamy np. menedżera encji takiego jak np. w Doctrine, jego zadaniem jest utrwalanie i wyciąganie encji z db. Logiczne jest więc, że potrzebuje obiektu kolaborującego – połączenia z db. Wstrzykuje go przez konstruktor. Czy potrzebuje gettera albo settera na obiekt połączenia do db ? Oczywiście, że nie. Po co miałbym z powrotem wyciągać z menedżera encji obiekt bazy danych albo go zmieniać ? Wszystkie czynności które chcę wykonać przy pomocy menedżera encji powinny mi umożliwiać jego metody. Jeżeli natomiast chce się połączyć z inną bazą tworzę nową instancję menedżera encji z innym obiektem db. Taki kod jest przewidywalny. Natomiast gdybym miał tam getter i setter na obiekt db to wtedy mógłbym go podmienić gdzieś w kodzie na inny, co otwiera furtkę do różnorakich błędów, albo chociażby pobrać ten obiekt i zamknąć połączenie co skutkuje tym, że menedżer encji nie mógł by działać.

    Co do przykładu z klasą User, celowo nie można uzyskać informacji o dacie urodzenia użytkownika. Dlaczego ? Bo nie jest to w żaden sposób potrzebne. Jeżeli założymy, że wymaganiem systemu jest to, żeby nam podawał informacje ile jakiś użytkownik ma lat albo czy jest pełnoletni to nie ma takiej potrzeby.

    Btw. nie robię żadnej krucjaty przeciwko akcesorom. Pokazuje tylko, jakie są wady i zalety oraz możliwe alternatywy.

    Dodam jeszcze, że jest chyba oczywistą rzeczą, że czym jakiś byt jest bardziej wyizolowany od otoczenia tym łatwiej nad nim zapanować. W przypadku oprogramowania jest dokładnie tak samo – minimalizacja zależności i maksymalizacja abstrakcji (w pewnym stopniu) prowadzi do lepszego kodu.

  31. P. says:

    Drogi Wojciechu!

    Nie chcę się znęcać, ale sam rozpocząłeś temat UAP. To jaka jest definicja UAP, hm?

    Wikipedia? Proszę bardzo:
    “[…] In simpler form, it states that there should be no difference between working with an attribute, precomputed property, or method/query.”

    Definicja z Twojego wpisu? Proszę bardzo:
    “[…] Ten fakt wynika z zastosowania w języku zasady „Uniform access principle”, która mówi, że dostęp do pól klasy i jej metod powinien być jednakowy.”

    Coś tu jest niejasne? W którym zatem miejscu C# implementuje UAP? Czy metodami można posługiwać się jak polami lub atrybutami? Czy można swobodnie zamienić jedno na drugie? Oczywiście, że nie! Jedyne ujednolicenie dostępu istnieje na poziomie pól i właściwości, ale tylko pozornie, bo często nie można zamiast pola użyć właściwości (i odwrotnie).

    Czy jesteś jednym z tych, którzy dozgonnie upierają się przy swoim zdaniu, mimo iż wiedzą, że nie mają racji?

    Nie będę kontynuował przykładu obiektu bazy danych, bo niepotrzebnie skomplikowałeś zagadnienie, które jest proste.

    Co do klasy User – naprawdę chcesz mnie przekonać, że gdzieś na produkcji masz kod, w którym jest setter na datę urodzenia (bo przecież można zmienić tę wartość), ale getter już nie (chociażby w celu wyświetlenia aktualnej wartości na formularzu zmiany daty urodzenia?) 😀

    A gdybyś chciał zrobić prosty test jednostkowy metody changeBirthday? No tak, testów też pewnie nie potrzebujesz 😉

    Jeśli programowałbyś w C#, i miałbyś do czynienia z interfejsem IUser, który wcześniej zaprezentowałem, czy skasowałbyś gettera na właściwości DateOfBirth?

    Zapewne tak, bo dla Ciebie upublicznienie podstawowej informacji (którą zresztą ustawiasz w konstruktorze, i zmieniasz setterem) o obiekcie, jest złamaniem idei enkapsulacji i Twojej autorskiej wizji OOP :)

    Nie “robisz krucjaty przeciwko akcesorom”, tak? To dlaczego ta notatka to podsumowanie dyskusji o nazwie “Pozbycie się getterów i setterów”? 😉

    Natomiast w kilku pierwszych akapitach tego wpisu można przeczytać:

    “[…] Giorgio wysunął śmiałą tezę, że stosowanie „getterów i setterów” psuje enkapsulację, która jest jedną z podstaw programowania zorientowanego obiektowo. […]
    Jako, że zgadzam się z główną tezą artykułu Giorgia, w dalszej części artykułu zamierzam zaprezentować przypadki w których użycie getterów bądź setterów jest szkodliwe, wraz z uzasadnieniem i przykładowym rozwiązaniem problemu.”

    Z ostatniego akapitu Twojego komentarza wnioskuję, że jednak przekonałem Cię, że settery i gettery nie psują enkapsulacji, a dokładnie odwrotnie – są środkiem zapewniającym enkapsulację, tam gdzie to potrzebne :)

  32. I was able to find good information fdom your content.

  1. There are no trackbacks for this post yet.

Leave a Reply

Notify me of followup comments via e-mail. You can also subscribe without commenting.