21 октября 2013 в 01:24

Публичные свойства, геттеры и сеттеры или магические методы? перевод tutorial

PHP*
Как правило, мнения расходятся касательно того, хорошей ли практикой является использование публичных свойств в PHP классах или всё же стоит использовать геттеры и сеттеры (и хранить свойства приватными или защищёнными). Ещё одно, компромиссное мнение, состоит в том, чтобы использовать магические методы __get() и__set().
У каждого из подходов существуют свои достоинства и недостатки, давайте взглянем на них…


Пример использования публичных свойств:
class Foo 
{
    public $bar;
}
 
$foo = new Foo();
$foo->bar = 1;
$foo->bar++;

В данном примере bar является публичным свойством класса Foo. При таком подходе мы можем манипулировать данным свойством как угодно и хранить в нём любые данные.

Достоинства публичных свойств
  • По сравнению с геттерами и сеттерами, нужно печатать на много меньше текста!
  • Код выглядит более читабельным и работать с ним проще, чем с вызовами геттеров и сеттеров.
  • Вызов публичного свойства (вместо set или get) работает быстрее и использует меньше памяти чем вызов метода, но выгода от этого будет почти незаметной до тех пор, пока вы не будете вызывать метод множество раз в длинном цикле.
  • Объекты с публичными свойствами могут использоваться в качестве параметров для некоторых PHP функций (таких как http_build_query).

Недостатки публичных свойств
  • Нет возможности осуществлять контроль за тем, какие именно данные содержатся в свойствах — их очень легко заполнить данными, некорректными для методов этого класса. Например, если класс будет использоваться сторонним разработчиком, отличным от автора класса, то могут возникать проблемы связанные с тем, что он может быть не в курсе (да он и не обязан) внутреннего устройства класса. В конце концов, ни для кого не секрет, что зачастую по прошествии несколько месяцев даже сам автор забывает логику написанного им кода.
  • Если вы хотите использовать публичные свойства в API, то этого нельзя будет сделать используя интерфейс, так как в PHP интерфейсы допускают только определения сигнатур методов.


Пример использования геттеров и сеттеров:
class Foo
{
    private $bar;
     
    public function getBar()
    {
        return $this->bar;
    }
     
    public function setBar($bar)
    {
        $this->bar = $bar;
    }
}
 
$foo = new Foo();
$foo->setBar(1);
$foo->setBar($foo->getBar() + 1);

Свойство bar тут является приватным, в связи с этим, доступ к нему не может быть получен напрямую. Для того, чтобы получить значение свойства, вам придётся использовать метод getBar, или setBar для присвоения свойству значения. Чтобы вы могли быть уверенны в том, что входные данные полностью корректны, данные методы могут включать в себя соответствующий функционал для их валидации.

Достоинства геттеров и сеттеров
  • Используя геттеры и сеттеры вы можете осуществлять контроль за тем, какие именно данные содержатся в свойствах объекта, и отклонять любые некорректные значения.
  • Так же вы можете осуществлять дополнительные операции перед тем, как установить или получить значение свойства (например, если обновление данного свойства должно вызывать некоторое действие, такое как оповещение пользователя).
  • При установке значения, которое является объектом или массивом, вы можете явно указать тип переменной в сигнатуре функции(прим. public function setBar(Bar $bar)). К большому сожалению, PHP не позволяет проделывать тоже самое с типами int и string!
  • Если значение свойства должно получаться из внешнего источника или среды исполнения, вы можете использовать ленивую загрузку данных — таким образом ресурсы, требуемые для загрузки данных, будут задействованы непосредственно во время получения значения свойства. Разумеется, в данном случае нужно соблюдать осторожность, и не следует получать данные из внешнего источника при каждом обращении к свойству. Будет лучше сделать одно обращение к базе данных и заполнить значения всех свойств сразу, чем делать это для каждого в отдельности.
  • Вы можете сделать свойство доступным только на чтение или только на запись, путём создания только геттера или только сеттера.
  • Вы можете добавить геттеры и сеттеры в интерфейс для того, чтобы отобразить их в API.

