objects02 44

Классы и объекты

Первое препятствие для понимания объектно-ориентированного программирования — это странная и удивительная связь между классом и объектом. Для многих людей именно эта связь становится первым моментом откровения, первой искрой интереса к объектно-ориентированному программированию. Поэтому давайте уделим должное внимание основам.

Классы часто описывают с помощью объектов. Это очень интересно, потому что объекты часто описывают с помощью классов. Это хождение по кругу может сделать очень трудными первые шаги в объектно-ориентированном программировании. Поскольку именно классы определяют объекты, мы должны начать с определения классов.

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

Код PHP
class ShopProduct 
{
	// Тело класса
}

Класс ShopProduct из этого примера — уже полноправный класс, хотя пока и не слишком полезный. Но тем не менее мы сделали нечто очень важное. Мы определили тип, т.е. создали категорию данных, которые можем использовать в своих сценариях.

Если класс — это шаблон для создания объектов, следовательно, объект — это данные, которые структурируются в соответствии с шаблоном, определенным в классе. При этом говорят, что объект — это экземпляр класса. Его тип определяется классом. Мы будем использовать класс ShopProduct как форму для создания объектов типа ShopProduct. Для этого нам нужен оператор new. Он используется совместно с именем класса следующим образом:

Код PHP
$product1 = new ShopProduct();
$product2 = new ShopProduct();

После оператора new указывается имя класса в качестве его единственного операнда, в результате он создает экземпляр этого класса; в нашем примере создается объект типа ShopProduct. Итак, мы использовали класс ShopProduct как шаблон для создания двух объектов типа ShopProduct. Хотя функционально они идентичны (т.е. пусты), $product1 и $product2 — это два разных объекта одного типа, созданных с помощью одного класса.

Если вам все еще не понятно, давайте приведем такую аналогию. Представьте, что класс — форма для отливки, с помощью которой изготавливают пластмассовые утки. Объекты — это и есть утки. Тип создаваемых объектов определяется формой отливки. Утки выглядят одинаковыми во всех отношениях, но все-таки это разные предметы. Другими словами, это разные экземпляры одного и того же типа. У уток могут быть даже разные серийные номера, подтверждающие их индивидуальность. Каждому объекту, создаваемому в PHP-сценарии, также присваивается уникальный идентификатор (он уникален на время жизни объекта), т.е. PHP повторно использует идентификаторы, даже в пределах одного и того же процесса (т.е. запуска сценария). Это можно продемонстрировать, выведя на печать объекты $product1 и $product2:

Код PHP
<?php
class ShopProduct 
{
	// Тело класса
}

$product1 = new ShopProduct();
$product2 = new ShopProduct();

var_dump($product1, $product2);
?>

После выполнения этой функции будут выведены следующие данные:

Дамп двух объектов

Передав наши объекты функции var_dump(), мы можем узнать о них полезную информацию, включая внутренний идентификатор каждого объекта, указанный после символа '#'. Чтобы сделать эти объекты более интересными, мы должны немного изменить определение класса ShopProduct, добавив в него специальные поля данных, называемые свойствами (properties).

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

В классах можно определять специальные переменные, которые называются свойствами. Свойство, которое называется также переменной-членом (member variable), содержит данные, которые могут меняться от одного объекта к другому. В случае объектов ShopProduct нам нужно иметь возможность менять, например, поля названия товара и его цены.

Определение свойства в классе похоже на определение обычной переменной, за исключением того, что перед операторами объявления или присвоения нужно поместить одно из ключевых слов, характеризующих область его видимости: public, protected или private.

Область видимости (scope) определяет контекст функции или класса, в котором можно пользоваться данной переменной (или методом, о чем мы поговорим в следующей статье). Так переменная, определенная внутри тела функции, имеет локальную область видимости, а переменная, определенная за пределами функции — глобальную область видимости. Как правило, нельзя получить доступ к данным, находящимся в локализованных областях видимости по отношению к текущей области. Поэтому если вы определяете переменную внутри функции, то впоследствии не сможете получить к ней доступ извне этой функции. Объекты в этом смысле более "проницаемы", и к некоторым объектным переменным можно иногда получать доступ из другого контекста. К каким переменным можно получать доступ и из какого контекста, и определяют ключевые слова public, protected или private.

К этим ключевым словам и вопросу видимости мы вернемся в следующей статье. А сейчас давайте определим некоторые свойства с помощью ключевого слова public:

Код PHP
class ShopProduct 
{
	public $title = "Стандартный товар";
	public $producerMainName = "Фамилия автора";
	public $producerFirstName = "Имя автора";
	public $price = 0;
}

Как видите, мы определили четыре свойства, присвоив каждому из них стандартное значение. Теперь любым объектам, экземпляры которых мы будем создавать с помощью класса ShopProduct, будут присвоены стандартные данные. А ключевое слово public, присутствующее в объявлении каждого свойства, обеспечит доступ к этому свойству извне контекста объекта.

Ключевые слова public, protected и private, определяющие область видимости свойств, появились в PHP 5. В версии PHP 4 приведенный выше пример работать не будет. В PHP 4 все свойства должны быть объявлены с помощью ключевого слова var, что, по сути, идентично использованию ключевого слова public. Исходя из принципа обратной совместимости, в PHP 5 допускается использование для свойств ключевого слова var вместо public.

К переменным свойств можно обращаться с помощью символов '->', указав имя объектной переменной и имя свойства. Поскольку свойства объектов были определены как public, мы можем считывать их значения, а также присваивать им новые значения, заменяя тем самым набор стандартных значений, определенный в классе:

Код PHP
class ShopProduct 
{
	public $title = "Стандартный товар";
	public $producerMainName = "Фамилия автора";
	public $producerFirstName = "Имя автора";
	public $price = 0;
}

$product1 = new ShopProduct();
$product2 = new ShopProduct();

// Изменяем значение по умолчанию объекта $product1
$product1->title = 'Золотые сказки';
$product2->title = 'Собачье сердце';

// Вывести на экран значение свойства
echo $product2->title;

Объявляя и определяя свойство $title в классе ShopProduct, мы гарантируем, что при создании любого объекта типа ShopProduct это свойство будет присутствовать и его значение будет заранее определено. Это означает, что при таком предположении код, использующий данный класс, сможет работать с любыми объектами типа ShopProduct. Но поскольку мы можем легко переопределить это свойство, значение $title может меняться от одного объекта к другому.

На самом деле в PHP необязательно объявлять все свойства в классе. Свойства можно динамически добавлять к объекту следующим образом:

Код PHP
$product1->newProperty = "Новое свойство";

Но нужно отметить, что этот способ присвоения свойств объектам считается дурным тоном в объектно-ориентированном программировании и почти никогда не используется. Но почему динамическое определение свойств — это дурной тон? Создавая класс, вы определяете тип. Вы сообщаете всем о том, что ваш класс (и любой объект, который является его экземпляром) содержит определенный набор полей и функций. Если в классе ShopProduct определяется свойство $title, то любой код, который работает с объектами типа ShopProduct, может исходить из предположения, что свойство $title определено. Но подобной гарантии относительно свойств, определенных динамически, не существует.

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

Код PHP
$product2 = new ShopProduct();

$product2->title = 'Собачье сердце';
$product2->producerMainName = "Булгаков"; 
$product2->producerFirstName = "Михаил";
$product2->price = 5.99;

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

Код PHP
echo "Автор: <b>{$product2->producerFirstName} "."{$product2->producerMainName}</b>";

В результате на выходе получим следующее:

Вывод значений нескольких свойств объекта

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

Код PHP
$product2->producerSecondName = "Булгаков"; 

С точки зрения интерпретатора PHP, этот код абсолютно корректен, поэтому никакого предупреждения об ошибке мы не получим. Но когда понадобится вывести имя автора, мы получим неожиданные результаты. Еще одна проблема — наши объекты, в целом, слишком "нестрогие". Мы не обязаны определять название книги, цену или имя автора. Клиентский код может быть уверен, что эти свойства существуют, но, вполне вероятно, очень часто их стандартные значения не будут вас устраивать. В идеале, следовало бы заставлять всякого, кто создает экземпляры объекта ShopProduct, определять осмысленные значения его свойств.

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

Методы

Так же как свойства позволяют объектам сохранять данные, методы позволяют объектам выполнять задачи. Методы (methods) — это специальные функции, которые объявляются внутри класса. Как и можно было ожидать, объявление метода напоминает объявление функции. За ключевым словом function следует имя метода, а за ним — необязательный список переменных-аргументов в круглых скобках. Тело метода заключается в фигурные скобки:

public function myMethod( $arg1, $arg2, ... argN )
{ 
	// ...
}

