objects04 13

Расширенные возможности использования объектов

В предыдущей статье вы уже познакомились с уточнением типов аргументов методов класса и управлением доступом к свойствам и методам. Все это позволяет довольно гибко управлять интерфейсом класса. В этой статье мы подробнее изучим более сложные объектно-ориентированные возможности PHP.

Для проработки последующих примеров вам понадобиться структура класса ShopProduct, которую мы определили к данному моменту и вспомогательные классы - исходный код.

Статические методы и свойства

Во всех примерах показанных ранее мы работали с объектами. Я охарактеризовал классы как шаблоны, с помощью которых создаются объекты, а объекты — как активные компоненты, методы которых мы вызываем и к свойствам которых получаем доступ. Отсюда следовал вывод, что в объектно-ориентированном программировании реальная работа выполняется с помощью экземпляров классов. А классы, в конечном счете, — это просто шаблоны для создания объектов.

Но на самом деле не все так просто. Мы можем получать доступ и к методам, и к свойствам в контексте класса, а не объекта. Такие методы и свойства являются "статическими" и должны быть объявлены с помощью ключевого слова static:

Код PHP
class StaticExample
{
	// Статическое свойство
	static public $num = 0;
	
	// Статический метод
	static public function HelloWorld()
	{
		echo 'Hello, world!';
	}
}

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

Поскольку доступ к статическому элементу осуществляется через класс, а не экземпляр объекта, вам не нужна переменная, которая ссылается на объект. Вместо этого используется имя класса, после которого указывается два двоеточия "::".

Код PHP
echo StaticExample::$num;
StaticExample::HelloWorld();

С этим синтаксисом вы познакомились в предыдущей статье. Мы использовали конструкцию "::" в сочетании с ключевым словом parent, чтобы получить доступ к переопределенному методу родительского класса. Сейчас, как и тогда, мы обращаемся к классу, а не к данным, содержащимся в объекте. В коде класса можно использовать ключевое слово parent, чтобы получить доступ к суперклассу, не используя имя класса. Чтобы получить доступ к статическому методу или свойству из того же самого класса (а не из дочернего класса), мы будем использовать ключевое слово self. Ключевое слово self используется для обращения к текущему классу, а псевдопеременная $this — к текущему объекту. Поэтому из-за пределов класса StaticExample мы обращаемся к свойству $num с помощью имени его класса. А внутри класса StaticExample можно использовать ключевое слово self:

Код PHP
class StaticExample
{
	static public $num = 0;
	
	static public function HelloWorld()
	{
		// Доступ к статическому свойству внутри класса
		self::$num++;
		echo 'Hello, world!';
	}
}

Вызов метода с помощью ключевого слова parent — это единственный случай, когда следует использовать статическую ссылку на нестатический метод. Кроме случаев обращения к переопределенному методу родительского класса, конструкция "::" должна всегда использоваться только для доступа к статическим методам или свойствам. Однако в документации часто можно увидеть использование конструкции "::" для ссылок на методы или свойства (использовать в данном случае "->" в документации не совсем корректно). Это не означает, что рассматриваемый элемент — обязательно статический; это всего лишь значит, что он принадлежит к указанному классу. Например, ссылку на метод write() класса ShopProductWriter можно записать так: ShopProductwriter::write(), несмотря на то что метод write() не является статическим. При этом использовать эту конструкцию в коде нельзя!

По определению статические методы не вызываются в контексте объекта. По этой причине статические методы и свойства часто называют переменными и свойствами класса. Как следствие, нельзя использовать псевдопеременную $this внутри статического метода.

А зачем вообще использовать статический метод или свойство? У статических элементов есть ряд полезных характеристик. Во-первых, они доступны из любой точки сценария (при условии, что у вас есть доступ к классу). Это означает, что вы можете обращаться к функциям, не передавая экземпляр класса от одного объекта другому или, что еще хуже, сохраняя экземпляр объекта в глобальной переменной. Во-вторых, статическое свойство доступно каждому экземпляру объекта этого класса. Поэтому можно определить значения, которые должны быть доступны всем объектам данного типа. И наконец, сам факт, что не нужно иметь экземпляр класса для доступа к его статическому свойству или методу, позволит избежать создания экземпляров объектов исключительно ради вызова простой функции.

Постоянные свойства

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

В PHP 5 можно определять постоянные свойства внутри класса. Как и глобальные константы, константы класса нельзя изменять после того, как они были определены. Постоянное свойство объявляют с помощью ключевого слова const. В отличие от обычных свойств, перед именем постоянного свойства не ставится знак доллара. По принятому соглашению им часто назначают имена, состоящие только из прописных букв, как в следующем примере:

Код PHP
class SomeClass
{
	const AVAILABLE = 0;
	const COUNT = 10;
	// ...
}

Постоянные свойства могут содержать только значения, относящиеся к элементарному типу. Константе нельзя присвоить объект. Как и к статическим свойствам, доступ к постоянным свойствам осуществляется через класс, а не через экземпляр объекта. Подобно тому как константа определяется без знака доллара, при обращении к ней также не требуется использовать никакой символ впереди:

Код PHP
echo SomeClass::COUNT;

Попытка присвоить константе значение после того, как она была объявлена, приведет к ошибке на этапе синтаксического анализа. Константы следует использовать, когда свойство должно быть доступным для всех экземпляров класса и когда значение свойства должно быть фиксированным и неизменным.

Абстрактные классы

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

Абстрактный класс определяется с помощью ключевого слова abstract. Давайте переопределим класс ShopProductWriter, который мы создали ранее, в виде абстрактного класса:

Код PHP
abstract class ShopProductWriter 
{
	protected $products = array();
	
	public function addProduct(ShopProduct $shopProduct) {
		$this->products[] = $shopProduct;
	}
}

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

