Объектно-ориентированный PHP: работа с наследованием. Наследование классов в PHP

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

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

Чтобы создать подкласс, необходимо использовать в объявлении класса ключевое слово extends , и после него указать имя класса, от которого выполняется наследование:

age = $age; } function add_age () { $this->age++; } } // объявляем наследуемый класс class my_Cat extends Cat { // определяем собственный метод подкласса function sleep() { echo "
Zzzzz..."; } } $kitty = new my_Cat(10); // вызываем наследуемый метод $kitty->add_age(); // считываем значение наследуемого свойства echo $kitty->age; // вызываем собственный метод подкласса $kitty->sleep(); ?>

Подкласс наследует доступ ко всем методам и свойствам родительского класса, так как они имеют тип public . Это означает, что для экземпляров класса my_Cat мы можем вызывать метод add_age() и обращаться к свойству $age не смотря на то, что они определены в классе cat . Также в приведенном примере подкласс не имеет своего конструктора. Если в подклассе не объявлен свой конструктор, то при создании экземпляров подкласса будет автоматически вызываться конструктор суперкласса.

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

age"; } } class my_Cat extends Cat { public $age = 10; } $kitty = new my_Cat; $kitty->foo(); ?>

При вызове $kitty->foo() интерпретатор PHP не может найти такой метод в классе my_Cat , поэтому используется реализация этого метода заданная в классе Cat . Однако в подклассе определено собственное свойство $age , поэтому при обращении к нему в методе $kitty->foo() , интерпретатор PHP находит это свойство в классе my_Cat и использует его.

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

foo(new my_Cat); ?>

Мы можем обращаться с экземпляром класса my_Cat так, как будто это объект типа Cat , т.е. мы можем передать объект типа my_Cat методу foo() класса Cat , и все будет работать, как надо.

Оператор parent

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

Чтобы вызвать нужный метод из родительского класса, вам понадобится обратиться к самому этому классу через дескриптор. Для этой цели в PHP предусмотрено ключевое слово parent . Оператор parent позволяет подклассам обращаться к методам (и конструкторам) родительского класса и дополнять их существующую функциональность. Чтобы обратиться к методу в контексте класса, используются символы " :: " (два двоеточия). Синтаксис оператора parent:

Parent::метод_родительского_класа

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

