№63 Абстрактные методы и абстрактные классы. Интерфейсы. Использование интерфейсов и абстрактных классов


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

В одних языках создавать экземпляры абстрактных классов запрещено, в других это допускается (например Delphi), но обращение к абстрактному методу объекта этого класса в процессе выполнения программы приведёт к ошибке. Во многих языках допустимо объявить любой класс абстрактным, даже если в нём нет абстрактных методов (например, Java), именно для запрещения создания экземпляров. Абстрактный класс можно рассматривать в качестве интерфейса к семейству классов, порождённому им, но, в отличие от классического интерфейса, абстрактный класс может иметь определённые методы, а также свойства.

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


Абстрактный методПаскаль), чисто виртуальный методC++), отложенный метод — в объектно-ориентированном программировании, метод класса, реализация для которого отсутствует. Класс, содержащий абстрактные методы, также принято называть абстрактным (там же и пример). Абстрактные методы зачастую путают с виртуальными. Абстрактный метод может являться виртуальным, но это вовсе не обязательно.

Назначение абстрактных методов[1]:


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

Абстрактный класс создается с помощью добавления ключевого слова 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->answerarray_shift$answers ) ); 
    print 
"Запрос:   {$q->prompt}\n"
    print 
"Ответ: {$q->response}\n"
    print 
"Счет:    {$q->score}\n\n"


// Вывод: 
// Запрос:   Сколько типов данных в 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 anInterfaceanotherInterface 
        
//... 

Вот дополненная версия класса 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 
addXMLableXMLable $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 методы, необходимые для обхода коллекции.

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



Hosted by uCoz