Ошибка создания экземпляра абстрактного класса

В большинстве случаев абстрактный класс будет содержать, по меньшей мере, один абстрактный метод. Как и класс, он описывается с помощью ключевого слова abstract. Абстрактный метод не может иметь реализацию в абстрактном классе. Он объявляется как обычный метод, но объявление заканчивается точкой с запятой, а не телом метода. Давайте добавим абстрактный метод write() к классу ShopProductWriter:

Код PHP
abstract class ShopProductWriter 
{
	protected $products = array();
	
	public function addProduct(ShopProduct $shopProduct) {
		$this->products[] = $shopProduct;
	}
	
	abstract public function write();
}

Создавая абстрактный метод, вы гарантируете, что его реализация будет доступной во всех конкретных дочерних классах, но детали этой реализации остаются неопределенными. Итак, в любом классе, который расширяет абстрактный класс, должны быть реализованы все абстрактные методы либо сам класс должен быть объявлен абстрактным. При этом в расширяющем классе должны быть не просто реализованы все абстрактные методы, но и должна быть воспроизведена сигнатура их методов. Это означает, что уровень доступа в реализующем методе не может быть более строгим, чем в абстрактном методе. Реализующему методу также должно передаваться такое же количество аргументов, как и абстрактному методу, а также в нем должны воспроизводиться все уточнения типов класса.

Ниже приведены две реализации класса ShopProductWriter:

Код PHP
class XmlShopProductWriter extends ShopProductWriter{
    public function write() {
        $str = '<?xml version="1.0" encoding="UTF-8"?>'."\n";
        $str .= "<products>\n";
         foreach ( $this->products as $product ) {
            $str .= "\t<product title=\"{$product->getTitle()}\">\n";
            $str .= "\t\t<summary>\n";
            $str .= "\t\t{$product->getSummaryLine()}\n";
            $str .= "\t\t</summary>\n";
            $str .= "\t</product>\n";
        }
        $str .= "</products>\n";   
        print $str;
    }
}

class TextShopProductWriter extends ShopProductWriter{
    public function write() {
        $str = "ТОВАРЫ: ";
		foreach ($this->products as $product)
		{
			$str .= "{$product->getTitle()}: <b>{$product->getProducer()}</b>, {$product->getPrice()}$<br>";
		}
		echo $str;
    }
}

Я создал два класса, каждый с собственной реализацией метода write(). Первый выводит данные о товарах в формате XML, а второй — в текстовом виде. Теперь методу, которому требуется передать объект типа ShopProductWriter, не нужно точно знать, какой из этих двух классов он получает, поскольку ему достоверно известно, что в обоих классах реализован метод write(). Обратите внимание на то, что мы не проверяем тип переменной $products, прежде чем использовать ее как массив. Причина в том, что это свойство инициализируется как пустой массив в классе ShopProductWriter.

В PHP 4 работу абстрактных классов моделировали с помощью методов, которые выводили предупреждающие сообщения или даже содержали операторы die(). Это заставляло программиста реализовывать абстрактные методы в производном классе, поскольку в противном случае сценарий переставал работать. Проблема состояла в том, что абстрактная природа базового класса проверяется только в случае вызова абстрактного метода. В PHP 5 абстрактные классы проверяются еще на этапе синтаксического анализа, что намного безопаснее.

Интерфейсы

Как известно, в абстрактном классе допускается частичная реализация методов, не объявленных абстрактными. В отличие от них, интерфейсы — это чистой воды шаблоны. Интерфейс может только определять функциональность, но никогда не реализует ее. Для объявления интерфейса используется ключевое слово interface. Интерфейс может содержать объявления свойств и методов, но не тела этих методов.

Давайте определим интерфейс:

Код PHP
interface IChargeable 
{
	public function getPrice();
}

Как видите, интерфейс очень похож на класс. Любой класс, содержащий этот интерфейс, должен реализовывать все методы, определенные в интерфейсе; в противном случае класс должен быть объявлен как абстрактный.

При реализации интерфейса в классе, имя интерфейса указывается в объявлении этого класса после ключевого слова implements. После этого процесс реализации интерфейса станет точно таким же, как расширение абстрактного класса, который содержит только абстрактные методы. Давайте сделаем так, чтобы в классе ShopProduct был реализован интерфейс IChargeable:

Код PHP
class ShopProduct implements IChargeable
{
	// ...
	
	public function getPrice () {
		return $this->price;
	}
    
    // ...
}

В классе ShopProduct уже есть метод getPrice(), что же может быть полезного в реализации интерфейса IChargeable? И снова ответ связан с типами. Дело в том, что реализующий класс принимает тип класса и интерфейса, который он расширяет. Это означает, что класс CDProduct относится к следующим типам: CDProduct, ShopProduct, IChargeable. Эту особенность можно использовать в клиентском коде. Как известно, тип объекта определяет его функциональные возможности. Поэтому метод

Код PHP
public function CDInfo ( CDProduct $prod ) {
	// ...
}

"знает", что у объекта $prod есть метод getPlayLength() в дополнение ко всем методам, определенным в классе ShopProduct и интерфейсе IChargeable. Если тот же самый объект CDProduct передается методу

Код PHP
public function addProduct( ShopProduct $prod ) {
	// ...
}

то известно, что объект $prod поддерживает все методы, определенные в классе ShopProduct. Однако без дальнейшей проверки данный метод ничего не будет знать о методе getPlayLength(). И снова, если передать тот же объект CDProduct методу

Код PHP
public function addChargeableItem( IChargeable $item ) {
	//...
}

данному методу ничего не будет известно обо всех методах, определенных в классах ShopProduct или CDProduct. При этом интерпретатор только проверит, содержит ли аргумент $item метод getPrice().