В отличие от функций, методы необходимо объявлять в теле класса. При этом можно также указывать ряд спецификаторов, включая ключевое слово, определяющее видимость метода. Как и свойства, методы можно определять как public, protected или private. Объявляя метод как public, мы тем самым обеспечиваем возможность его вызова извне текущего объекта. Если в определении метода вы опустите ключевое слово, определяющее видимость, то метод будет объявлен неявно как public. К модификаторам методов мы вернемся в следующей статье.

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

Код PHP
class ShopProduct 
{
	public $title = "Стандартный товар";
	public $producerMainName = "Фамилия автора";
	public $producerFirstName = "Имя автора";
	public $price = 0;
	
	function getProducer() {
		return "{$this->producerFirstName} {$this->producerMainName}";	
	}
}

$product2 = new ShopProduct();

$product2->title = 'Собачье сердце';
$product2->producerMainName = "Булгаков"; 
$product2->producerFirstName = "Михаил";
$product2->price = 5.99;

// Вызов метода
echo 'Автор: <b>'.$product2->getProducer().'</b>';

В результате на выходе получим результат аналогичный предыдущему.

Мы добавили метод getProducer() к классу ShopProduct. Обратите внимание на то, что при определении метода мы не включили ключевое слово, определяющее его видимость. Это означает, что метод getProducer() относится к типу public и его можно вызвать из-за пределов класса.

При определении метода getProducer() мы воспользовались новой возможностью — псевдопеременной $this. Она представляет собой механизм, посредством которого класс может обратиться к экземпляру объекта. Если вы считаете, что это трудно для понимания, попробуйте заменить $this "текущим экземпляром объекта". Тогда оператор $this->producerFirstName превратится в свойство $producerFirstName текущего экземпляра объекта. Так, метод getProducer() объединяет и возвращает значения свойств $producerFirstName и $producerMainName, избавляя нас от неприятной работы всякий раз, когда нужно вывести полное имя автора.

Итак, нам удалось немного улучшить наш класс. Но для него по-прежнему характерна слишком большая "гибкость". Мы полагаемся на то, что программист будет изменять стандартные значения свойств объекта ShopProduct. Но это проблематично в двух отношениях. Во-первых, нужно пять строк кода, чтобы должным образом инициализировать объект типа ShopProduct, и ни один программист вам не скажет за это спасибо. Во-вторых, у нас нет способа гарантировать, что какое-либо свойство будет определено при инициализации объекта ShopProduct. Поэтому нам нужен метод, который будет вызываться, автоматически при создании экземпляра объекта на основе класса.

Конструктор класса

Метод конструктора вызывается при создании объекта. Его можно использовать, чтобы все настроить, обеспечить определение необходимых свойств и выполнить всю необходимую предварительную работу. До PHP 5 имя метода конструктора совпадало с именем класса, к которому оно относилось. Так, класс ShopProduct мог использовать метод ShopProduct() в качестве своего конструктора. В PHP 5 вы должны назвать метод конструктора __construct(). Обратите внимание на то, что имя метода начинается с двух символов подчеркивания. Это правило наименования действует для многих других специальных методов в PHP-классах. Давайте определим конструктор для класса ShopProduct:

Код PHP
class ShopProduct 
{
	public $title = "Стандартный товар";
	public $producerMainName = "Фамилия автора";
	public $producerFirstName = "Имя автора";
	public $price = 0;
	
	function __construct($title, $firstName, $mainName, $price) {
		$this->title = $title;
		$this->producerMainName = $mainName;
		$this->producerFirstName = $firstName;
		$this->price = $price;
	}
	
	function getProducer() {
		return "{$this->producerFirstName} {$this->producerMainName}";	
	}
}

// Удобная инициализация объектов, за счет использования конструктора
$product1 = new ShopProduct('Золотые сказки', 'Александр', 'Пушкин', 10.99);
$product2 = new ShopProduct('Собачье сердце', 'Михаил', 'Булгаков', 5.99);

// Вызов метода
echo 'Автор: <b>'.$product2->getProducer().'</b>';

И снова мы добавляем к классу функциональность, стараясь сэкономить время и силы программиста и избавить его от необходимости дублирования кода, работающего с этим классом. Метод __construct вызывается, когда создается объект с помощью оператора new. Значения всех перечисленных аргументов при создании объекта передаются конструктору. Так, в нашем примере мы передаем конструктору название произведения, имя и фамилию автора, а также цену. В методе конструктора используется псевдопеременная $this для присвоения значений соответствующим свойствам объекта.