title = $title; $this->price = $price; } } class new_book extends book { public $pages; function __construct($title, $price, $pages) { // вызываем метод-конструктор родительского класса parent::__construct($title, $price); // инициализируем свойство определенное в подклассе $this->pages = $pages; } } $obj = new new_book("азбука", 35, 500); echo "Книга: $obj->title
Цена: $obj->price
Страниц: $obj->pages"; ?>

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

Ключевое слово parent можно использовать не только в конструкторах, но и в любом другом методе, функциональность которого вы хотите расширить, достигнуть этого можно, вызвав метод родительского класса:

name}."; return $str; } } class my_Cat extends Cat { public $age = 5; function getstr() { $str = parent::getstr(); $str .= "
Возраст: {$this->age} лет."; return $str; } } $obj = new my_Cat; echo $obj->getstr(); ?>

Здесь сначала вызывается метод getstr() из суперкласса, значение которого присваивается переменной, а после этого выполняется остальной код определенный в методе подкласса.

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

public, protected и private: управление доступом

До этого момента мы явно объявляли все свойства как public (общедоступные). И такой тип доступа задан по умолчанию для всех методов.

Элементы класса можно объявлять как public (общедоступные), protected (защищенные) и private (закрытые). Рассмотрим разницу между ними:

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

public - открытый доступ:

hello"; } } $obj = new human; // доступ из вызывающей программы echo "$obj->age"; // Допустимо $obj->say(); // Допустимо?>

private - доступ только из методов класса:

age"; } } $obj = new human; // напрямую из вызывающей программы доступа к закрытым данным нет echo "$obj->age"; // Ошибка! доступ закрыт! // однако с помощью метода можно выводить закрытые данные $obj->say(); // Допустимо?>

protected - защищенный доступ:

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

Наследование - это один из "трёх китов " ООП , который позволяет создавать дочерние классы на основе родительских (супер-классов ), забрав у них свойства, методы и конструкторы. Хочется заметить, что наследование классов в PHP встречается только уже в серьёзных проектах. Вдобавок, новички наследование редко используют вообще, но тем не менее. Когда я делал свой движок для сайта (а он достаточно большой), то использовал наследование классов в PHP на полную катушку. И о том, как это делается, я и напишу в этой статье.

Давайте для начала создадим какой-нибудь несложный класс, например, класс, отвечающий за объект "Автомобиль " (в файле "car.php "):

class Car {
public $x;
public $y;
public function __construct($x, $y) {
$this->x = $x;
$this->y = $y;
}
public function move ($x, $y) {
$this->sound();
echo "Движение автомобиля из координат ($this->x, $this->y) в координаты ($x, $y)
";
$this->x = $x;
$this->y = $y;
}
public function sound() {
echo "Звук движения автомобиля
";
}
}
?>

В классе "Car " мы определили два свойства, отвечающие за текущие координаты местоположения автомобиля. Создали конструктор, который позволяет назначить начальные координаты. Затем создали метод move() , позволяющий начать движение в координаты, переданные в параметрах метода. Внутри этого метода мы вызываем метод sound() , который запускает звук движения. Вот такой придуманный класс.

Теперь создадим класс, отвечающий за "Легковой автомобиль " (в файле "auto.php "):

require_once "car.php";
class Auto extends Car {
public function move($x, $y) {
$this->sound();
echo "Движение легкового автомобиля из координат ($this->x, $this->y) в координаты ($x, $y)
";
$this->x = $x;
$this->y = $y;
}
public function sound() {
echo "Звук движения легкового автомобиля
";
}
}
?>

В самом начале мы подключаем наш класс "Car ". Затем мы начинаем создавать класс "Auto ", который является наследником для класса "Car " ("class Auto extends Car "). Ввиду того, что класс "Auto " уже забирает все свойства, методы и конструкторы, то нам нет необходимости их описывать заново. Однако, методы move() и sound() должны иметь другую реализацию, поэтому мы пишем другой код этих методов.

И, напоследок, создадим скрипт, который создаст объект "Car " и "Auto " и воспользуемся их методами и свойствами:

require_once "auto.php";
$car = new Car(10, 20);
echo $car->x;
echo "
";
echo $car->y;
echo "
";
$car->move(15, 5);
$auto = new Auto(5, 10);
echo $auto->x;
echo "
";
echo $auto->y;
echo "
";
$auto->move(0, 0);
?>

Вначале всё просто: мы создаём объект "Car ", выводим его свойства, используем его метод move() . А вот затем мы создаём объект "Auto ". Ввиду того, что мы не определяли своего конструктора в этом классе, то он берётся из родительского. Затем выводим свойства, которые мы, кстати, также не определяли, и они тоже берутся из родительского класса. А потом используем метод move() . И, разумеется, берётся реализация не из класса "Car ", а из класса "Auto ", который отвечает у нас за легковой автомобиль.

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

Кстати, даю домашнее задание: создать класс, отвечающий за "Грузовой автомобиль " (по аналогии с "Легковым автомобилем "), и который будет наследоваться от класса "Автомобиль ".

А наследование классов в PHP на этом не заканчивается, и уже в следующей статье мы продолжим эту тему.

В этом уроке мы поговорим о наследовании в ООП и о том, как оно работает в PHP. С помощью наследования можно сделать классы намного сильнее и гибче, а также сэкономить уйму времени на написание скриптов.

Мы рассмотрим следующее:

  • Концепцию наследования, и почему его полезно использовать;
  • Как один PHP класс может наследоваться от другого;
  • Как один из “детей” класса может перегружать функциональность методов своего “родителя”;
  • Работа с методами и классами final;
  • Использование абстрактных классов;
  • Работа с интерфейсами.

Готовы? Тогда вперед!

Как осуществляется наследование?

Наследование базируется на понятиях класс-родитель и класс-наследник. Используя определенный синтаксис, вы можете создать класс, который будет наследовать другой класс (становится его наследником).

На заметку: классы-родители также называют базовыми классами или супер-классами. Классы-наследники, в свою очередь, можно назвать дочерними классами или подклассами.

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

К примеру, в веб-приложении форума есть класс Member, у которого есть методы createPost(), editProfile(), showProfile() и др. Так как администраторы форума также являются его членами, то вы можете создать класс Administrator - дочерний класса Member. Класс Administrator наследует все поля и методы класса Member, а значит, объект класса Administrator будет вести себя точно так же, как объект Member.

Вы можете расширить функциональность класса Administrator, добавив в него такие методы, как createForum(), deleteForm() и banMember(). А если хотите назначать роли еще и администраторам, то добавьте в данный дочерний класс поле, например, $adminLevel.

Действуя таким образом, вы не захламляете класс Member методами и полями, которые не подходят для обычных участников форума, а только для администраторов. Вам также не придется копировать и вставлять методы и поля из класса Member в дочерний класс. Так что наследование подойдет вам как нельзя лучше.

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

Создание дочерних классов

Итак, как же создать класс, который станет наследником другого класса, в PHP? Для этого существует ключевое слово extends:

Class ParentClass { // описание полей и методов } class ChildClass extends ParentClass { // описание дополнительных полей и методов }

Мы создали класс ParentClass, а затем класса ChildClass, который наследуется от ParentClass. ChildClass наследует все поля и методы класса ParentClass, и в него также могут быть добавлены свои поля и методы.

А теперь - пример. Создадим класс Member для воображаемого веб-форума, а затем класс Administrator - дочерний от Member:

Class Member { public $username = ""; private $loggedIn = false; public function login() { $this->loggedIn = true; } public function logout() { $this->loggedIn = false; } public function isLoggedIn() { return $this->loggedIn; } } class Administrator extends Member { public function createForum($forumName) { echo "$this->username created a new forum: $forumName
"; } public function banMember($member) { echo "$this->username banned the member: $member->username
"; } }

Как видите, класс Member содержит поле public $username и поле private $loggedIn, а также методы для входа-выхода из форума и метод для определения того, зашел ли пользователь или нет.

  • createForum($forumName) для создания нового форума с названием $forumName;
  • banMember($member) для бана пользователя $member.

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

Давайте посмотрим на наши классы в действии. Создадим по одному объекту обоих классов, а затем вызовем некоторые из их методов:

// создаем участника форума и логиним его $member = new Member(); $member->username = "Fred"; $member->login(); echo $member->username . " is " . ($member->
"; // создаем администратора и логиним его $admin = new Administrator(); $admin->username = "Mary"; $admin->login(); echo $admin->username . " is " . ($member->isLoggedIn() ? "logged in" : "logged out") . "
"; // отобразит "Mary created a new forum: Teddy Bears" $admin->createForum("Teddy Bears"); // отобразит "Mary banned the member: Fred" $admin->banMember($member);

На странице отобразится:

Fred is logged in Mary is logged in Mary created a new forum: Teddy Bears Mary banned the member: Fred

Вот, как это работает:

  1. Сначала мы создали объект класса Member, задали имя пользователя “Fred”, залогинили его и отобразили на странице сообщение о том, что он вошел на форум.
  2. Затем создаем объект класса Administrator. Так как данный класс наследуется от Member, мы можем пользоваться всеми методами и полями этого класса для объектов класса Administrator. Мы даем имя администратору - Mary - и логиним ее, после чего отображаем сообщение о том, что она вошла.
  3. Теперь вызываем метод класса Administrator createForum(), передав в него название форума - “Teddy Bears”.
  4. Наконец, вызываем метод banMember() от объекта - админа, передав имя пользователя Fred.

В этом и заключается суть наследования в ООП. Дальше в уроке мы рассмотрим различные способы манипулирования наследованием, включая перегрузку, классы и методы final, абстрактные классы и интерфейсы.

Перегрузка родительских методов

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

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

Перегрузкой метода login() в дочернем классе, вы можете изменить данный метод по своему усмотрению.

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

Class ParentClass { public function myMethod() { // (действия) } } class ChildClass extends ParentClass { public function myMethod() { // вызывется для объекта класса ChildClass // вместо метода супер-класса MyMethod() } }

Давайте перегрузим метод login() для класса Administrator так, чтобы в файл записывались логи:

Class Member { public $username = ""; private $loggedIn = false; public function login() { $this->loggedIn = true; } public function logout() { $this->loggedIn = false; } } class Administrator extends Member { public function login() { $this->loggedIn = true; echo "Log entry: $this->username logged in
"; } } // создаем нового пользователя и логиним его $member = new Member(); $member->username = "Fred"; $member->login(); $member->logout(); // создаем нового администратора и логиним его $admin = new Administrator(); $admin->username = "Mary"; $admin->login(); // отобразит "Log entry: Mary logged in" $admin->logout();

Как видите, мы перегрузили метод login() класса Administrator, чтобы он отображал сообщения, как в файлах - логах.

Затем мы создали обычного пользователя (Fred) и администратора (Mary). При вызове метода login() от Фреда вызывается метод Member::login(). А когда мы вызываем метод от администратора Mary, вызовется метод Administrator::login(), так как PHP видит, что мы перегрузили этот метод для данного класса. На странице отобразится строка "Log entry: Mary logged in".

С другой стороны, мы не перегрузили метод logout() в дочернем классе, поэтому Member:logout() вызывается и для админов и для обычных пользователей.

Вызов метода супер-класса из дочернего класса

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

В примере, приведенном в прошлом параграфе, мы перегрузили метод login(). Но мы продублировали часть метода Member::login() в Administrator::login():

Class Administrator extends Member { public function login() { $this->loggedIn = true; echo "Log entry: $this->username logged in
"; } }

Вместо того, чтобы дублировать код, лучше вызвать метод Member::login() из Administrator::login().

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

Parent::myMethod();

Давайте теперь перепишем метод login() в дочернем классе так, чтобы из него вызывался тот же метод из класса-родителя, а затем добавим в него что-то новое:

Class Administrator extends Member { public function login() { parent::login(); echo "Log entry: $this->username logged in
"; } }

Это не только сокращает код, но также обеспечивает легкость его будущих корректировок. Если вам позже понадобится изменить способ, по которому логинится любой пользователь, вам потребуется подкорректировать метод login() только в классе Member, и в классе Administrator будет вызываться уже измененный метод.

Предотвращение наследования с помощью методов и классов final

В большинстве случаев, давать возможность классам расширять свою функциональность посредством наследования - хорошая практика. Это ведь часть силы ООП.

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

Чтобы запретить дочерним классам перегружать методы супер-класса, добавьте перед его описанием ключевое слово final. Например, вы можете запретить перегрузку метода login() класса Member по причинам усиления безопасности:

Class Member { public $username = ""; private $loggedIn = false; public final function login() { $this->loggedIn = true; } public function logout() { $this->loggedIn = false; } public function isLoggedIn() { return $this->loggedIn; } }

Если кто-то попытается наследовать класс и перегрузить данный метод:

Class NaughtyMember extends Member { public function login() { $this->loggedIn = true; // сделать что-то плохое } }

… PHP выведет сообщение об ошибке:

Fatal error: Cannot override final method Member::login()

Вы можете также запретить наследование от всего класса:

Final class Member { // от этого класса нельзя вообще наследовать }

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

Fatal error: Class NaughtyMember may not inherit from final class (Member)

На заметку: несмотря на то что это больше касается Java, нежели PHP, приводит преимущества использования методов и классов final.

Работа с абстрактными классами

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

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

На заметку: как только вы создали хотя бы один абстрактный метод в классе, вы должны объявить этот класс как абстрактный.

Когда от абстрактного класса наследуется обычный класс, он должен реализовать все абстрактные методы класса-родителя. В противном случае, PHP сгенерирует ошибку. Так, абстрактный класс создает “правила поведения” для своих дочерних классов.

На заметку: вы можете добавлять в абстрактный класс и не абстрактные методы. Они будут обыкновенным образом наследоваться дочерними классами.

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

Abstract class Person { private $firstName = ""; private $lastName = ""; public function setName($firstName, $lastName) { $this->firstName = $firstName; $this->lastName = $lastName; } public function getName() { return "$this->firstName $this->lastName"; } abstract public function showWelcomeMessage(); }

Как видите, мы создали абстрактный класс, добавив в его описание ключевое слово abstract. В этом классе есть несколько свойств, общих для всех людей, - $frstName и $lastName - а также методы для инициализации и чтения значений этих полей.

В классе также есть абстрактный метод showWelcomeMessage(). Этот метод выводит приветствие, когда пользователь входит на сайт. Опять же, мы добавляем ключевое слово abstract в описание данного метода, чтобы сделать его абстрактным. Так как он абстрактный, в нем нет ни строчки кода, это просто его объявление. Тем не менее, любой дочерний класс обязан добавить и описать метод showWelcomeMessage().

Теперь давайте создадим пару классов от абстрактного класса Person:

  1. класс Member для участников форума;
  2. класс Shopper для покупателей онлайн-магазина.

Class Member extends Person { public function showWelcomeMessage() { echo "Hi " . $this->getName() . ", welcome to the forums!
"; } public function newTopic($subject) { echo "Creating new topic: $subject
"; } } class Shopper extends Person { public function showWelcomeMessage() { echo "Hi " . $this->getName() . ", welcome to our online store!
"; } public function addToCart($item) { echo "Adding $item to cart
"; } }

Как видите, каждый из них описывает метод showWelcomeMessage() из абстрактного супер-класса. Они имплементированы по-разному: в классе Member отображается сообщение "welcome to the forums", а в классе Shopper - "welcome to our online store", но это нормально. Главное то, что они оба описали данный метод.

Если бы один из них, например, Shopper, не описал метод, PHP выдал бы ошибку:

Наряду с имплементацией абстрактного метода, в каждом классе есть свои обычные методы. В Member есть метод newTopic() для создания новой темы в форуме, а в Shopper - метод addToCart() для добавления товаров в корзину.

Теперь мы можем создавать участников форума и покупателей на нашем сайте. Мы можем вызывать методы newTopic() и addToCart() от этих объектов, а также getName() и setName(), так как они наследуются от супер-класса Person.

Более того, зная, что классы Member и Shopper наследуются от Person, мы можем спокойно вызывать метод showWelcomeMessage() для обоих классов, так как он точно реализован и в том и в другом. Мы в этом уверены, так как знаем, что он был объявлен как абстрактный метод в классе Person.

Вот пример:

$aMember = new Member(); $aMember->setName("John", "Smith"); $aMember->showWelcomeMessage(); $aMember->newTopic("Teddy bears are great"); $aShopper = new Shopper(); $aShopper->setName("Mary", "Jones"); $aShopper->showWelcomeMessage(); $aShopper->addToCart("Ornate Table Lamp");

На странице отобразится:

Hi John Smith, welcome to the forums! Creating new topic: Teddy bears are great Hi Mary Jones, welcome to our online store! Adding Ornate Table Lamp to cart

Создание и использование интерфейсов

Интерфейсы во многом схожи с абстрактными классами. Интерфейс - это шаблон, который задает поведение одного или более классов.

Вот основные отличия между интерфейсами и абстрактными классами:

  1. Ни один метод не может быть описан в интерфейсе. Они все абстрактны. В абстрактном классе могут быть и не абстрактные методы.
  2. Интерфейс не может содержать полей - только методы.
  3. Класс имплементирует интерфейс, и класс наследует или расширяет другой класс.
  4. Класс может имплементировать несколько интерфейсов одновременно. Этот же класс может наследовать другой класс. Но у дочернего класса может быть только один супер-класс (абстрактный или нет).

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

Interface MyInterface { public function aMethod(); public function anotherMethod(); }

Чтобы создать класс, который имплементирует тот или иной интерфейс, напишите так:

Class MyClass implements MyInterface { public function aMethod() { // (имплементация метода) } public function anotherMethod() { // (имплементация метода) } }

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

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

Тем не менее, давайте предположим, что нам нужно будет доставать их и записывать в базу данных как объекты класса Member, так и объекты Topic. Для этого мы создадим интерфейс Persistable, в котором будут методы для сохранения объектов в БД и извлечения их оттуда:

Interface Persistable { public function save(); public function load(); public function delete(); }

Теперь давайте создадим класс Member и имплементируем для него интерфейс Persistable. Это значит, что в интерфейсе должны быть методы save(), load() и delete():

Class Member implements Persistable { private $username; private $location; private $homepage; public function __construct($username, $location, $homepage) { $this->username = $username; $this->location = $location; $this->homepage = $homepage; } public function getUsername() { return $this->username; } public function getLocation() { return $this->location; } public function getHomepage() { return $this->homepage; } public function save() { echo "Saving member to database
"; } public function load() { echo "Loading member from database
"; } public function delete () { echo "Deleting member from database
"; } }

Наш класс Topic также будет имплементировать данный интерфейс, поэтому в нем тоже должны быть эти три метода:

Class Topic implements Persistable { private $subject; private $author; private $createdTime; public function __construct($subject, $author) { $this->subject = $subject; $this->author = $author; $this->createdTime = time(); } public function showHeader() { $createdTimeString = date("l jS M Y h:i:s A", $this->createdTime); $authorName = $this->author->getUsername(); echo "$this->subject (created on $createdTimeString by $authorName)
"; } public function save() { echo "Saving topic to database
"; } public function load() { echo "Loading topic from database
"; } public function delete () { echo "Deleting topic from database
"; } }

На заметку: так как у нас форум - воображаемый, вместо взаимодействия с базой данных в методах save(), load() и delete() просто выводятся сообщения.

Теперь можем создать объекты классов Member и Topic, а затем вызвать их методы getUsername() и showHeader(). Более того, зная, что эти классы имплементируют интерфейс Persistable, мы можем вызывать такие методы, как save(), load() или delete():

$aMember = new Member("fred", "Chicago", "http://example.com/"); echo $aMember->getUsername() . " lives in " . $aMember->getLocation() ."
"; $aMember->save(); $aTopic = new Topic("Teddy Bears are Great", $aMember); $aTopic->showHeader(); $aTopic->save();

На странице отобразится:

Fred lives in Chicago Saving member to database Teddy Bears are Great (created on Wednesday 25th May 2011 02:19:14 AM by fred) Saving topic to database

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

Class MyClass implements anInterface, anotherInterface { ... }

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

Заключение

В этом уроке вы ознакомились с одним из самых мощных свойств ООП - наследованием. Вы узнали:

  1. Как работает наследование, и как его использовать для расширения классов;
  2. Как создавать дочерние классы в PHP;
  3. Почему вам может понадобиться перегружать методы в дочерних классах;
  4. Как получить доступ к методам супер-класса;
  5. Всё о методах и классах final, и почему полезно их использовать;
  6. Концепцию абстрактных классов для создания шаблонов дочерних классов;
  7. Как использовать интерфейсы, чтобы задать общую функциональность несвзянным между собой классам.

Если вы прошли все уроки из данной серии, то вы уже сможете писать сложные приложения на ООП. Поздравляю!

В следующем и последнем уроке я покажу вам супер-полезные ООП свойства, которые есть в PHP.

А пока удачного кодирования!

Фреймворк Bootstrap: быстрая адаптивная вёрстка

Пошаговый видеокурс по основам адаптивной верстки в фреймворке Bootstrap.

Научитесь верстать просто, быстро и качественно, используя мощный и практичный инструмент.

Верстайте на заказ и получайте деньги.

Бесплатный курс "Сайт на WordPress"

Хотите освоить CMS WordPress?

Получите уроки по дизайну и верстке сайта на WordPress.

Научитесь работать с темами и нарезать макет.

Бесплатный видеокурс по рисованию дизайна сайта, его верстке и установке на CMS WordPress!

*Наведите курсор мыши для приостановки прокрутки.

Назад Вперед

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

Наследование - это механизм, посредством которого один или несколько классов можно получить из некоторого базового класса.

Класс, который получается в результате наследования от другого, называется его подклассом. Эту связь обычно описывают с помощью терминов "родительский" и "дочерний".

Дочерний класс происходит от родительского и наследует его характеристики. Эти характеристики состоят из свойств и методов.

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

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


Проблема наследования

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

$product1 = new ShopProduct("Собачье сердце", "Михаил", "Булгаков", 5.99); $product2 = new ShopProduct("Пропавший без вести", "Группа", "ДДТ", 10.99); print "Автор: ".$product1->getProducer()."\n"; print "Исполнитель: ".$product2->getProducer()."\n";

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

Лень - это отличительная стратегия проектирования, поэтому на данном этапе вам не следует переживать по поводу использования класса ShopProduct для более чем одного типа товара.

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

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

Как расширить наш пример, чтобы учесть эти изменения? На ум сразу приходят два варианта. Во-первых, можно поместить все данные в класс ShopProduct . Во-вторых, можно разбить ShopProduct на два отдельных класса.

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

Class ShopProduct { public $numPages; public $playLength; public $title; public $producerMainName; public $producerFirstName; public $price; function __construct($title, $firstName, $mainName, $price, $numPages=0, $playLength=0) { $this->title = $title; $this->producerFirstName = $firstName; $this->producerMainName = $mainName; $this->price = $price; $this->numPages = $numPages; $this->playLength = $playLength; } function getNumberOfPages() { return $this->numPages; } function getPlayLength() { return $this->playLength; } function getProducer() { return "{$this->producerFirstName}". " {$this->producerMainName}"; } }

Чтобы продемонстрировать большой объем выполняемой работы по кодированию, в данном примере были использованы методы доступа к свойствам $numPages и $playLength .

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

Таким образом, для альбомов будут сохраняться информация и функциональные возможности класса, относящиеся к книгам (общее количество страниц), а для книг - данные о времени звучания альбомов.

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

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

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

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

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

Function getSummaryLine() { $base = "{$this->title} ({$this->producerMainName}, "; $base .= "{$this->producerFirstName})"; if ($this->type == "book") { $base .= ": страниц - {$this->numPages}"; } else if ($this->type == "cd") { $base .= ": время звучания - {$this->playLength}"; } return $base; }

Как видите, чтобы правильно установить значение свойства $type , нам нужно будет в конструкторе еще проверить значение аргумента $numPages . И снова класс ShopProduct стал более сложным, чем нужно.

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

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

Class CdProduct { public $playLength; public $title; public $producerMainName; public $producerFirstName; public $price; function __construct($title, $firstName, $mainName, $price, $playLength) { $this->title = $title; $this->producerFirstName = $firstName; $this->producerMainName = $mainName; $this->price = $price; $this->playLength = $playLength; } function getPlayLength() { return $this->playLength; } function getSummaryLine() { $base = "$this->title ($this->producerMainName, "; $base .= "$this->producerFirstName)"; $base .= ": время звучания - $this->playLength"; return $base; } function getProducer() { return "{$this->producerFirstName}". " {$this->producerMainName}"; } } class BookProduct { public $numPages; public $title; public $producerMainName; public $producerFirstName; public $price; function __construct($title, $firstName, $mainName, $price, $numPages) { $this->title = $title; $this->producerFirstName = $firstName; $this->producerMainName = $mainName; $this->price = $price; $this->numPages = $numPages; } function getNumberOfPages() { return $this->numPages; } function getSummaryLine() { $base = "$this->title ($this->producerMainName, "; $base .= "$this->producerFirstName)"; $base .= ": страниц - $this->numPages"; return $base; } function getProducer() { return "{$this->producerFirstName}". " {$this->producerMainName}"; } }

Мы постарались справиться с этой сложностью, хотя пришлось кое-чем пожертвовать.

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

А жертва состояла в дублировании. Методы getProducer() абсолютно одинаковы для каждого класса. Каждый конструктор одинаково устанавливает ряд идентичных свойств. Это еще один признак дурного тона, и вы должны научиться избавляться от подобного.

Если нужно, чтообы метооды getProducer() работали одинаково для каждого класса, то любые изменения, внесенные в одну реализацию, должны быть внесены и в другую. Но так мы довольно скоро нарушим синхронизацию классов.

Даже если мы уверены, что можем поддерживать дублирование, на этом наши проблемы не закончатся. Ведь у нас теперь есть два типа, а не один.

Помните класс ShopProductWriter ? Его метод write() предназначен для работы с одним типом: ShopProduct . Как выйти из сложившейся ситуации, чтобы все работало как раньше?

Мы можем удалить уточнение типа класса из объявления метода, но тогда мы должны надеяться, что методу write() будет передан объект правильного типа.

Мы можем добавить в тело метода собственный код для проверки типа.

Class ShopProductWriter { public function write($shopProduct) { if (! ($shopProduct instanceof CdProduct) && ! ($shopProduct instanceof BookProduct)) { die("Передан неверный тип данных"); } $str = "{$shopProduct->title}: " . $shopProduct->getProducer() . " ({$shopProduct->price})\n"; print $str; } }

Обратите внимание на оператор instanseof , использованный в этом примере. Вместо него подставляется значение true (истина), если объект в операнде слева относится к типу, представляемому операндом справа.

И снова мы были вынуждены добавить новый уровень сложности. Нам нужно не только проверять аргумент $shopProduct на соответствие двум типам в методе write() , но и надеяться, что в каждом типе будут поддерживаться те же поля и методы, что и в другом.

Согласитесь, иметь только один тип было бы гораздо лучше, потому что мы могли бы использовать уточнение типа класса для аргумента метода write() , и мы были бы уверены, что класс ShopProduct поддерживал определенный интерфейс.

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

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

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

P.S. Вы явно хотите разобраться с PHP и ООП) Обратите внимание на премиум-уроки по различным аспектам сайтостроения, включая программирование на PHP, а также на бесплатный курс по созданию своей CMS-системы на PHP с нуля с использованием ООП:

Понравился материал и хотите отблагодарить?
Просто поделитесь с друзьями и коллегами!


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

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

extends

Рассмотрим основные принципы наследования классов:

  • С помощью наследования мы можем создавать классы, которые будут содержать в себе все свойства и методы родительского класса, т.е. дочерние классы наследуют public и protected свойства и методы родительского класса.
  • Дочерний класс является расширением родительского класса, так как кроме унаследованных свойств и методов он может содержать собственные.
  • Наследование устанавливается использованием слова extends при создании класса.
  • Если у наследника нет своего конструктора (о нем писал ранее), то автоматически запустится конструктор родителя. Если в дочернем классе есть конструктор, то родительский конструктор не будет запускаться.
  • В дочерних классах могут переопределяться свойства и методы суперкласса. Определяя подкласс, мы гарантируем, что его экземпляр определяется характеристиками сначала дочернего, а затем родительского класса.

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

class Ploshad {
protected $width;
protected $height;
public function __construct($x, $y) {
$this->width = $x;
$this->height = $y; }
public function Square() { return $this->width * $this->height; }
}

class Obyem extends Ploshad {
protected $glybina;
public function Obyemss($g) {
$this->glybina = $g;
return $this->Square()* $this->glybina;
}
}

За счет записи class Obyem extends Ploshad мы объявили, что класс Obyem наследует свойства и методы класса Ploshad и может ими управлять. Внутри Obyem мы прописали новой свойство $glybina и метод public function Obyemss($g) . Давайте теперь рассчитаем объем и площадь:

$obj = new Obyem(2,12); //создаем объект от наследника
echo "Площадь: ".$obj->Square(); //старая функция родителяя
echo "Объем: ".$obj->Obyemss(2); //новая функция наследника

Как видно из комментариев $obj теперь может использовать как старые методы класса Ploshad, так и новые дочернего класса Obyem.

При рассчете площади интерпретатор PHP ищет конструктор в Obyem классе, если его не находи, то запускает конструктор в Ploshad. Он переопределяет значения переданных свойств(Obyem(2,12)). Затем запускается метод Square() и использует эти данные.

В случае с объемом, также запускается конструктор в Ploshad, а затем метод Obyemss($g), в которой передается высота. Внутри метода используем рассчет площади умноженный на высоту и получаем результат.

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

Интерпретатор PHP всегда сначала смотрит на дочерний класс:

  1. Есть ли конструктор. Если есть, то игнорирует конструктор родителя
  2. Есть ли свойства и методы. Если в дочернем и родительском они совпадают, то используются дочерние. Таким образом, можно полностью переписать функционал методов родительского класса.

extends parent::

parent:: - используется если в дочернем классе нужно изменить функционал родительского метода

Пример:
class PloshadProcent extends Ploshad {
function Square() {
$str = parent::Square();
$str = $str*0.1;
return $str;
}
}

Теперь при вызове, мы получим 10% от нашей площади:
$obj2 = new PloshadProcent(2,12);
echo "Площадь 10%: ".$obj2->Square(); //выдаст 2.4

Мы переопредилили метод суперкласса Square() внутри PloshadProcent

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

Пусть есть класс book и в нем конструктор принимает всего 2 параметра: $title, $price.

class book {
public $title;
public $price;
function __construct($title, $price) {
$this->title = $title;
$this->price = $price;
}
}

Вдруг нам понадобилось вывести новый параметр $pages. Создадим экземпляр класса new_book и сделаем в нем свой конструктор, в котором уже будут нужные данные: $title, $price, $pages

Когда в дочернем классе определяется свой конструктор, PHP не вызывает конструктор родительского класса автоматически. Это необходимо сделать вручную в конструкторе подкласса.

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

class new_book extends book {
public $pages;
function __construct($title, $price, $pages) {
parent::__construct($title, $price); // вызываем метод-конструктор родительского класса
$this->pages = $pages; // инициализируем свойство определенное в подклассе
}
}
$obj = new new_book("азбука", 35, 500);
echo "Книга: $obj->title | Цена: $obj->price | Страниц: $obj->pages";

Таким образом, мы можем расширять функционал суперклассов, не изменяя их самих. В следующем уроке вы узнаете



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: