Getter’ы и Setter’ы — магия, которая должна знать свое место / PHP. Особенности и фичи


Оригинал: Getters and Setters — a Magic That Should Know Its Place

Джозеф Кроуфорд, один из моих читателей, прочитал статью о том, как я не люблю писать getter’ы и setter’ы и предположил, что я могу использовать волшебные методы __get и __set.
Я скажу вам, почему это не очень хорошая идея, использовать их обычным способом. Кроме того, я собираюсь поведать вам историю, где они действительно оказались полезными, — речь пойдет о создании статических типов в PHP (динамический язык).
Для тех, кто не знаком с методами __get и __set — это два «магических» метода, которые работают следующим образом:

class Animal {
  function __get($property) {
    //...
  }

  function __set($property, $value) {
    //...
  }
}

$cow = new Animal;
$cow->weight = '1 ton'; // same as $cow->__set('weight', '1 ton')
print $cow->weight;     // same as print $cow->__get('weight');


Как правило, вышеперечисленные методы используются для создания динамических свойств. Какой вывод можно из этого сделать? Если вы хотите создавать любые случайные свойства, просто используйте хэш (он же массив с ключами).
Что же хорошего в getter’ах и setter’ах?
Давайте посмотрим:
class Animal
{
  public $weightInKgs;
}

$cow = new Animal;
$cow->weightInKgs = -100;


Что? Вес с отрицательным значением? Это с большинства точек зрения неправильно.
Корова не должна весить меньше 100 кг (я так думаю :). В пределах 1000 — допустимо.
Как же нам обеспечить такое ограничение.
Использовать __get и __set — довольно быстрый способ.
class Animal
{
  private $properties = array();
  
  public function __get($name) {
    if(!empty($this->properties[$name])) {
        return $this->properties[$name];
    } else {
       throw new Exception('Undefined property '.$name.' referenced.');
    }
  }

  public function __set($name, $value) {
    if($name == 'weight') {
      if($value < 100) {
        throw new Exception("The weight is too small!")
      }
    }
    $this->properties[$name] = $value;
  }
}

$cow = new Animal;
$cow->weightInKgs = -100; // throws an Exception


А что если у вас есть класс с 10—20 свойствами и проверками для них? В этом случае неприятности неизбежны.
public function __set($name, $value) {
    if($name == 'weight') {
      if($value < 100) {
        throw new Exception("The weight is too small!")
      }
      if($this->weight != $weight) {
        Shepherd::notifyOfWeightChange($cow, $weight);
      }
    }

    if($name == 'legs') {
      if($value != 4) {
        throw new Exception("The number of legs is too little or too big")
      }
      $this->numberOfLegs = $numberOfLegs;
      $this->numberOfHooves = $numberOfLegs;
    }

    if($name == 'milkType') {
      .... you get the idea ....
    }
    $this->properties[$name] = $value;
  }


И наоборот, getter’ы и setter’ы проявляют себя с лучшей стороны, когда дело доходит до проверки данных.
class Animal
{
    private $weight;
    private $numberOfLegs;
    private $numberOfHooves;
    public $nickname;


    public function setNumberOfLegs($numberOfLegs)
    {
        if ($numberOfLegs != 100) {
            throw new Exception("The number of legs is too little or too big");
        }
        $this->numberOfLegs = $numberOfLegs;
        $this->numberOfHooves = $numberOfLegs;
    }

    public function getNumberOfLegs()
    {
        return $this->numberOfLegs;
    }


    public function setWeight($weight)
    {
        if ($weight < 100) {
            throw new Exception("The weight is too small!");
        }
        if($this->weight != $weight) {
          Shepherd::notifyOfWeightChange($cow, $weight);
        }
        $this->weight = $weight;
    }

    public function getWeight()
    {
        return $this->weight;
    }
}


Ничто не идет в сравнение с краткими функциями {get, set;} из C#. Вполне вероятно, такая поддержка скоро появится в PHP, ну а пока не расслабляемся…
Каждый метод несет ответственность только за собственную область, благодаря этому в коде легче ориентироваться. Все равно получается слишком много кода, но он чище, чем __set-версия. Существует хороший эвристический подход, который заключается в следующем: если ваш метод (функция) занимает больше, чем 1 экран — нужно сокращать. Это улучшит восприятие кода.
Мы также храним некоторую бизнес-логику. Копыт всегда будет ровно столько, сколько и ног, а если мы заметим изменение веса скотинки, мы тут же уведомим пастуха.
Так как мы не заботимся о прозвищах коров и не проверяем их, пусть данные будут общедоступными без getter’ов и setter’ов.
Опять же, я действительно не писал всех этих getter’ов и setter’ов — PHP Storm сделал это за меня. Я просто написал следующее:
class Animal {
  private $weight;
  private $numberOfLegs;
}


и нажал Alt+Insert -> Getters and setters. PHPStorm сгенерировал все автоматически.
Теперь в виде дополнительного преимущества PHP Storm при работе с getter’ами и setter’ами у меня есть возможность использовать функцию автозаполнения:


В случае с __get я не имею такой возможности, я могу лишь написать это:
$cow->wieght = -100


Теперь корова «весит» (wIEghts) минус 100 кг.
Я могу забыть, что это вес в кг, достаточно просто написать weight — все будет работать.
Итак, getter’ы and setter’ы бывают очень даже полезны (но все же не поклоняйтесь им, вы же не программист Java). Если вам просто нужны свободные свойства, используйте массив:
$cow = array('weight' => 100, 'legs' => 4);


Этот трюк гораздо легче провернуть, чем __get и __set.
Но, если вы хотите быть уверенным, что ваши данные всегда имеют только допустимые значения, используйте setter’ы с проверкой. При наличии интегрированной среды разработки (IDE), типа PHP Storm, вы будете любить setter’ы, потому что они очень просты в использовании. Вместо $cow->setLegs() для PHP Storm достаточно будет набрать co[TAB]sl[TAB]. Да, легко! Нет больше опечаток, и вы можете видеть, какие параметры принимает метод.
Метод __set имеет и еще один недостаток. Он принимает только 1 параметр. Что делать, если вам нужно 2? Например, как здесь: $store1->setPrice('item-1', 100). Вам необходимо установить цену товара в магазине. Метод __set не позволит вам этого сделать, а setter позволит.