Устранение дублирование кода, не единственная задача наследования классов. Иногда оно применяется для изменения существующего поведения базового класса.

Тег <select>, в DOM представлен классом HTMLSelectElement. У него есть дополнительные методы, которые нужны для работы со списком элементов. Один из таких методов: item(index). С его помощью можно извлекать конкретный вариант из списка.

<form>
  <select name="variants">
    <option>Opt 1</option>
    <option>Opt 2</option>
    <option>Opt 3</option>
  </select>
</form>

<?php
// Гипотетический код, который возвращает форму выше в виде элемента HTMLSelectElement.
$element = $document->querySelector('select');

$element->item(0); // HTMLOptionElement(textContent="Opt 1")
$element->item(1); // HTMLOptionElement(textContent="Opt 2")

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

<?php

// свойство length описывает число option элементов внутри select
$element->item($element->length - 1);

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

<?php
// Последний элемент
$element.item(-1); // HTMLOptionElement(textContent="Opt 3")
// Трети с конца
$element.item(-3); // HTMLOptionElement(textContent="Opt 1")

Как это сделать? Наследование дает возможность переопределять методы суперклассов. Посмотрите на пример:

<?php

class HTMLCustomSelectElement extends HTMLSelectElement
{
    public function item($possibleIndex)
    {
        $realIndex = $possibleIndex > 0 ? $possibleIndex : $this->length + $possibleIndex; 
        // parent указыавет на родительский класс
        return parent::item($realIndex);
    }
}

Выше создан подкласс HTMLCustomSelectElement, который переопределяет метод item($index). Переопределение означает, что в подклассе создается метод с тем же именем, что и в родительском классе. Наш новый метод выполняет дополнительную работу по вычислению индекса, но ему все еще нужен исходный метод item($index), для выборки нужного элемента. Для этого применяется специальный синтаксис, который указывает явно что нужно взять метод из родительского класса: parent::item($readIndex).

Почему понадобился специальный синтаксис? Представьте что вместо него там был бы такой код:

<?php

public function item($possibleIndex)
{
    $this->item($possibleIndex);
}

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

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

<?php

$select = new HTMLCustomSelectElement();

// Этот вызов всегда относится к методу item переопределенному внутри HTMLCustomSelectElement
// Вызвать напрямую item из HTMLSelectElement невозможно
$select->item(3);

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

Переопределение не ограничивается одним уровнем наследования. Любой переопределенный метод можно снова переопределить в наследниках текущего класса. Главное соблюдать два условия:

  • У обоих методов должны совпадать имена
  • Они должны иметь одинаковое количество аргументов

Использование наследников

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

Например при работе с DOM, объекты этих классов иногда порождаются самим программистом, а иногда системой. Например:

<?php

// Создаем сами
$element1 = new HTMLSelectElement();

// Где-то внутри создается объект HTMLSelectElement
$element2 = $document->querySelector('select');

Можно ли подменить класс в примере с querySelector? Зависит от реализации библиотеки по работе с DOM. В тех библиотеках что мне известны, это сделать невозможно. Это значит что единственный выход использовать класс, конвертировать вернувшийся объект в объект нужного нам класса. Стоит ли оно того? Почти наверняка нет.

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


Дополнительные материалы

  1. Принцип подстановки Лисков

Для продолжения нужно перейти в курс и вступить в него.