Контроль типа — это важно

Перевод статьи Wojciech Sznapka «Type Hinting is important»

Один из моих любимых вопросов на собеседованиях по PHP звучит так: «Что такое контроль типа (Type Hinting) и почему он важен?» Если дать определение в одном предложении, то контроль типа — это способ описания типа для параметров в сигнатуре функции, и это — sine qua non для достижения полиморфизма. Поскольку PHP имеет динамическую типизацию, то вообще говоря параметрам не обязательно иметь какой-то явный тип. Также, в данной статье под типами я имею ввиду сложные типы данных (класс, абстрактный класс, интерфейс, массив, замыкание), а не примитивные (скалярные) типы, такие, как integer или double.

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

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

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

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

Выше были представлены три варианта для использования в контроле типа (интерфейс, базовый класс и конкретный класс). Есть ещё одна очень важная вещь, о которой стоит помнить. Несмотря на то, что PHP позволяет вам вызывать методы с другими типами, нежели описаны в определении метода, вы никогда не должны так делать! Это может привести к странным ошибкам и противоречит Принципу подстановки Лисков (LSP, Liskov Substitution Principle). Проще говоря, если вы указываете какой-то тип в сигнатуре метода, то метод в своей реализации должен полагаться только на этот тип, а не на его наследников (даже если мы знаем о их существовании), чтобы вы в любой момент могли заменить в вызывающем коде объект указанного типа любым объектом его подклассов без изменения кода самого метода.

Посмотрим на возможное нарушение Принципа подстановки Лисков:

<?php
// ..
class UserRepository extends \Doctrine\ORM\EntityRepository
{
    public function findActiveUsers()
    {
        // .. тут делаем какой-нибудь запрос для получения результата
        return $activeUserCollection;
    }
}
// ..
public function notifyActiveUsers(EntityRepository $repo)
{
    if ($repo instanceof UserRepository) {
        $usersCollection = $repo->findActiveUsers();
    } elseif ($repo instanceof ManagersRepository) {
        // .. делаем что-нибудь ещё
    }
    // .. делаем что-нибудь с $usersCollection
}

Как мы можем видеть, типизированный метод notifyActiveUsers имеет внутреннюю зависимость от специфического расширения EntityRepository. Это ломает LSP и ведёт к нечитаемой модели. Хуже того, ситуация может быть следующей:

<?php
// ..
public function notifyActiveUsers(EntityRepository $repo)
{
    $usersCollection = $repo->findActiveUsers();
    // .. делаем что-нибудь с $usersCollection
}

На этапе написания этого кода мы знаем, что только одна конкретная реализация EntityRepository передавалась в этот метод. Однако, в какой-нибудь случайный момент времени кто-нибудь другой (или вы сами) может передать другую реализацию EntityRepository — и это вызовет проблемы. В Java компилятор не разрешит вам скомпилировать такой код, но в PHP это разрешено на этапе интерпретации, однако, такой код упадёт во время выполнения.

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

comments powered by Disqus
Система Orphus