objects03 14

Наследование классов

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

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

Код PHP
class ShopProduct 
{
	public $title;
	public $producerMainName;
	public $producerFirstName;
	public $price;
	
	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('Собачье сердце', 'Михаил', 'Булгаков', 5.99);
$product2 = new ShopProduct('Первый снег', 'Группа', 'Моральный кодекс', 2.99);

echo "Автор: <b>".$product1->getProducer()."</b><br>";
echo "Исполнитель: <b>".$product2->getProducer()."</b>";

На выходе получаем следующее:

Вывод двух объектов различающихся семантически (по смыслу)

Как видим, разделение имени автора на две части пригодилось нам при работе и с книгами, и с компакт-дисками. В этом случае мы можем сортировать товары по фамилии автора (т.е. по полю, содержащему "Булгаков" и "Моральный кодекс"), а не по имени, в котором содержатся малозначимые "Михаил" и "Группа". Лень — это отличная стратегия проектирования, поэтому на данном этапе вам не следует переживать по поводу использования класса ShopProduct для более чем одного типа товара.

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

Давайте рассмотрим первый подход. Итак, мы объединяем данные о книгах и компакт-дисках в одном классе:

Код PHP
class ShopProduct 
{
	public $title;
	public $producerMainName;
	public $producerFirstName;
	public $price;
	
	// Два новых свойства
	public $playLength;
	public $numPages;
	
	function __construct($title, $firstName, $mainName, $price, $numPages = 0, $playLength = 0) {
		$this->title = $title;
		$this->producerMainName = $mainName;
		$this->producerFirstName = $firstName;
		$this->price = $price;
		
		// Расширяем конструктор
		$this->numPages = $numPages;
		$this->playLength = $playLength;
	}
	
	function getProducer() {
		return "{$this->producerFirstName} {$this->producerMainName}";	
	}
	
	// Два новых метода
	function getNumberOfPages() {
		return $this->numPages;
	}
	
	function getPlayLength() {
		return $this->playLength;
	}
}

Чтобы продемонстрировать большой объем выполняемой работы по кодированию, в данном примере были использованы методы доступа к свойствам $numPages и $playLength. В результате объект, экземпляр которого создается с помощью такого класса, будет всегда содержать избыточные методы. Кроме того, для CD экземпляр объекта нужно создавать с помощью бессмысленного аргумента конструктора. Таким образом, для CD будет сохраняться информация и функциональные возможности класса, относящиеся к книгам (общее количество страниц), а для книг — данные о времени звучания CD. Вероятно, пока вы можете с этим смириться. Но что будет, если мы добавим больше типов товаров, причем каждый — с собственными методами, а затем добавим больше методов для каждого типа? Наш класс будет становиться все более сложным и трудным для использования.

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

Код PHP
	function getSummaryLine() {
		$base = "{$this->title} ({$this->producerMainName}, {$this->producerFirstName})";
		if ($this->type == 'book')
			$base .= ": {$this->numPages} стр.";
		else if ($this->type == 'cd')
			$base .= ": Продолжительность - {$this->playLength}";
		return $base;
	}

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

Поскольку ShopProduct начинает напоминать "два класса в одном", мы должны это признать и создать два типа вместо одного. Однако если мы будем создавать два отдельных класса (например, CDProduct и BookProduct) то возникнет чрезмерное дублирование, т.к. в обоих классах содержаться некоторые одинаковые свойства и методы. Нам нужно создать общую функциональность в одном месте, чтобы избежать дублирования, но в то же время сделать так, чтобы при вызове метода, выводящего краткую информацию о товаре, учитывались особенности форматирования. Одним словом, нам необходимо использовать наследование.

Первый шаг в построении дерева наследования — найти элементы базового класса, которые не соответствуют друг другу или которыми нужно оперировать иначе. Мы знаем, что методы getPlayLength() и getNumberOfPages() противоречат друг другу. Нам также известно, что нужно создать разные реализации метода getSummaryLine(). Давайте используем эти различия как основу для создания двух производных классов:

Код PHP
class ShopProduct 
{
	public $title;
	public $producerMainName;
	public $producerFirstName;
	public $price;
	public $playLength;
	public $numPages;
	