Недостатки геттеров и сеттеров
  • Для разработчиков, которые используют прямой доступ к свойствам, геттеры и сеттеры кажутся настоящей головной болью! Для каждого свойства нужно определить само свойство, геттер и сеттер; и для того чтобы использовать данное свойство в коде, нужно осуществлять дополнительные вызовы метода — намного легче написать $foo->bar++; вместо $foo->setBar($foo->getBar() + 1); (хотя, конечно, можно добавить ещё один метод $foo->incrementBar();)
  • Как уже отмечалось выше, существуют небольшие дополнительные расходы, затрачиваемые на вызов метода.
  • Имена геттеров и сеттеров принято начинать с глаголов get и set, но данные глаголы так же могут использоваться и в других методах, которые ни коим образом не относятся к свойствам класса.


Пример использования магических геттеров и сеттеров:

class Foo
{
    protected $bar;
 
    public function __get($property)
    {
        switch ($property)
        {
            case 'bar':
                return $this->bar;
            //etc.
        }
    }
 
    public function __set($property, $value)
    {
        switch ($property)
        {
            case 'bar':
                $this->bar = $value;
                break;
            //etc.
        }
    }
}
 
$foo = new Foo();
$foo->bar = 1;
$foo->bar++;

В данном случае свойство bar не является публичным, однако в коде оно используется так, как если бы было публичным. Когда PHP не может найти соответствующего публичного свойства он вызывает соответствующий магический метод (__get() для получения значения, __set() для установки значения). Данный подход может показаться золотой серединой, но у него есть существенный недостаток (см. недостатки ниже!). Следует также отметить, что __get() и __set() методы НЕ вызываются для публичных свойств, и вызываются в случае, если свойство помечено как protected или private и находится за пределами области видимости, или если свойство не определено.

Достоинства магических геттеров и сеттеров
  • Вы можете манипулировать свойствами напрямую (как если бы они были публичными) и сохраняете полный контроль над тем, какие данные хранятся в каких свойствах.
  • Аналогично использованию обычных геттеров и сеттеров, вы можете осуществлять дополнительные операции, когда свойство используется.
  • Вы можете использовать ленивую загрузку данных.
  • Вы можете сделать свойства доступными только на чтение или только на запись.
  • Вы можете использовать магические методы для перехвата всех вызовов к свойствам, которые не были определены и обрабатывать их некоторым образом

Недостатки магических геттеров и сеттеров
  • Недостатком магических методов является то, что они не делают свойство доступным(«видимым»). Для того чтобы использовать или расширить класс, вам необходимо «просто знать» какие свойства он содержит. В большинстве случаев это невозможно (если только вы не один из хардкорных программистов, которые думают что notepad является IDE!), хотя бывают случаи, когда упомянутые выше преимущества, перевешивают это ограничение. Как заметил один из комментаторов, этот недостаток может быть устранён использованием phpDoc тегов @property, @property-read, и @property-write. Круто.


Какой подход использовать
Очевидно, что у геттеров и сеттеров есть ряд существенных преимуществ, и некоторые люди считают, что их стоит использовать всё время (особенно те, у кого прошлое связано с Java!). Но на мой взгляд, этим они нарушают естественное развитие языка, и их излишняя сложность и детализация принуждают меня работать с этим, даже если в этом нет надобности (меня раздражает, когда обычные геттеры и сеттеры НЕ делают ничего, кроме получения и установки свойства). В таких случаях я стараюсь использовать публичные свойства, а геттеры и сеттеры я использую для критических свойств, которые необходимо более строго контролировать, или если в них необходимо использовать отложенную загрузку данных.

Другие альтернативы?
До изучения PHP я использовал C#. В C# все свойства имеют методы доступа, но вам не нужно вызывать их как методы, вы можете манипулировать свойсвами напрямую и соответствующие методы будут вызываться магически. Это в некотором роде это похоже на магические методы __get() и __set() в PHP, однако свойства остаются определены и доступны. Это золотая середина и было бы очень хорошо увидеть аналогичную возможность в PHP.

