Джозеф Кроуфорд, один из моих читателей, прочитал статью о том, как я не люблю писать 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 позволит.