	function __construct($title, $firstName, $mainName, $price, $numPages = 0, $playLength = 0) {
		$this->title = $title;
		$this->producerMainName = $mainName;
		$this->producerFirstName = $firstName;
		$this->price = $price;
		$this->numPages = $numPages;
		$this->playLength = $playLength;
	}
	
	function getProducer() {
		return "{$this->producerFirstName} {$this->producerMainName}";	
	}	
	
	function getSummaryLine() {
		$base = "{$this->title} ({$this->producerMainName}, {$this->producerFirstName})";
		return $base;
	}
}

class CDProduct extends ShopProduct
{
	function getPlayLength() {
		return $this->playLength;
	}
	
	function getSummaryLine() {
		$base = "{$this->title} ({$this->producerMainName}, {$this->producerFirstName})";
		$base .= ": Продолжительность - {$this->playLength}";
		return $base;
	}
}

class BookProduct extends ShopProduct
{
	function getNumberOfPages() {
		return $this->numPages;
	}
	
	function getSummaryLine() {
		$base = "{$this->title} ({$this->producerMainName}, {$this->producerFirstName})";
		$base .= ": {$this->numPages} стр.";
		return $base;
	}
}

Чтобы создать дочерний класс, необходимо использовать в объявлении класса ключевое слово extends. В данном примере мы создали два новых класса, BookProduct и CDProduct. Оба они расширяют класс ShopProduct.

Поскольку в производных классах конструкторы не определяются, при создании экземпляров объектов этих классов будет автоматически вызываться конструктор родительского класса. Дочерние классы наследуют доступ ко всем методам типа public и protected родительского класса (но не к методам и свойствам типа private). Это означает, что мы можем вызвать метод getProducer() для экземпляра объекта класса CDProduct, хотя метод getProducer() определен в классе ShopProduct:

Код PHP
$product2 = new CDProduct('Первый снег', 'Группа', 
				'Моральный кодекс', 2.99, null, 55.01);

echo "Исполнитель: <b>".$product2->getProducer()."</b>";

Таким образом, оба наших дочерних класса наследуют поведение общего родительского класса. И мы можем обращаться с объектом BookProduct так, как будто это объект типа ShopProduct. Обратите внимание на то, что для обеспечения собственной реализации в обоих классах CDProduct и BookProduct переопределяется метод getSummaryLine(). Производные классы могут расширять и изменять функциональность родительских классов. И в то же время каждый класс наследует свойства родительского класса.

Реализация этого метода в Суперклассе может показаться избыточной, поскольку метод переопределяется в обоих дочерних классах. Тем не менее мы предоставляем базовый набор функциональных возможностей, который можно будет использовать в новом дочернем классе. Наличие этого метода в суперклассе также гарантирует для клиентского кода, что во всех объектах типа ShopProduct будет присутствовать метод getSummaryLine(). Позже вы увидите, как можно выполнить это требование в базовом классе, не предоставляя никакой его реализации. Каждый дочерний объект класса ShopProduct унаследует все свойства своего родителя. В собственных реализациях метода getSummaryLine() для обоих классов CDProduct и BookProduct обеспечивается доступ к свойству $title.

С понятием наследования сразу разобраться непросто. Определяя класс, который расширяет другой класс, мы гарантируем, что экземпляр его объекта определяется характеристиками сначала дочернего, а затем — родительского класса. Чтобы понять это, нужно размышлять с точки зрения поиска. При вызове $product2->getProducer() интерпретатор PHP не может найти такой метод в классе CDProduct. Поиск заканчивается неудачей, и поэтому используется стандартная реализация этого метода, заданная в классе ShopProduct. С другой стороны, когда мы вызываем $product2->getSummaryLine(), то интерпретатор PHP находит реализацию метода getSummaryLine() в классе CDProduct и вызывает его.

То же самое верно и в отношении доступа к свойствам. При обращении к свойству $title в методе getSummaryLine() из класса BookProduct, интерпретатор PHP не находит определение этого свойства в классе BookProduct. Поэтому он использует определение данного свойства, заданное в родительском классе ShopProduct. Поскольку свойство $title используется в обоих подклассах, следовательно, оно должно определяться в суперклассе.

Даже поверхностного взгляда на конструктор ShopProduct достаточно, чтобы понять, что в базовом классе по-прежнему выполняется доступ к тем данным, которыми должен оперировать дочерний класс. Так, конструктору класса BookProduct должен передаваться аргумент $numPages, значение которого заносится в одноименное свойство, а конструктор класса CDProduct должен обрабатывать аргумент и свойство $playLength. Чтобы добиться этого, мы определим методы конструктора в каждом дочернем классе.

Конструкторы и наследование

При определении конструктора в дочернем классе вы берете на себя ответственность за передачу требуемых аргументов родительскому классу. Если же вы этого не сделаете, то у вас получится частично сконструированный объект. Чтобы вызвать нужный метод из родительского класса, вам понадобится обратиться к самому этому классу через дескриптор. Для этой цели в PHP предусмотрено ключевое слово parent.

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

Код PHP
class ShopProduct 
{
	public $title;
	public $producerMainName;
	public $producerFirstName;
	public $price;
	// Здесь мы удалили два избыточных свойства
	
	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}";	
	}	
	
	function getSummaryLine() {
		$base = "{$this->title} ({$this->producerMainName}, {$this->producerFirstName})";
		return $base;
	}
}

