Абстрактные классы и интерфейсы - Web - Shelek
1. Предполагаемая аудитория

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

2. Введение

Большинство PHP-программистов знают, что поддержка объектно-ориентированного программирования в Zend Engine II значительно улучшена. Три новые возможности - приведение типов, абстрактные классы и интерфейсы определяют возможности установки и проверки типов классов. В этой статье мы увидим, как эти возможности помогают писать более качественный код и предотвращать появление ошибок.

3. Типы классов

В PHP существует 12 предопределенных типов данных, включая простые типы, например, целые и с плавающей точкой, комплексные типы (массивы и объекты) и специальные типы (ресурсы и NULL). Поскольку PHP поддерживает классы и объекты, у вас также есть возможность определять собственные типы. На самом деле, каждый раз при создании класса вы определяете новый тип данных.

Вот простой класс:

<?php

class Question {

function answer( $response_s ) {
// ...
}

}
?>

Создав класс Question, вы также автоматически определили и тип данных Question. Объект класса Question принадлежит типу данных Question, так же, как и объекты классов, пронаследованных от класса Question.

Вот класс, наследующий Question:

<?php

class ResourceQuestion extends Question {
// ...

function addResourceURL( $url_s ) {
// ...
}

}

$question = new ResourceQuestion();
?>

У объекта $question комплексная принадлежность. Он принадлежит к типу ResourceQuestion, но в то же время он принадлежит и к типу Question, так как класс ResourceQuestion пронаследован от класса Question.



4. Почему важны типы классов?

Если вы знаете тип объекта, вы знаете его характеристики и возможности. Если вы скажете мне, что объект $question принадлежит типу Question, я сразу пойму, что у него есть метод answer(), который я могу вызвать. А если вы мне скажете, что $question также является объектом класса ResourceQuestion, вы также дадите мне понять, что у объекта есть и метод answer(), и метод setResourceURL(). Вооружившись этой информацией об объекте $question, я смогу нормально работать с ним.

5. Проверяем и задаем типы

В PHP 5 появился новый оператор instanceof, при помощи которого можно проверить тип объекта. Оператор instanceof принимает два параметра. Первый - это тестируемый объект, второй - тип, на принадлежность к которому проверяется объект. Соответственно, для того, чтобы проверить, что объект $question принадлежит типу Question, используется следующий синтаксис:

if ( $question instanceof Question ) {
print "$question is type Questionn";
}

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

Давайте расширим класс Question. Это часть сильно упрощенной библиотеки "Викторина". Каждому объекту Question необходим объект Marker, отвечающий за проверку ответа пользователя на вопрос. В этом примере класс Question принимает объект Marker как аргумент конструктора.

<?php

class Question {
private $marker;
public $prompt = "";

function __construct( $prompt_s, $marker ) {
$this->prompt = $prompt_s;
$this->marker = $marker;
}

}

?>

За код, вызывающий __construct(), может отвечать другой программист, поэтому представим, что произойдет если в конструктор будет передан неправильный тип объекта? Правильный ответ - "ничего", и в этом-то и заключается проблема. Мы просто сохраним объект Marker в атрибут, готовый к дальнейшему использованию в методе answer(). Еси в конструктор был передан объект не того типа, мы не узнаем этого до тех пор, пока не вызовем метод answer():

<?

class Question {
private $marker;
public $prompt = "";

function __construct( $prompt_s, $marker ) {
$this->prompt = $prompt_s;
$this->marker = $marker; // здесь произойдет ошибка
}

function answer( $response_s ) {
$this->marker->mark( $response_s ); // последствия возникшей ошибки мы увидим здесь
}
}

?>

Это именно та ситуация, в которой проверка типов может предотвратить возникновение проблем в дальнейшем. Если мы не хотим, чтобы в наш класс пробрались данные неправильного типа, и затаились, ожидая подходящей возможности доставить нам побольше неприятностей, мы должны вручную проверять тип аргумента $marker:

function __construct( $prompt_s, $marker ) {
if ( ! ( $marker instanceof Marker ) ) {
die( "Ожидался объект типа Marker" );
}

$this->prompt = $prompt_s;
$this->marker = $marker; // правильный тип гарантирован
}

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

В PHP 5 эта проблема решена за счёт строгой типизации классов. Просто написав перед аргументом название типа, вы укажете, что PHP должен следить, чтобы переданные данные были указанного типа:

<?
function __construct( $prompt_s, Marker $marker ) {
$this->prompt = $prompt_s;
$this->marker = $marker;
}

Написав 'Marker' перед переменной $marker, мы жестко задали ее тип. Это значит, что передача в объект любого другого типа, кроме типа Marker вызовет следующую ошибку:

Fatal error: Argument 2 must be an object of class Marker in...

Теперь, когда вызывают наш метод answer(), мы можем быть уверены, что атрибут Question::$marker содержит объект типа Marker (тем не менее, мы все равно должны проверять, что его значение не NULL.)

6. Работаем с типами классов: абстрактные классы

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

Вот реализации классов Question и Marker:

<?php

class Question {
private $marker;
public $prompt = "";
public $score = 0;
public $response = "";

function __construct( $prompt_s, Marker $marker ) {
$this->prompt = $prompt_s;
$this->marker = $marker;
}

function answer( $response_s ) {
$this->score = 0;
$this->response = $response_s;
if ( $this->marker->mark( $response_s ) ) {
$this->score = 1;
}
}
}

class Marker {
protected $condition;
function __construct( $condition_s ) {
$this->condition = $condition_s;
}

function mark( $response_s ) {
return ( $this->condition == $response_s );
}
}
?>

В этом примере мы расширили класс Question, и теперь он поддерживает счетчик, равный единице или нулю. Класс Marker хранит правильный ответ. Когда он вызывается в классе Question, он попросту сравнивает строку из $condition с переданным методу mark() ответом пользователя. Если строки совпадают, метод возвращает true. Вот небольшой пример, проверяющий работу этих классов:

$q = new Question( "сколько нужно бобов, чтобы из было 5",
new Marker( 'пять' ) );
$responses = array( "пять", "шесть" );

foreach ( $responses as $response ) {
$q->answer( $response );
print "результат: за ответ $response оценка {$q->score}n";
}

// вывод:
// результат: за ответ пять оценка 1
// результат: за ответ шесть оценка 0

Для отражения взаимоотношений между классами можно использовать диаграммы. Чаще всего для этого используют Unified Modeling Language (UML). На этой диаграмме показаны взаимоотношения между классами Question и Marker.



Пока что логика оценок ответов в классе Marker сыровата. Что произойдет, если требования станут более сложными? Администратор викторин вероятнее всего потребует большей гибкости в создании критериев оценки. Возможно, ей понадобится создавать вопросы типа: "Назовите ключевые слова контроля доступа", при этом пользователь должен указать их все через запятую.

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

class Marker {
protected $condition;
protected $condWords;

const MATCH = 1;
const CLIST = 2;
private $type = MATCH;

function __construct( $condition_s, $type=1 ) {
$this->type = $type;
if ( $type == self::CLIST ) {
$this->condWords =
preg_split( "/s*,s*/", $condition_s );
} else {
$this->condition = $condition_s;
}
}

function mark( $response_s ) {
if ( $this->type == self::CLIST ) {
// реализуем совпадение с пунктом списка
return true;
} else {
return ( $this->condition == $response_s );
}
}
}

В классе Marker используются две константы: Marker::MATCH и Marker::CLIST, чтобы отслеживать способ оценки ответов, для которой вызван класс. Если свойство Marker::$type содержит значение, определенное в Marker::MATCH, то метод Marker::mark() сравнивает строки. Если Marker::$type содержит значение, определенное в Marker::CLIST, то объект разбивает строку ответа на части и сравнивает каждую из них.

Мы устанавливаем значение для флага Marker::$type в конструкторе. Аргумент $type в определении метода конструктора задает значение по умолчанию, которое может быть изменено пользователем.

В этом примере мы пока обошли стороной логику оценки, чтобы не тратить место и сфокусироваться на дизайне кода как таковом. Вы заметили похожие участки в конструкторе и методе mark()? Вы можете работать с таким кодом до тех пор, пока клиент не захочет поддержки регулярных выражений и арифметического подсчета правильных ответов. К этому моменту наш класс Marker станет непомерно раздутым. Причём, сложность будет не только в размере - ведь придется поддерживать два шага в обработке ответов. Первый в конструкторе, при необходимости приводящий ответ на вопрос к нужному виду, а второй в методе mark(), производящий обработку ответа пользователя. Необходимость их поддерживать может сделать поддержку кода крайне трудоемким процессом.

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

Абстрактный класс создается с помощью добавления ключевого слова abstract к обычному объявлению класса.

<?php
abstract class MyClass {
// ...
}
?>

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

abstract class MyClass {
abstract function aMethod( $an_arg );
}

Этот код изменяет класс Marker, делая его абстрактным:

<?php
abstract class Marker {
protected $condition;
function __construct( $condition_s ) {
$this->condition = $condition_s;
$this->handleCondition( $condition_s );
}

/* делаем возможность получить protected-атрибут condition */
public function getCondition() {
return $this->condition;
}

abstract protected function handleCondition( $condition_s );
abstract function mark( $response_s );
}

?>

В этом коде используется ключевое слово abstract для модификации как класса Marker, так и его методов handleCondition() и mark(). Конструктор сохраняет необработанную строку с условием (переданную через аргумент $condition_s) в атрибут, а потом вызывает абстрактный метод handleCondition(), оставляя детали дальнейшей ее обработки дочерним классам.

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

class WrongMarker extends Marker {
}

//Fatal error: Cannot instantiate abstract class WrongMarker in..

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

Таким образом, абстрактный класс гарантирует реализацию для всех своих методов. Вот два дочерних класса Marker.

<?php
class MatchMarker extends Marker {
protected function handleCondition( $condition_s ) {
// реализация не требуется
}

function mark( $response_s ) {
return ( $this->condition == $response_s );
}
}

class ListMarker extends Marker {
protected $condWords = array();

protected function handleCondition( $condition_s ) {
$this->condWords = preg_split( "/s*,s*/", $condition_s );
}

function mark( $response_s ) {
$respWords = preg_split( "/s*,s*/", $response_s );
$commonTerms = array_intersect( $this->condWords, $respWords );
if ( count( $commonTerms ) == count( $this->condWords ) ) {
return true;
}
return false;
}
}
?>

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

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

Вот код для проверки наших классов:

$questions[] = new Question("Сколько типов данных в PHP?", new MatchMarker("12") );
$questions[] = new Question("Назовите ключевые слова, определяющие доступ в классах.", new ListMarker("private, public, protected") );
$answers = array( "12", "private, public, protected" );

foreach( $questions as $q ) {
$q->answer( array_shift( $answers ) );
print "Запрос: {$q->prompt}n";
print "Ответ: {$q->response}n";
print "Счет: {$q->score}nn";
}

// Вывод:
// Запрос: Сколько типов данных в PHP?
// Ответ: 12
// Счет: 1
//
// Запрос: Назовите ключевые слова, определяющие доступ в классах.
// Ответ: private, public, protected
// Счет: 1

Разделив реализации оценки, мы перестали принимать решение, какой вариант обработки использовать для данной строки с условиями. В данном случае этот выбор делается в коде примера. Задача выбора одной из реализаций абстрактного суперкласса часто встречается в ООП. Обычно этот выбор реализуется с использованием шаблона factory.

Структура наших классов теперь выглядит следующим образом:



Несмотря на все эти изменения, произошедшие с классом Marker, мы до сих пор не трогали класс Question. Класс Question работает и с объектом типа Marker, причем тип этого объекта жестко прописан в определении конструктора. Даже несмотря на то, что мы создали два новых класса, то, что они пронаследованы от класса Marker, сделало все эти изменения прозрачными для класса Question.

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

Типы классов и абстрактные классы в PHP 5 позволяют работать с объектами гораздо более уверенно. Абстрактные классы можно эмулировать и в PHP 4. Можно использовать разные схемы именования или определять абстрактные методы, которые содержат в себе операторы die(). Например:

<?php

class Marker {
// ...

function handleCondition( $condition_s ) {
die( "handleCondition - абстрактный метод" );
}

function mark( $response_s ) {
die( "handleCondition - абстрактный метод" );
}
}

?>

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

7. Работаем с типами классов: интерфейсы

Как мы уже видели, объект может принадлежать более чем к одному типу. Объект, принадлежащий какому-то классу, в то же время принадлежит и ко всем родительским классам в иерархии наследования.

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

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

Вот интерфейс для вывода XML-данных.

<?php
interface XMLable {
public abstract function toXML( );
}
?>

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

class MyClass extends ParentClass implements anInterface {
//...
}

Любой класс, реализующий интерфейс должен предоставлять реализацию для всех методов, определенных в интерфейсе, или сам должен быть объявлен как абстрактный. Класс может реализовывать множество интерфейсов, при этом имена реализуемых интерфейсов просто добавляются к строке implements:

class MyClass implements anInterface, anotherInterface {
//...
}

Вот дополненная версия класса Question, реализующая интерфейс XMLable:

class Question implements XMLable {
// пропустим методы и аттрибуты

function toXML( ) {
return
<<<XMLFRAGMENT
<question>
<prompt>{$this->prompt}</prompt>
<response>{$this->response}</response>
<condition>{$this->marker->getCondition()}</condition>
</question>

XMLFRAGMENT;
}
}

Объект Question теперь принадлежит и к типу Question, и к типу XMLable. В реализации метода toXML() мы создаем XML-строку, содержащую данные объекта Question. В реальности имело бы смысл использовать расширение DOM или SimpleXML, но в этом примере ясность для нас важнее гибкости.

Теперь представьте себе класс, не имеющий отношения к классу Question. К примеру, класс User, который содержит в себе имя пользователя и его e-mail. Давайте реализуем пример, в котором класс User (пронаследованный от некоего класса Individual) также будет реализовывать интерфейс XMLable.

<?php
class Individual {
public $name = "";
public $email = 0;

function __construct( $name_s, $email_s ) {
$this->name = $name_s;
$this->email = $email_s;
}
}

class User extends Individual implements XMLable {

function getLastLogin() {
// ...
}

function toXML() {
return
<<<XMLFRAGMENT
<userdata>
<name>{$this->name}</name>
<email>{$this->email}</email>
</userdata>

XMLFRAGMENT;
}
}
?>

Класс User не имеет ничего общего с классом Question за исключением того, что они оба реализуют интерфейс XMLable.



Это значит, что некий сторонний класс может работать с объектами классов Question и User как с объектами типа XMLable, не обращая внимания на их основной тип. Вот класс, который так и поступает:

class ObjectDump {
private $xmlStr;
private $objects = array();

function addXMLable( XMLable $object ) {
$this->objects[] = $object;
}

function output() {
$xmlStr = "<odump>n";
foreach($this->objects as $output) {
$xmlStr .= $output->toXML();
}
$xmlStr .= "</odump>n";
return $xmlStr;
}

function __toString() {
return $this->output();
}
}

Класс ObjectDump принимает объекты типа XMLable, используя указание принимаемого типа при объявлении метода addXMLable(). Независимо от того, насколько разные передаваемые ему объекты, ObjectDump знает, что все они содержат реализацию метода toXML(), и это все, что нужно ему для нормальной работы. Объект ObjectDump использует это знание в методе output(), который проходит по всем переданным ему объектам XMLable и объединяет вывод их методов toXML() в одну XML-строку. Само собой, все наши знания об этих объектах сводятся к тому, что у каждого из них есть интересующий нас метод. Типизация может применяться к аргументам объекта, но это не накладывает ограничений на тип возвращаемых значений. Необходимо хорошо документировать интерфейсы, чтобы на их основе можно было создавать четко работающие классы.

Как и абстрактные классы, интерфейсы описывают характеристики, на которые может опираться клиент при работе с объектом. Сам PHP работает с интерфейсами, определяя встроенные интерфейсы Iterator и IteratorAggregate. Реализуйте их, и вам потребуется реализовать набор методов требуемых интерфейсом iterator с одной стороны и классом-коллекцией с другой.

Когда PHP встречает объект типа IteratorAggregate внутри оператора foreach, он использует реализованные в нем и в создаваемом им объекте Iterator методы, необходимые для обхода коллекции.

Эти интерфейсы удобно использовать по трем причинам:

* Предопределенная функциональность. PHP знает, что он может вызывать методы valid() и current() (помимо прочих) в любом классе, реализующем интерфейс Iterator.
* Интерфейсы не мешают нормальному наследованию. Ваш класс может наследовать другой класс и в то же время реализовывать интерфейс Iterator.
* Вы сами определяете в своей реализации, каким образом обрабатывать коллекцию. Захотели реализовать некий фильтр? Вперед, имеете полное право.

8. Резюме

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

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

Приведение типов дополняют абстрактные классы и интерфейсы. Абстрактные методы с типизованными аргументами определяют интерфейс, на который можно положиться. С другой стороны, типизация недоступна ни для простых типов, ни для значений, возвращаемых методами. Поэтому их нужно контролировать вручную. Обзор web камеры Logitech для бизнес видеосвязи
Information
  • Posted on 27.04.2013 17:25
  • Просмотры: 2188