Поскольку интерфейс можно реализовать в любом классе (на самом деле, в классе можно реализовать любое количество интерфейсов), с помощью интерфейсов можно эффективно объединить типы данных, не связанных никакими другими отношениями. В результате мы можем определить совершенно новый класс, который реализует интерфейс IChargeable:

Код PHP
class Shipping implements IChargeable
{
	public function getPrice () {
		// ...
	}
}

Затем объект типа Shipping мы можем передать методу addChargeableItem(), точно так же как мы передавали ему объект типа CDProduct. Для клиента, работающего с объектом типа IChargeable, очень важно то, что он может вызвать метод getPrice(). Любые другие имеющиеся методы связаны с другими типами — через собственный класс объекта, суперкласс или другой интерфейс. Но они не имеют никакого отношения к нашему клиенту.

В классе можно как расширить суперкласс, так и реализовать любое количество интерфейсов, при этом ключевое слово extends должно предшествовать ключевому слову implements.

В PHP поддерживается только наследование от одного родителя (так называемое одиночное наследование), поэтому после ключевого слова extends можно указать только одно имя базового класса.

Позднее статическое связывание: ключевое слово static

После того как мы рассмотрели абстрактные классы и интерфейсы, самое время снова обратиться к статическим методам. Статический метод можно использовать в качестве фабрики, создающей экземпляры объектов того класса, в котором содержится этот метод. Если вы такой же ленивый программист, как и я, то вас должны раздражать повторы кода наподобие тех, что приведены ниже:

Код PHP
abstract class DomainObject {}

class User extends DomainObject {
	public static function create() { 
		return new User();
	}
}

class Document extends DomainObject { 
	public static function create() 
	{
		return new Document();
	}
}

Сначала я создал суперкласс под именем DomainObject. Само собой разумеется, что в реальном проекте в нем будет находиться функциональность, общая для всех дочерних классов. После этого я создал два дочерних класса User и Document. Я хотел, чтобы в каждом из моих конкретных классов находился метод create(). Созданный мною код прекрасно работает, но в нем есть досадный недостаток — дублирование. Мне совсем не нравится повторять однотипный код наподобие того, что приведен выше, для каждого создаваемого дочернего объекта, расширяющего класс DomainObject. Как насчет того, чтобы переместить метод create() в суперкласс?

Код PHP с ошибкой
abstract class DomainObject {
	public static function create() { 
		return new self();
	}
}

class User extends DomainObject { }
class Document extends DomainObject { }

Document::create();

Ну что ж, все это выглядит круто! Теперь весь общий код сосредоточен в одном месте, и чтобы обратиться к текущему классу, я воспользовался ключевым словом self. Однако насчет ключевого слова self я сделал допущение, что оно должно так работать. На самом деле оно не работает для классов так же, как псевдопеременная $this для объектов. С помощью ключевого слова self нельзя сослаться на вызывающий объект. Оно используется только для разрешения ссылок на содержащий класс, в контексте которого вызывается метод. Поэтому при попытке запуска приведенного выше примера получим следующее сообщение об ошибке:

PHP Fatal error: Cannot instantiate abstract class ...

Таким образом, ключевое слово self трансформируется в ссылку на класс DomainObject, в котором определен метод create(), а не на класс Document, для которого этот метод должен быть вызван. До появления PHP 5.3 это было серьезным ограничением языка, которое породило массу неуклюжих обходных решений. В PHP 5.3 впервые введена концепция позднего статического связывания (late static bindings). Самым заметным ее проявлением является введение нового (в данном контексте) ключевого слова static - оно аналогично ключевому слову self, за исключением того, что относится к вызывающему, а не содержащему классу.

Итак, теперь я смогу воспользоваться всеми преимуществами наследования в статическом контексте:

Код PHP
abstract class DomainObject {
	public static function create() { 
		return new static();
	}
}

class User extends DomainObject { }
class Document extends DomainObject { }

print_r(Document::create());

В результате будет выведено "Document Object ( )". Ключевое слово static можно использовать не только для создания объектов. Так же как и self и parent, его можно использовать как идентификатор для вызова статических методов даже из нестатического контекста. Например, я хочу реализовать идею группировки моих классов DomainObject. По умолчанию все классы попадают в категорию 'default'. Но для некоторых веток иерархии наследования моих классов мне нужно это переопределить:

Код PHP
abstract class DomainObject {
    private $group;
    public function __construct() {
        $this->group = static::getGroup();
    }

    public static function create() {
        return new static();        
    }

    static function getGroup() {
        return "default"; 
    }
}

class User extends DomainObject {
}

class Document extends DomainObject {
    static function getGroup() {
        return "document"; 
    }
}

class SpreadSheet extends Document {
}

print_r(User::create());
echo '<br>';
print_r(SpreadSheet::create());

Здесь в класс DomainObject я ввел конструктор, в котором используется ключевое слово static для вызова метода getGroup(). Стандартное значение группы сосредоточено в классе DomainObject, но оно переопределяется в классе Document. Я также создал новый класс SpreadSheet, расширяющий класс Document. Вот что получим в результате.

Использование позднего статического связывания для вызова метода из контекста класса

Все происходящее с классом User не настолько очевидно и поэтому требует объяснений. В конструкторе класса DomainObject вызывается метод getGroup(), который интерпретатор находит в текущем классе. Несмотря на это, в случае с классом SpreadSheet поиск метода getGroup() начинается не с класса DomainObject, а с класса SpreadSheet, для которого из метода create() был вызван стандартный конструктор. Поскольку в классе spreadsheet реализация метода getGroup() не предусмотрена, интерпретатор вызывает аналогичный метод класса Document (т.е. идет вверх по иерархии объектов). До появления PHP 5.3 и позднего статического связывания, здесь у меня возникала проблема из-за использования ключевого слова self, которое находило метод getGroup() только в классе DomainObject.

Завершенные классы и методы

Наследование открывает большие возможности для широкого поля действий в пределах иерархии класса. Вы можете переопределить класс или метод, чтобы вызов в клиентском методе приводил к совершенно разным результатам, в зависимости от типа объекта, переданного методу в качестве аргумента. Но иногда код класса или метода нужно зафиксировать, если предполагается, что в дальнейшем он не должен изменяться. Если вы создали необходимый уровень функциональности для класса и считаете, что его переопределение может только повредить идеальной работе программы, используйте ключевое слово final. Ключевое слово final позволяет положить конец наследованию. Для завершенного класса нельзя создать подкласс. А завершенный метод нельзя переопределить.

Давайте объявим класс завершенным:

Код PHP
final class Checkout {
	// ...
}

А теперь сделаем попытку создать подкласс класса Checkout:

Код PHP
class IllegalCheckout extends Checkout {
	// ...
}

Это приведет к ошибке PHP: "Class IllegalCheckout may not inherit from final class". Мы можем несколько "смягчить" ситуацию, объявив завершенным только метод в классе Checkout, а не весь класс. Ключевое слово final должно стоять перед любыми другими модификаторами, такими как protected или static:

Код PHP
class Checkout {
	final function some() {
		// ...
	}
}

Теперь мы можем создать подкласс класса Checkout, но любая попытка переопределить метод some() приведет к неустранимой ошибке.

В хорошем объектно-ориентированном коде обычно во главу угла ставится строго определенный интерфейс. Но за этим интерфейсом могут скрываться разные реализации. Разные классы или сочетания классов могут соответствовать общим интерфейсам, но при этом вести себя по-разному в разных ситуациях. Объявляя класс или метод завершенным, вы тем самым ограничиваете эту гибкость. В некоторых случаях это выгодно. Но прежде чем объявлять что-либо завершенным, следует серьезно подумать. Действительно ли нет таких ситуаций, в которых переопределение было бы полезным? Конечно, вы всегда можете передумать, но внести изменения впоследствии может быть нелегко, например, если вы распространяете библиотеку для совместного использования. Поэтому будьте осторожны, используя ключевое слово final.

Работа с методами-перехватчиками

В PHP предусмотрены встроенные методы-перехватчики, которые могут перехватывать сообщения, посланные неопределенным (т.е. несуществующим) методам или свойствам. Это свойство называется также перегрузкой (overloading), но поскольку этот термин в Java и С# означает нечто совершенно другое, я думаю, будет лучше использовать термин "перехват" (interception).

В PHP 5 поддерживается несколько встроенных методов-перехватчиков. Как и в случае метода __construct(), вызов этих методов происходит неявно, когда удовлетворяются соответствующие условия. Эти методы описаны в таблице ниже:

Методы-перехватчики
Метод Описание
__get($property) Вызывается при обращении к неопределенному свойству
__set($property, $value) Вызывается, когда неопределенному свойству присваивается значение
__isset($property) Вызывается, когда функция isset() вызывается для неопределенного свойства
__unset($property) Вызывается, когда функция unset() вызывается для неопределенного свойства
__call($method, $arg_array) Вызывается при обращении к неопределенному методу

Методы __get() и __set() предназначены для работы со свойствами, которые не были объявлены в классе (или его родителе). Метод __get() вызывается, когда клиентский код пытается прочитать необъявленное свойство. Он вызывается автоматически с одним строковым аргументом, содержащим имя свойства, к которому клиентский код пытается получить доступ. Все, что вернет метод __get(), будет отослано обратно клиенту, как будто искомое свойство существует с этим значением. Рассмотрим короткий пример:

Код PHP
class Person {
    function __get( $property ) {
        $method = "get{$property}";
        if ( method_exists( $this, $method ) ) {
            return $this->$method();
        }
    }

    function __isset( $property ) {
        $method = "get{$property}";
        return ( method_exists( $this, $method ) );
    }  

    function getName() {
        return "Вася";
    }
                                                                                
    function getAge() {
        return 24;
    }
}

Когда клиентский код пытается получить доступ к неопределенному свойству, вызывается метод __get(), в котором перед именем переданного ему свойства добавляется строка "get". Затем полученная строка, содержащая новое имя метода, передается функции method_exists(). Этой функции передается также ссылка на текущий объект, для которого проверяется существование метода. Если метод существует, мы вызываем его и передаем возвращенное им значение клиентскому коду. Поэтому если клиентский код запрашивает свойство $name:

Код PHP
$p = new Person();
echo $p->name;

то метод getName() вызывается неявно и выводится строка "Вася".

Если же метод не существует, то ничего не происходит. Свойству, к которому пользователь пытается обратиться, присваивается значение NULL. Метод __isset() работает аналогично методу __get(). Он вызывается после того, как в клиентском коде вызывается функция isset() и ей в качестве параметра передается имя неопределенного свойства. Метод __set() вызывается, когда клиентский код пытается присвоить значение неопределенному свойству. При этом передается два аргумента: имя свойства и значение, которое клиентский код пытается присвоить. Затем вы можете решить, как работать с этими аргументами.

Метод __call(), вероятно, самый полезный из всех методов-перехватчиков. Он вызывается, когда клиентский код обращается к неопределенному методу. При этом методу __call() передается имя несуществующего метода и массив, в котором содержатся все аргументы, переданные клиентом. Значение, возвращаемое методом __call(), передается клиенту так, как будто оно было возвращено вызванным несуществующим методом.

Метод __call() может использоваться для делегирования. Делегирование — это механизм, посредством которого один объект может вызвать метод другого объекта. Это чем-то напоминает наследование, когда дочерний класс вызывает метод, реализованный в родительском классе. В случае наследования взаимосвязь между родительским и дочерним классами фиксирована. Поэтому возможность изменить объект-получатель во время выполнения программы означает, что делегирование является более гибким, чем наследование. Чтобы лучше это понять, давайте проиллюстрируем все на примере. Рассмотрим простой класс, предназначенный для форматирования информации, полученной от класса Person:

Код PHP
class PersonWriter {

    function writeName( Person $p ) {
        print $p->getName()."<br>";
    }

    function writeAge( Person $p ) {
        print $p->getAge()."<br>";
    }
}

class Person {
    private $writer;

    function __construct( PersonWriter $writer ) {
        $this->writer = $writer;
    }

    function __call( $method, $args ) {
        if ( method_exists( $this->writer, $method ) ) {
            return $this->writer->$method( $this );
        }
    }

    function getName()  { return "Вася"; }
    function getAge() { return 24; }
}

Здесь конструктору класса Person в качестве аргумента передается объект типа PersonWriter, который сохраняется в переменной свойства. В методе __call() используется значение аргумента $method и проверяется наличие метода с таким же именем в объекте PersonWriter, ссылка на который была сохранена в конструкторе. Если такой метод найден, его вызов делегируется объекту PersonWriter. При этом методу передается ссылка на текущий экземпляр объекта типа Person, которая хранится в псевдопеременной $this. Поэтому если клиент вызовет несуществующий в классе Person метод:

Код PHP
$person= new Person( new PersonWriter() );
$person->writeName();
$person->writeAge();

то будет вызван метод __call(). В нем определяется, что в объекте типа PersonWriter существует метод с именем writeName(), который и вызывается. Это позволяет избежать вызова делегированного метода вручную, как показано ниже:

Код PHP
function writeName() {
	$this->writer->writeName( $this );
}

Таким образом класс Person, как по волшебству, получил два новых метода класса PersonWriter. Хотя автоматическое делегирование избавляет вас от рутинной работы по однотипному кодированию вызовов методов, сам код становится труден для понимания. И если в вашей программе активно используется делегирование, то для внешнего мира создается динамический интерфейс, который не поддается рефлексии (исследованию аспектов класса во время выполнения программы) и не всегда с первого взгляда понятен программисту клиентского кода.

Причина в том, что логика, лежащая в основе взаимодействия между делегирующим классом и целевым объектом, может быть непонятной. Ведь она скрыта в таких методах, как __call(), а не явно задана отношениями наследования или уточнениями типа аргументов методов. Методы-перехватчики имеют свою область применения, но использовать их следует с осторожностью. И в классах, которые применяют эти методы, факт такого применения необходимо очень четко и ясно документировать. К вопросам делегирования и рефлексии мы вернемся позже в следующих статьях.

Определение методов деструктора

Как мы уже видели, при создании экземпляра объекта автоматически вызывается метод __construct() - конструктор класса. В PHP 5 также существует метод __destruct() - деструктор класса. Он вызывается непосредственно перед тем, как происходит "сборка мусора", т.е. я хочу сказать, перед тем как объект удаляется из памяти. Вы можете использовать этот метод для выполнения завершающей очистки объекта, если это необходимо.

Например, предположим, что класс после получения специальной команды сохраняет данные объекта в базе данных. Тогда метод __destruct() можно использовать для того, чтобы гарантированно сохранить данные объекта перед его удалением из памяти. Добавим деструктор в класс Person, как показано ниже:

Код PHP
class Person {
    protected $name;    
    private $age;    
    private $id;    

    function __construct( $name, $age ) {
        $this->name = $name;
        $this->age  = $age;
    }

    function setId( $id ) {
        $this->id = $id;
    }
    
    function __destruct() {
        if ( ! empty( $this->id ) ) {
            // В реальном коде возможны операции с базой данных
			// перед удалением объекта из памяти
            echo 'Сохранение объекта Person: name='.$this->name.' age='.$this->age;
        }
    }
}

$person = new Person( "Вася", 24 );
$person->setId( 343 );
unset( $person );

Метод __destruct() вызывается каждый раз, когда объект Person удаляется из памяти. Это происходит при вызове функции unset(), которой передается ссылка на удаляемый объект, а также в случае, когда в текущем процессе не остается никаких ссылок на наш объект. Поэтому, если мы создадим объект Person, а затем решим его уничтожить, то увидим, как на самом деле работает метод __destruct().

Хотя такие приемы выглядят очень занимательно, следует добавить нотку предостережения. Методы __call(), __destruct() и им подобные иногда называют магическими. Если вы когда-либо читали произведения жанра "фэнтези", то, наверное, знаете, что магия — это не всегда хорошо. Магия случайна и непредсказуема. Магия нарушает правила. Магия приносит скрытые расходы.

Например, в случае метода __destruct() может случиться так, что программист клиентского кода столкнется с неприятными сюрпризами. Задумайтесь, как работает класс Person — допустим он делает запись в базу данных с помощью своего метода __destruct(). А теперь представьте, что начинающий разработчик решает использовать класс Person, не разобравшись, что к чему. Он не заметил метод __destruct() и собирается создать ряд экземпляров объекта Person. Передавая значения конструктору, он назначает тайную и обидную кличку генерального директора свойству $name и устанавливает для свойства $age значение 150. Разработчик прогоняет тестовый сценарий несколько раз, используй красочные сочетания имени и возраста. А на следующее утро начальник вызывает его к себе в кабинет и просит объяснить почему в базе данных служащих компании содержатся записи типа "Бугор 150". Мораль сей басни такова: не доверяйте магии.

Копирование объектов с помощью метода __clone()

В PHP 4 копирование объекта выполнялось очень просто — достаточно было присвоить значение одной объектной переменной другой:

Код PHP
$product1 = new BookProduct('Собачье сердце', 'Михаил', 'Булгаков', 5.99, 380);
$product2 = $product1;
// В PHP 4 переменные $product1 и $product2 ссылаются на 2 разных объекта 
// Начиная с PHP 5 переменные $product1 и $product2 ссылаются на один объект

Но эта простота часто служила источником множества ошибок, поскольку копии объекта случайно "размножались" при присвоении значений переменным, вызове методов и возврате объектов. Еще больше ухудшало ситуацию то, что не существовало способа протестировать две переменные, чтобы понять, относятся ли они к одному и тому же объекту. С помощью оператора эквивалентности (==) можно было проверить идентичность значений всех полей у двух объектов либо являются ли обе переменные объектами (===). Но в результате этих проверок нельзя было понять, указывают ли обе переменные на один и тот же объект.

В PHP 5 объекты всегда присваиваются и передаются по ссылке. Это означает, что если предыдущий пример запустить в PHP 5, переменные $product1 и $product2 будут содержать ссылки на один и тот же объект, а не на две его копии. И хотя в большинстве случаев при работе с объектами это именно то, что нам нужно, будут ситуации, когда нам понадобится получить копию объекта, а не ссылку на объект. В PHP 5 для этой цели предусмотрено ключевое слово clone. Оно применяется к экземпляру объекта и создает дополнительную копию:

Код PHP
$product1 = new BookProduct('Собачье сердце', 'Михаил', 'Булгаков', 5.99, 380);
$product2 = clone $product1;
// В PHP 5 и более поздних версиях переменные 
// $product1 и $product2 ссылаются на два разных объекта

И здесь вопросы, касающиеся копирования объектов, только начинаются. Рассмотрим класс Person, который мы создали в предыдущем разделе. Стандартная копия объекта Person содержит идентификатор (свойство $id), который при реализации полноценного приложения будет использоваться для нахождения нужной строки в базе данных. Если мы разрешим копировать это свойство, то получим два различных объекта, ссылающихся на один и тот же источник данных, причем, вероятно, не тот, который мы хотели, когда создавали копию. Изменение в одном объекте повлияет на другой и наоборот.

К счастью, мы можем контролировать то, что копируется при использовании перед объектом ключевого слова clone. Для этого следует реализовать специальный метод __clone() (обратите внимание на два символа подчеркивания, с которых начинаются имена всех встроенных методов). Метод __clone() вызывается автоматически, когда для копирования объекта используется ключевое слово clone.

При реализации метода __clone() важно понимать контекст, в котором работает данный метод. Метод __clone() работает в контексте скопированного объекта, а не исходного. Давайте добавим метод __clone() к одной из версий нашего класса Person:

Код PHP
class Person {
    private $name;    
    private $age;    
    private $id;    

    function __construct( $name, $age ) {
        $this->name = $name;
        $this->age = $age;
    }

    function setId( $id ) {
        $this->id = $id;
    }
    
    function __clone() {
        $this->id = 0;
    }
}

Когда оператор clone вызывается для объекта типа Person, создается его новая поверхностная (shallow) копия, и для нее вызывается метод __clone(). Это означает, что все изменения значений свойств, выполняемые в методе __clone(), отразятся только на новой копии объекта. Старые значения, полученные из исходного объекта, будут затерты. В данном случае мы гарантируем, что свойство $id скопированного объекта устанавливается равным нулю:

Код PHP
$person = new Person( "Вася", 24 );
$person->setId( 343 );
$person2 = clone $person;

print_r($person);  // id = 343
echo '<br>';
print_r($person2); // id = 0

Поверхностное копирование гарантирует, что значения элементарных свойств будут скопированы из старого объекта в новый. Однако свойства-объекты также будут скопированы, но по ссылке. Возможно, это не совсем то, что вы хотите, когда клонируете объект. Предположим, мы добавим к объекту Person свойство-объект Account. В этом объекте хранятся данные о состоянии счета, которые мы тоже хотим скопировать в клонированный объект. Однако при этом мы не хотим, чтобы в обоих объектах Person сохранялись ссылки на один и тот же счет:

Код PHP
class Account {
    public $balance;
    function __construct( $balance ) {
        $this->balance = $balance;
    }
}

class Person {
    private $name;    
    private $age;    
    public $account;

    function __construct( $name, $age, Account $account ) {
        $this->name = $name;
        $this->age  = $age;
        $this->account = $account;
    }

    function setId( $id ) {
        $this->id = $id;
    }
    
    function __clone() {
        $this->id = 0;
    }
}

$person = new Person( "Вася", 24, new Account(200) );
$person->setId( 343 );
$person2 = clone $person;

// Добавим $person немного денег
$person->account->balance += 10;

// Это изменение увидит и $person2
print $person2->account->balance;

В результате будет выведено "210".

Объектная переменная $person содержит ссылку на объект типа Account, который мы сделали общедоступным ради компактности (как вы знаете, мы обычно ограничиваем доступ к свойству, создавая в случае необходимости метод доступа). Когда создается клон, он содержит ссылку на тот же самый объект Account, на который ссылается и $person. Мы демонстрируем это, добавляя немного денег к балансу объекта Account переменной $person, а затем распечатывая ее значение с помощью переменной $person2.

Если мы не хотим, чтобы после выполнения операции клонирования в новом объекте осталась ссылка на старое свойство-объект, последнее нужно клонировать явно в методе __clone():

Код PHP
function __clone() {
	$this->account = clone $this->account;
	$this->id = 0;
}

Определение строковых значений для объектов