class CDProduct extends ShopProduct
{
	public $playLength;
	
	// Конструктор класса CDProduct
	function __construct ($title, $firstName, $mainName, $price, $playLength) {
		// Вызов базового конструктора
		parent::__construct($title, $firstName, $mainName, $price);
		$this->playLength = $playLength;
	}
	
	function getPlayLength() {
		return $this->playLength;
	}
	
	function getSummaryLine() {
		$base = "{$this->title} ({$this->producerMainName}, {$this->producerFirstName})";
		$base .= ": Продолжительность - {$this->playLength}";
		return $base;
	}
}

class BookProduct extends ShopProduct
{
	public $numPages;
	
	// Конструктор класса BookProduct
	function __construct ($title, $firstName, $mainName, $price, $numPages) {
		// Вызов базового конструктора
		parent::__construct($title, $firstName, $mainName, $price);
		$this->numPages = $numPages;
	}
	
	function getNumberOfPages() {
		return $this->numPages;
	}
	
	function getSummaryLine() {
		$base = "{$this->title} ({$this->producerMainName}, {$this->producerFirstName})";
		$base .= ": {$this->numPages} стр.";
		return $base;
	}
}

Каждый дочерний класс вызывает конструктор своего родительского класса, прежде чем определять собственные свойства. Базовый класс теперь "знает" только о собственных данных. Дочерние классы — это обычно "специализации" родительских классов. Как правило, следует избегать того, чтобы давать родительским классам какие-либо особые "знания" о дочерних классах. В случае изменения иерархии классов это часто приводило к проблемам. Множество ошибок возникало из-за того, что программисты, после непосредственного изменения "родителя" класса, забывали обновить сам конструктор. А при использовании унифицированного конструктора вызов родительского конструктора parent::__construct() означает обращение непосредственно к родительскому классу, независимо от того, какие изменения произошли в иерархии классов. Но, конечно, нужно позаботиться о том, чтобы этому родительскому классу были переданы правильные аргументы!

Вызов переопределенного метода

Ключевое слово parent можно использовать в любом методе, который переопределяет свой эквивалент в родительском классе. Когда мы переопределяем метод, то, возможно, хотим не удалить функции "родителя", а, скорее, расширить их. Достичь этого можно, вызвав метод родительского класса в контексте текущего объекта. Если вы снова посмотрите на реализации метода getSummaryLine(), то увидите, что значительная часть кода в них дублируется. И лучше этим воспользоваться, чем воспроизводить функциональность, уже разработанную в классе ShopProduct:

Код PHP
class ShopProduct 
{
	...
	
	function getSummaryLine() {
		$base = "{$this->title} ({$this->producerMainName}, {$this->producerFirstName})";
		return $base;
	}
}

class CDProduct extends ShopProduct
{
	...
	
	function getSummaryLine() {
		$base = parent::getSummaryLine().": Продолжительность - {$this->playLength}";
		return $base;
	}
}

class BookProduct extends ShopProduct
{
	...
	
	function getSummaryLine() {
		$base = parent::getSummaryLine().": {$this->numPages} стр.";
		return $base;
	}
}

Мы определили основные функции для метода getSummaryLine() в базовом классе ShopProduct. Вместо того чтобы воспроизводить их в подклассах CDProduct и BookProduct, мы просто вызовем родительский метод, прежде чем добавлять дополнительные данные к итоговой строке.

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

Модификаторы Public, Private и Protected: управление доступом к классам

До сих пор мы явно или неявно объявляли все свойства как public (общедоступные). Такой тип доступа задан по умолчанию для всех методов, а также свойств, объявленных с использованием устаревшего ключевого слова var. Элементы класса можно объявить как public (общедоступные), private (закрытые) или protected (защищенные). Ниже описана разница между ними:

  • К общедоступным свойствам и методам можно получать доступ из любого контекста.

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

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

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

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

Код PHP
class ShopProduct 
{
	...
	public $discount = 0;
	
	...
	
	function setDiscount($num) {
		$this->discount = $num;
	}
}

Но тут у нас есть проблема. Мы хотим показать всем только скорректированную цену, но клиент может легко обойти метод getPrice() (если определить такой метод) и получить доступ к свойству $price:

Код PHP
echo "Цена - {$product1->price}";

В результате будет выведена исходная цена, а не цена со скидкой, которую мы хотим представить. Чтобы предотвратить это, можно просто закрыть свойство $price. Это позволит запретить клиентам прямой доступ к нему, заставляя использовать метод getPrice(). Любая попытка получить доступ к свойству $price из-за пределов класса ShopProduct закончится неудачей. В результате для внешнего мира это свойство прекратит существование.

Но определение свойств как private — не всегда удачная стратегия, поскольку тогда дочерний класс не сможет получить доступ к закрытым свойствам. А теперь представьте, что правила вашего бизнеса таковы: при покупке только книг скидку на них делать нельзя. Мы можем переопределить метод getPrice(), чтобы он возвращал свойство $price без применения скидки:

Код PHP
// Класс BookProduct
function getPrice () {
	return $this->price;
}

Поскольку свойство $price объявлено в классе ShopProduct с модификатором private, а не в BookProduct, попытка в приведенном выше коде получить к нему доступ закончится неудачей. Чтобы решить эту проблему, нужно объявить свойство $price защищенным (protected) и тем самым предоставить доступ к нему дочерним классам. Помните, что к защищенному свойству или методу нельзя получить доступ из-за пределов иерархии того класса, в котором это свойство или метод были объявлены. Доступ к ним можно получить только из исходного класса или его дочерних классов.

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

Методы как средство доступа к свойствам

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

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

В предыдущей статье мы приводили пример класса ShopProductWriter, с помощью которого выводилась информация об объектах типа ShopProduct. Давайте попробуем пойти дальше и сделать так, чтобы класс ShopProductWriter мог выводить информацию о любом количестве объектов типа ShopProduct одновременно:

Код PHP
class ShopProductWriter 
{
	public $products = array();
	
	public function addProduct(ShopProduct $shopProduct) {
		$this->products[] = $shopProduct;
	}
	
	public function write() {
		$str = "";
		foreach ($this->products as $product)
		{
			$str .= "{$product->title}: <b>{$product->getProducer()}</b>; {$product->getPrice()}$<br>";
		}
		echo $str;
	}
}