В PHP 4 не распознается метод __construct в качестве конструктора. Если вы используете PHP 4, то для создания конструктора объявите метод, имя которого совпадает с именем содержащего его класса. Поэтому для класса с именем ShopProduct можно объявить конструктор с помощью метода под названием ShopProduct(). В PHP по-прежнему поддерживается эта схема именования конструктора. Но если вам не нужна совместимость со старыми версиями PHP то методы конструктора лучше называть __construct.

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

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

Аргументы и типы

Типы определяют, каким способом можно оперировать данными в сценариях. Например, строковый тип используется для отображения символьных данных и для выполнения операций над такими данными с помощью строковых функций. Целые числа используются в математических выражениях, булевы числа — в логических выражениях и т.д. Эти категории называются элементарными типами данных. Класс также определяет тип имени себя, но на более высоком уровне. Поэтому объект ShopProduct относится к элементарному типу object, а также к типу класса ShopProduct. Более подробно о типах читайте в статье "Типы данных".

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

Мы уже говорили, что переменная-аргумент может содержать любой элементарный тип данных, однако по умолчанию ее тип не оговаривается, поэтому она может содержать объект любого типа. Такая гибкость, с одной стороны, полезна, но, с другой, может стать причиной проблем при определении метода. Рассмотрим метод, предназначенный для работы с объектом типа ShopProduct:

Код PHP
class ShopProductWriter
{
	public function write($shop) {
		$str = "{$shop->title}: <em>{$shop->getProducer()}</em> ({$shop->price})";
		echo $str;
	}
}

Мы можем протестировать этот класс следующим образом:

Код PHP
$product2 = new ShopProduct('Собачье сердце', 'Михаил', 'Булгаков', 5.99);

$writer = new ShopProductWriter();
$writer->write($product2);

Тогда на выходе получим следующее:

Вызов метода вспомогательного объекта

Возможно, вас заинтересует, почему мы не добавили метод write() непосредственно в класс ShopProduct. Ответом на поставленный вопрос будет следующее: все дело в ответственности. Класс ShopProduct ответственен за хранение данных о товаре, а класс ShopProductWriter — за вывод этих данных. Позже вы начнете понимать, в чем польза такого разделения ответственности.

Класс ShopProductWriter содержит единственный метод - write(). Методу write() передается объект типа ShopProduct. В нем используются свойства и методы последнего для построения и вывода результирующей строки описания товара. Мы используем имя переменной-аргумента, $shop, как напоминание программисту о том, что методу $write() нужно передать объект типа ShopProduct. Но это требование не является обязательным. Это значит, что я могу передать некорректный объект или элементарный тип методу $write() и ничего об этом не узнать до момента обращения к аргументу $shop. К тому времени в нашем коде уже могут быть выполнены какие-либо действия так, как если бы мы передали методу настоящий объект типа ShopProduct.

Для решения описанной проблемы в PHP 5 появилась новая возможность — уточнение типов данных класса. Чтобы добавить уточнение типа к аргументу метода, просто поместите перед ним имя класса. Поэтому метод write() можно изменить следующим образом:

Код PHP
class ShopProductWriter
{
	public function write(ShopProduct $shop) {
		...
	}
}

Теперь методу write() можно передавать аргумент $shop, содержащий только объект типа ShopProduct. Передача любого другого типа приведет к неустранимой ошибке "Catchable fatal error".

Теперь нам не нужно каждый раз при вызове метода проверять тип передаваемого ему аргумента. Кроме того, использование уточнений делает запись метода намного понятнее для программиста клиентского кода. Он сразу же увидит требования метода write(). Программисту не нужно будет волноваться по поводу незаметных ошибок, возникающих в результате несоответствия типов аргументов, поскольку благодаря уточнению они определяются принудительно и строго.

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

Уточнения нельзя использовать для принудительного определения аргументов элементарных типов, таких как строки и целые значения. Для этой цели в теле методов следует использовать функции проверки типов, такие как is_int(). Но можно принудительно определить, что аргумент является массивом:

Код PHP
function setArray( array $storearray ) { 
	...
}

Поддержка уточнения для массивов была добавлена в PHP начиная с версии 5.1. В дальнейшем также была добавлена поддержка нулевых стандартных значений в аргументах с уточнениями. Это означает, что можно требовать, чтобы аргумент был либо определенного типа, либо нулевым значением. Вот как это сделать:

Код PHP

function setWriter( ObjectWriter $obj_writer = null ) 
{
	...
}

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

PHP и объекты
Наследование классов

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

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

Система Orphus