Еще одна функция, введенная в PHP 5 явно под влиянием строго типизированных объектно-ориентированных языков наподобие Java и C# — метод __toString(). Реализовав метод __toString(), вы можете контролировать то, какую информацию будут выводить объекты при печати. Метод __toString() должен возвращать строковое значение. Этот метод вызывается автоматически, когда объект передается функции print или echo, а возвращаемое им строковое значение будет выведено на экран. Давайте добавим версию метода __toString() к минимальной реализации класса Person:

Код PHP
class Person {
    function getName()  { return "Вася"; }
    function getAge() { return 24; }
	
    function __toString() {
        return $this->getName()." (возраст ". $this->getAge().")";
    }
}

Теперь при печати объекта Person:

Код PHP
$person = new Person();
print $person;

получим "Вася (возраст 24)".

Метод __toString() особенно полезен для записи информации в журнальные файлы (логи) и для выдачи сообщений об ошибках, а также для классов, основная задача которых — передавать информацию. Например, при выводе на печать объекта типа Exception можно выдавать краткий отчет о происшедших исключениях, реализовав в нем метод __toString().

Функции обратного вызова, анонимные функции и механизм замыканий

Несмотря на то что анонимные функции относятся не только к области объектно-ориентированного программирования, они настолько полезны, что я не мог их здесь не упомянуть. Все дело в том, что они активно используются в объектно-ориентированных приложениях наряду с функциями обратного вызова. Более того, в этой области за последнее время было сделано несколько довольно интересных открытий. Для начала давайте определим несколько классов:

Код PHP
class Product {
    public $name;
    public $price;
    function __construct( $name, $price ) {
        $this->name = $name;
        $this->price = $price;
    }
}

class ProcessSale {
    private $callbacks;

    function registerCallback( $callback ) {
        if ( ! is_callable( $callback ) ) {
            throw new Exception( "Функция обратного вызова не вызываемая!" );
        }
        $this->callbacks[] = $callback;
    }

    function sale( $product ) {
        print "{$product->name}: обрабатывается... ";
        
        foreach ( $this->callbacks as $callback ) {
            call_user_func( $callback, $product );
        }
    }
}

Этот код предназначен для запуска нескольких функций обратного вызова. В нем определены два класса. В классе Product просто сохраняются значения свойств $name и $price. Для компактности я объявил их открытыми. Не забудьте в реальном проекте сделать их закрытыми или защищенными и создать для этих свойств методы доступа. В классе ProcessSale определены два метода. Методу registerCallback() передается обычная скалярная переменная без всяких уточнений. После ее проверки, она добавляется в массив функций обратного вызова $callbacks. Процесс тестирования выполняется с помощью встроенной функции is_callable(). В результате гарантируется, что методу registerCallback() будет передано имя функции, которую можно вызвать с помощью таких функций, как call_user_func() или array_walk().

Методу sale() передается объект типа Product. Метод выводит об этом продукте информацию и затем в цикле выполняет перебор элементов массива $callback. Каждый элемент вместе с объектом типа Product передается функции call_user_func(), которая, собственно, и вызывает код, написанный пользователем. Все приведенные ниже примеры будут работать в контексте одной инфраструктуры.

Чем же так полезны функции обратного вызова? Они позволяют в процессе выполнения программы добавлять компоненту новые функциональные возможности, которые напрямую не были связаны с его первоначальной основной функциональностью. Предусмотрев, в объекте работу с функциями обратного вызова, вы тем самым позволите другим программистам расширять функциональность вашего кода, причем при таких обстоятельствах, о которых раньше вы даже и не догадывались.

Представьте себе, например, что через какое-то время пользователь класса ProcessSale захочет создать журнал продаж. Если у пользователя будет доступ к исходному коду класса, он может добавить код фиксации сделок прямо в метод sale(). Однако это не всегда удачное решение. Если этот пользователь не является владельцем пакета, в котором определен класс ProcessSale, то все его исправления будут затерты, когда выйдет его обновленная версия. И даже если он является владельцем пакета, все равно, добавлять в метод sale() дополнительные ветки кода, решающие случайные задачи, неразумно. В конечном итоге это приведет к изменению зоны ответственности класса и потенциально может снизить возможность его повторного использования в других проектах.

Несмотря на это, по счастливой случайности, я заложил в класс ProcessSale возможность обратного вызова. Ниже я создал функцию обратного вызова, которая фиксирует все сделки в журнале продаж:

Код PHP
$logger = create_function( '$product', 
                           'echo " Записываем... ({$product->name})";' );

$processor = new ProcessSale();
$processor->registerCallback( $logger );

$processor->sale( new Product( "Туфли", 6 ) );
echo '<br>';
$processor->sale( new Product( "Кофе", 6 ) );

Здесь для создания функции обратного вызова я воспользовался функцией create_function(). Как видите, ей передаются два параметра. Сначала указывается список параметров функции, а затем — тело самой функции. В результате у нас получилась конструкция, которую часто называют анонимной функцией, поскольку при создании мы не присвоили ей имя, как в обычных функциях. Вместо этого ссылка на вновь созданную функцию сохраняется в переменной, которую затем можно передать в качестве параметра другим функциям и методам. Собственно, это я и сделал в приведенном выше фрагменте кода. Я сохранил ссылку на анонимную функцию в переменной $logger, которую затем передал в качестве параметра методу ProcessSale::registerCallback(). В конце я создал пару объектов, описывающих товары, и передал их по очереди методу sale(). О том, что произойдет дальше, вы уже, наверное, догадались. Каждая сделка по продаже будет обработана (на самом деле будет выведено обычное сообщение о товаре), после чего вызываются все установленные функции обратного вызова, как показано ниже:

Передача анонимной функции в функцию обратного вызова