Печально, но, RFC необходимый для реализации C# подобного синтаксиса определения методов доступа к свойствам не набрал необходимых две трети голосов: wiki.php.net/rfc/propertygetsetsyntax-v1.2 Вот так!
Автор оригинала: Russell Walker
Иван Пантелеев @Yekver
карма
14,0
рейтинг 0,0
Пользователь
Похожие публикации
Самое читаемое Разработка

Комментарии (37)

  • +2
    Магические сеттеры и геттеры не могут быть эквивалентны публичным свойствам. Например:

    Вложенные массивы и Magic Methods

    Нельзя сделать вот так вот:
    $a = 1;
    $foo->bar = &$a;
    
    • НЛО прилетело и опубликовало эту надпись здесь
      • +1
        Валится на этапе интерпретации. 3v4l.org/TOKV0#v525
        • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Магические… В этом весь PHP, а не правильнее называть «Поля» (то что в первом случае) и «свойства» (то что в случае __get __set)?
  • +2
    Жалко wiki.php.net/rfc/propertygetsetsyntax-v1.2 не приняли :(
    • 0
      Да, очень обидно на самом деле.
  • +2
    использование магии увеличивает время работы и уменьшает читаемость кода.
    • +4
      Можете показать пример ухудшения читаемости?
      • –1
        class Human
        {
            private $passport;
            private $age;
        
            public function __get($name)
            {
                if ($this->isPassportPropertyName($name)) {
                    $name = $this->convertToPassportPropertyName($name);
                    $value = $this->passport->$name;
                } elseif (property_exists($this, $name)) {
                    $value = $this->$name;
                } else {
                    throw new \Exception('Oops, there is no such property here!');
                }
            }
        }
        

        vs
        class Human
        {
            private $passport;
            private $age;
        
            public function getPassportIssueDate()
            {
                return $this->passport->issueDate;
            }
        
            ...
        
            public function getAge()
            {
                return $this->age;
            }
        }
        
        • 0
          Да, согласен. Здесь действительно «чёрт ногу сломит».

          С другой стороны так «изощрённо» можно использовать любую языковую фичу. Пожалуй, нужно было задать вот такой вопрос: действительно не существует таких случаев, когда применение магических методов __set и __get не ухудшает (улучшает) читаемость кода?
          • 0
            Ну, в общем-то, я писал в контексте статьи — сравнение public, get() и __get().

            Конечно, как большинство языковых конструкций, __get/__set имеют свои области применения. Но имхо, зачастую они используются не по назначению. Вот пример: gist.github.com/elfet/7081560

            Кстати, интересно было бы послушать поставивших минусы (на сами минусы пофиг, конечно).

            P.S. В том комментарии не стал писать (т.к. разговор шёл именно про геттеры в пределах одного класса), но вообще, конечно, идеальная читаемость достигается в этом случае:
            $vasya->getPassport()->getIssueDate()
            
  • 0
    Однозначного выбора все равно нет.
    Где-то нужно использовать именно сеттеры, где то достаточно (и очень удобно) — использовать магику. А в части случаев вообще достаточно публичного атрибута.

    Тут главное без фанатизма. PHP-кодеры, повсеместно использующие гетеры и сеттеры меня, честно говоря, тоже убивают. А те, кто отказываются от них полностью только из принципа, мол, «геттеры/сеттеры — это моветон» ничем не отличаются от первых.
    • 0
      Имхо, стоит придерживаться одного стиля.

      Меня, например, убивает ситуация, когда в каких-то классах публичные свойства, а в каких-то геттеры. Иногда ещё умудряются сочетать и то, и другое в одном классе.

      Кстати, изначальное использование геттеров всегда сильно упрощает рефакторинг. Скажем, вы решили запретить повторное переназначение имени для пользователя. Если имя у вас было публичным свойством, то вам придётся добавлять геттер и менять логику во всех местах, где это свойство использовалось, в случае геттера — достаточно изменить логику геттера.
      • 0
        А реализуйте ка геттерами это:

        public function __get($name)
        {
        if(isset($this->_attributes[$name]))
        return $this->_attributes[$name];
        elseif(isset($this->getMetaData()->columns[$name]))
        return null;
        elseif(isset($this->_related[$name]))
        return $this->_related[$name];
        elseif(isset($this->getMetaData()->relations[$name]))
        return $this->getRelated($name);
        else
        return parent::__get($name);
        }

        :)
        • 0
          Жесть какая, в смысле «очевидности»
        • 0
          Я вообще не буду это реализовывать так.

          Через некоторое время вы поймёте, почему. И время это приблизится, если вы начнёте использовать тег source, расставлять фигурные скобки там, где надо, стараться не использовать множественных возвратов внутри одного метода и использовать array_key_exists вместо isset.

          Ещё ближе к этому моменту вы подойдёте тогда, когда вместо
          isset(($this->getMetaData()->...)
          

          будете писать:
          $this->getMetadata()->has...();
          


          И совсем близко вы к этому подойдёте тогда, когда поймёте, что в этом магическом __get вы пытаетесь получить некую сущность, которая является конкатенацией множеств атрибутов объекта, различных свойств метаданных и неких свойств родительского класса. Скажем, вы назовёте её «properties». И тогда вы подумаете, что и для метаданных колонки+связи — это некоторые «properties». И т.д.

          И получится что-то вроде этого:
          public function getProperties()
          {
              return array_merge(parent::getProperties(), $this->_attributes, $this->getMetadata()->getProperties());
          }
          public function getProperty($name)
          {
              $properties = $this->getProperties();
              if (!array_key_exists($name, $properties)) {
                  throw new \InvalidArgumentException('There is no [' . $name . '] property here.');
              }
          
              return $properties[$name];
          }
          
          • 0
            Вот только всё равно это не избавит от конфликтов именований. Поэтому я бы вообще не стал вводить такую «property»-сущность, а продолжал бы работать со всеми составляющими отдельно.
          • 0
            На самом деле я не буду так думать, это забота ребят из Yii Framework.
            Однако им удалось создать весьма популярный и один из самых быстрых PHP-фреймворков современности.

            Не думаю, что у вас есть что-то, что может сравнится с ним по популярности. Да, пускай ваш код более удобно читать новичкам. Но кто его будет читать, если он окажется никому не нужным? Кичиться надо результатом, а не теорией и размышлениями «о смысле жизни».

            Ваши примеры явно страдают в производительности перед примером из Yii. C таким же успехом можно писать на Java и не париться. Путь PHP же — это «быстрее, выше, сильнее», а понятный для новичков код — это уже вторичное и зависит от конкретного проекта.
            • +1
              Каким образом всё написанное вами связано с программированием и кодом? Yii — хорошая штука. Но это не делает её идеальной. Тем более при разговоре о коде, который присутствует в первом коммите в гитхаб 5 с лишним лет назад (после этого был лёгкий рефакторинг к упомянутому виду 4 с лишним года назад). В коде класса 2550 строк и 17 контрибьюторов.

              Производительность? Зависит от того, как использовать. В сферическом вакууме оптимизировать нечего, в другом вакууме можно лениво подсчитывать свойства один раз. В случае с ActiveRecord (как в приведённом коде), проще было бы один раз подсчитать (схема не так часто меняется). Но это всё экономия на спичках с учётом, что всё это происходит в ORM.

              В современном мире нужно не только «быстрее, выше, сильнее». Предположим, у вас итеративный процесс. Наговнокодили «быстро, круто, сильно» первую итерацию, а дальше пошли новую работу искать.

              Java-way? Если у вас есть желание и дальше считаться тупоголовыми php-шниками — не вопрос. Я предпочитаю использовать нестрогость PHP для получения некоторых преимуществ перед остальными платформами, вы, видимо, предпочитаете эту нестрогость использовать для говнокода.

              Хороший код нужен не только для новичков, но и чтобы более опытные коллеги вас дебилом не считали. Ну и чтобы экономить деньги работодателя.

              Кичиться? Лет 8 назад во времена засилия чёрного SEO мой php-софт, написанный за 2 месяца, делал на автомате по 100к уников в день и приносил неслабые деньги, т.е. со своей задачей справлялся. Вот только трогать его я старался как можно меньше — всё висело на соплях. А трогать хотелось, т.к. развивать было что. Даже через год после разработки хотел запустить это на новом сервере — не удалось. Мораль сей басни такова — говнокод тоже бывает крутым и полезным. Чаще всего один раз и в неизменном виде.

              P.S. Класс Record в Doctrine 1 состоит примерно из 2700 строк. Doctrine 1 — хороший для своих задач и своего времени ORM. В настоящее время есть Doctrine 2.
  • 0
    В таких случаях я стараюсь использовать публичные свойства, а геттеры и сеттеры я использую для критических свойств, которые необходимо более строго контролировать

    Считаю, что удобнее для восприятия использовать что-то одно в отдельно взятом классе. Программист, работающий с классом, может не знать, какое свойство критическое, а какое — нет, и придется ему отвлекаться на выяснение «а как же правильно» для каждого свойства.
    • 0
      Всегда надо использовать геттеры, т.к. это позволяет сохранять интерфейс доступа к объекту при изменении логики работы объекта без применения «магии».
      • 0
        «Без фанатизма»
        • 0
          Никакого фанатизма. Единообразие и гибкость при отсутствии каких-либо значимых дополнительных временных затрат.

          При наличии современных IDE вообще удивляюсь, что кому-то это может быть неудобно. Создал приватные свойства, на нужные повесил геттеры (public или protected, private для lazy-load'а и т.п.) В итоге пользователь интерфейса набирает в IDE «get» и видит все доступные геттеры.

          Это реально ускоряет разработку:
          1. Не надо раздумывать, какие свойства делать приватными, а какие — нет.
          2. Становится доступен рефакторинг доступа к свойствам.
          3. Список доступных для изменения/получения свойств доступен через набор префиксов get/set (при выборе стандартного паттерна именования геттеров/сеттеров).
  • +2
    Не нужно забывать о возможных изменениях кода. Допустим $a->bar у нас был когда-то int и мы делали так
    $a->bar = 1;
    switch($a->bar) { case 1: ...
    

    Со временем мы пришли к выводу, что нам нужно изменить int на array $a->bar = array(); и теперь по ВСЕМУ коду, который в своей работе использует класс $a, необходимо вносить изменения.

    Если бы мы использовали get/set то изменения коснулись бы только этих методов класса
    // было
    public function setBar($value) {
       $this->bar = (int) $value;
    }
    
    // стало
    public function setBar($value) {
       $this->bar[] = $value;
    }
    
    • 0
      Не нужно забывать и о том, что изменений вида $a->id = (int) в $a->id = (array) в принципе не должно быть. Переменные следует именовать, чтобы не было неоднозначности. Программист, который единичный целочисленный id преобразовал в массив и оставил его с тем же именем — очень плохой программист.

      А если мы уверены в том, что конкретные (а зачастую все) атрибуты классы в дальнейшем не поменяют свой тип — зачем «перестраховываться» от того, чего не будет?
      • +2
        Мне тоже странно видеть размышления на тему, а если вдруг пользователь станет емейл сообщением, то что делать с его возрастом?
      • 0
        Не хочу переходить, на личности, но зачем спорить? За плечами 8 лет ежедневного кодинга на PHP, сейчас мне этот язык нравится все меньше, в больших проектах «магия» и отсутствие типов иногда влечет за собой печальные последствия (в одном месте возвращает метод boolean в другом void и прочее)… «Мы уверены» — да ты можешь быть уверен, что ТВОЙ код не поменяется. Но если ты работаешь не один над проектом, который постоянно развивается, нельзя быть в чем-то уверенным. Это с родни преждевременной оптимизации, когда ты пишешь код основываясь на фичах текущей версии языка забывая о том, что язык изменяется — меняя свое поведение. Тем самым у тебя получается нечитаемый говнокод, который оптимально работает только с одной версией PHP.

        Пример из жизни «напиши опрос — ну там ставишь один вариант ответа и голосуешь» -> «ну нам бы хотелось что бы было несколько опросов сразу» -> «ну да, но кроме radio должен быть еще выбор нескольких вариантов ответов» -> «а добавьте так что если выбрали один вариант, появляются другие зависимые от него»… Между -> может пройти и месяц и год и неделя… Код изначально нужно проектировать так что бы его можно было легко модифицировать.

        Вариант с магией и огромным switch — плохая практика, во первых он разрастется со временем до такой степени, что понять его через 2 месяца не представится возможным, а заказчику нужно добавить фичу еще вчера…
      • 0
        1. Вот здесь habrahabr.ru/post/197332/#comment_6883056 вы писали от восторга, какой прекрасный код написали ребята из Yii. Почему же у них нет постфиксов «Array» в названиях переменных?

        2. Кроме смены типа может смениться логика:
        $query->limit = 100000;
        

        И вдруг (по объективным причинам, которые изначально не учли) мы захотели ограничить максимальное возможное количество возвращаемых записей.
        public function setLimit($limit)
        {
            if ($limit > self::MAX_LIMIT) {
                $limit = self::MAX_LIMIT;
            }
            $this->limit = $limit;
        }
        


        3. Кто вам сказал, что в ваш прекрасный int не насуют массивов? Вы это как-то отслеживаете? Почему вы вообще говорите о типе свойства, если разговор идёт о public в языке без строгой типизации?

        4. Сменить типа свойства просто — скажем, сменили строковую дату в ISO 8601 на DateTime.
        • 0
          Ребята из Yii говнокодеры, я понял вашу мысль. А вы гораздо умнее их, даже при том что выносите логику поведения на уровень моделей, да-да.
          • 0
            Вы не поняли мою мысль. Моя мысль в том, что у вас у самого нет сформировавшегося подхода.

            И поподробнее про логику и уровень моделей. Мне кажется, вы кое-что путаете в терминологии.
  • –4
    А мне вот такой вариант нравится: gist.github.com/elfet/7081560
    • 0
      image
  • 0
    Руководствуюсь принципом «поменьше магии».
  • 0
    Эх, опять.

    Что мне всегда «нравилось» в подобных размышлениях, так это выводы в духе «мне удобно использовать этот вариант».
    Т.е. мы как будто думаем о поддержке кода, но только со своей стороны.

    Без обид, но посмотрите все статьи на эту тему, итог всегда один и тот же: одни советуют одно, другие — другое. Т.е. рано или поздно выбранный вами вариант будет ломать головы тем, кому придётся поддерживать ваш код, потому что они другой религии предпочитают другой вариант.

    А выбора всё равно лишаем…
  • 0
    Я считаю что вместо того чтобы вводить магические свойства необходимо рефакторить код. Насчет геттеров и прямого доступа, я уверен, что необходимо чтобы все кодеры писали в одном стиле. Или не используем геттеры совсем или не используем публичный доступ. Конечно же, мы выбираем геттеры. Любая современная IDE должна умет их автоматически генерировать.
  • 0
    За достаточно продолжительный срок разработки на php я использовал все три приведённых способа. И пришёл к такому выводу: когда с кодом работают от 2 человек, то количество магии должно быть сведено к минимуму. Поэтому лучше использовать геттеры и сеттеры. Да, кода становится чуть больше. Да, мы теряем какие-то наносекунды на вызов метода, но плюсы перевешивают минусы. Помимо указанных в публикации, я добавлю такие:
    1. Проще рефакторинг
    2. Программист всегда знает что происходит: вызов метода или обращение к свойству, а это о многом может рассказать.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.