Reg.ru: домены и хостинг

Крупнейший регистратор и хостинг-провайдер в России.

Более 2 миллионов доменных имен на обслуживании.

Продвижение, почта для домена, решения для бизнеса.

Более 700 тыс. клиентов по всему миру уже сделали свой выбор.

Перейти на сайт->

Бесплатный Курс "Практика HTML5 и CSS3"

Освойте бесплатно пошаговый видеокурс

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

на HTML5 и CSS3 с полного нуля.

Начать->

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

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

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

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

Получить в подарок->

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

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

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

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

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

Получить в подарок->

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


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

Перед изучением данной статьи вы можете прочитать предыдущую статью из этой серии - "Определение типов объектов в объектно-ориентированном программировании на 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, связанные с книгами и музыкальными альбомами, плохо работают вместе, но, похоже, не могут существовать по отдельности.

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

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

Об этом читайте в следующей статье - "Работа с наследованием в PHP".

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

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


Смотрите также:

PHP: Получение информации об объекте или классе, методах, свойствах и наследовании

PHP: Получение информации об объекте или классе, методах, свойствах и наследовании

CodeIgniter: жив или мертв?

CodeIgniter: жив или мертв?

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

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

Применение функции к каждому элементу массива

Применение функции к каждому элементу массива

Слияние массивов. Преобразование массива в строку

Слияние массивов. Преобразование массива в строку

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

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

Эволюция веб-разработчика или Почему фреймворк - это хорошо?

Эволюция веб-разработчика или Почему фреймворк - это хорошо?

Магические методы в PHP или методы-перехватчики (сеттеры, геттеры и др.)

Магические методы в PHP или методы-перехватчики (сеттеры, геттеры и др.)

PHP: Удаление элементов массива

PHP: Удаление элементов массива

Ключевое слово final (завершенные классы и методы в PHP)

Ключевое слово final (завершенные классы и методы в PHP)

50 классных сервисов, программ и сайтов для веб-разработчиков

50 классных сервисов, программ и сайтов для веб-разработчиков

Наверх