Давайте еще раз проанализируем пример кода с функцией create_function(). Обратили внимание, насколько уродливо он выглядит? При помещении исполняемого кода в строку всегда возникает головная боль. Для начала вам нужно выполнить экранирование всех символов '$' и '?', которые встречаются в тексте программы. Более того, по мере роста тела функции обратного вызова, ее и в самом деле будет все труднее и труднее проанализировать и понять. Было бы просто замечательно, если бы существовал какой-нибудь более элегантный способ для создания таких функций. Начиная с PHP 5.3 такой способ существует! Теперь вы можете просто объявить функцию как обычно, а затем присвоить ссылку на нее переменной. И все это — в одном операторе! Ниже приведен предыдущий пример, в котором использован новый синтаксис:

Код PHP
$logger = function( $product ) {
	echo " Записываем... ({$product->name})";
};

// ...

Единственное отличие здесь заключается в способе создания переменной, ссылающейся на анонимную функцию. Как видите, этот код намного понятнее. Я указал в операторе присваивания ключевое слово function и не задал имя функции. Обратите внимание на то, что поскольку в операторе присваивания используется встроенная функция, то в конце блока нужно обязательно поместить точку с запятой. Конечно, если ваш код должен работать в одной из предыдущих версий PHP, вам придется продолжать использовать уродливый синтаксис с функцией create_function(). Результат работы нового фрагмента кода ничем не будет отличаться от предыдущего.

Само собой разумеется, что функции обратного вызова не обязательно должны быть анонимными. В качестве такой функции вы смело можете использовать имя обычной функции или даже ссылку на метод какого-либо объекта. Ниже приведен пример:

Код PHP
class Mailer {
    function doMail( $product ) {
        print " Записываем... ({$product->name})\n";
    }
}

$processor = new ProcessSale();
$processor->registerCallback( array( new Mailer(), "doMail" ) );

$processor->sale( new Product( "Туфли", 6 ));
echo '<br>';
$processor->sale( new Product( "Кофе", 6 ));

Здесь я создал новый класс Mailer, содержащий единственный метод doMail(). Этому методу передается объект типа Product, о котором метод выводит сообщение. При вызове метода registerCallback() я передал ему в качестве параметра массив, а не ссылку на функцию обратного вызова, как это было раньше. Первым элементом этого массива является объект типа Mailer, а вторым — строка, содержащая имя метода, который мы хотим вызвать. Помните, что в методе registerCallback() с помощью функции is_callable() выполняется проверка аргумента на предмет того, можно ли его вызвать? Данная функция достаточно интеллектуальна и распознает массивы подобного вида. Поэтому при указании функции обратного вызова в виде массива, в первом элементе такого массива должен находиться объект, содержащий вызываемый метод, а имя этого метода помещается в виде строки во второй элемент массива. Таким образом, мы успешно прошли проверку типа аргумента, и результат выполнения программы будет таким же, как и раньше.

Разумеется, что анонимную функцию можно вернуть из метода, как показано ниже:

Код PHP
class Totalizer {
    static function warnAmount() {
        return function( $product ) {
            if ( $product->price > 5 ) {
                print " покупается дорогой товар: {$product->price}";
            }
        };
    }
}

$processor = new ProcessSale();
$processor->registerCallback( Totalizer::warnAmount());

// ...

В этом примере нет ничего интересного, кроме того, что метод warnAmount() используется в качестве фабрики анонимной функции. Тем не менее подобная структура позволяет мне делать гораздо большее, чем просто генерировать анонимную функцию. Она позволяет мне воспользоваться преимуществами механизма замыкания (closures). В анонимной функции нового типа могут использоваться переменные, объявленные в другой анонимной функции, находящейся в родительской области видимости. Данная концепция очень трудна, чтобы понять ее сразу. Вкратце это можно объяснить так, как будто бы анонимная функция запоминает контекст, в котором она была создана.

Предположим, я хочу сделать так, чтобы метод Totalizer::warnAmount() выполнял следующее. Во-первых, я хочу, чтобы ему можно было передавать пороговое значение стоимости товаров. Во-вторых, я хочу, чтобы он подсчитывал стоимость (т.е. сумму цен) проданного товара. И когда стоимость проданного товара превысит установленный порог, функция должна выполнить некоторые действия (в нашем случае, как вы уже можете догадаться, она просто выведет соответствующее сообщение).

Чтобы анонимная функция могла воспользоваться переменными, определенными в родительской области видимости, используется ключевое слово use, как показано в примере ниже:

Код PHP
class Totalizer {
    static function warnAmount( $amt ) {
        $count=0;
        return function( $product ) use ( $amt, &$count ) {
            $count += $product->price;
            print "  сумма: $count";
            if ( $count > $amt ) {
                print "<br> Продано товаров на сумму: {$count}";
            }
        };
    }
}

$processor = new ProcessSale();
$processor->registerCallback( Totalizer::warnAmount(8));

// ...

В директиве use анонимной функции, которая возвращается методом Totalizer::warnAmount(), указаны две переменные. Первая из них — это $amt, которая является аргументом, переданным методу warnAmount(). Вторая — замкнутая переменная $count. Она объявлена в теле метода warnAmount(), и начальное ее состояние равно нулю. Обратите внимание на то, что перед именем переменной $count в директиве use я указал символ амперсанда '&'. Это означает, что данная переменная будет передаваться в анонимную функцию по ссылке, а не по значению. Дело в том, что в теле анонимной функции я добавляю к ней цену товара и затем сравниваю новую сумму со значением переменной $amt. Если будет достигнуто пороговое значение, выводится соответствующее сообщение, как показано ниже:

Использование замыканий

В этом примере было показано, что значение переменной $count сохраняется между вызовами функции обратного вызова. Обе переменные, и $count и $amt, остаются связанными с этой функцией, поскольку они указаны в контексте ее объявления и перечислены в директиве use.

Наследование классов
Пакеты и пространства имен

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

Результаты поиска по запросу

Система Orphus