$product1 = new BookProduct('Собачье сердце', 'Михаил', 'Булгаков', 5.99, 380);
$product2 = new CDProduct('Первый снег', 'Группа', 
				'Моральный кодекс', 2.99, 55.01);

$write = new ShopProductWriter();
$write->addProduct($product1);
$write->addProduct($product2);
$write->write();
Вывод информации о всех объектах класса ShopProduct с помощью вспомогательного класса ShopProductWriter

Теперь класс ShopProductWriter стал намного полезнее. Он может содержать много объектов типа ShopProduct и сразу выводить информацию обо всех их. Но мы все еще должны полагаться на то, что программисты клиентского кода будут строго придерживаться правил работы с классом. Хотя мы предоставили метод addProduct(), мы не запретили программистам непосредственно выполнять операции над свойством $products. В результате можно не только добавить объект неправильного типа к массиву свойств $products, но и затереть весь массив и заменить его значением элементарного типа. Чтобы не допустить этого, нужно сделать свойство $products закрытым:

Код PHP
class ShopProductWriter 
{
	private $products = array();
	
	...
}

Теперь внешний код не сможет повредить массив свойств $products. Весь доступ к нему должен осуществляться через метод addProduct(), а уточнения типа класса, которые используются в объявлении этого метода, гарантируют, что к массиву свойств могут быть добавлены только объекты типа ShopProduct.

И в заключение давайте изменим класс ShopProduct и его дочерние классы так, чтобы ограничить доступ к свойствам:

Код PHP
class ShopProduct 
{
	private $title;
	private $producerMainName;
	private $producerFirstName;
	private $discount;
	protected $price;
	
	function __construct($title, $firstName, $mainName, $price) {
		$this->title = $title;
		$this->producerMainName = $mainName;
		$this->producerFirstName = $firstName;
		$this->price = $price;
	}
	
	public function getPrice () {
		return $this->price;
	}
	
	public function getProducerMainName() {
		return $this->producerMainName;
	}
	
	public function getProducerFirstName() {
		return $this->producerFirstName;
	}
	
	public function setDiscount( $num ) {
		$this->discount=$num;
	}

    public function getDiscount() {
		return $this->discount;
    }

    public function getTitle() {
		return $this->title;
    }
	
	public function getProducer() {
		return "{$this->producerFirstName} {$this->producerMainName}";	
	}	
	
	function getSummaryLine() {
		$base = "{$this->title} ({$this->producerMainName}, {$this->producerFirstName})";
		return $base;
	}
}

class CDProduct extends ShopProduct
{
	private $playLength = 0;
	
	function __construct ($title, $firstName, $mainName, $price, $playLength) {
		parent::__construct($title, $firstName, $mainName, $price);
		$this->playLength = $playLength;
	}
	
	function getPlayLength() {
		return $this->playLength;
	}
	
	function getSummaryLine() {
		$base = parent::getSummaryLine().": Продолжительность - {$this->playLength}";
		return $base;
	}
}

class BookProduct extends ShopProduct
{
	private $numPages = 0;
	
	function __construct ($title, $firstName, $mainName, $price, $numPages) {
		parent::__construct($title, $firstName, $mainName, $price);
		$this->numPages = $numPages;
	}
	
	function getNumberOfPages() {
		return $this->numPages;
	}
	
	function getSummaryLine() {
		$base = parent::getSummaryLine().": {$this->numPages} стр.";
		return $base;
	}
}

class ShopProductWriter 
{
	private $products = array();
	
	public function addProduct(ShopProduct $shopProduct) {
		$this->products[] = $shopProduct;
	}
	
	public function write() {
		$str = "";
		foreach ($this->products as $product)
		{
			$str .= "{$product->getTitle()}: <b>{$product->getProducer()}</b>; {$product->getPrice()}$<br>";
		}
		echo $str;
	}
}

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

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

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

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

Система Orphus