Практическое наследование

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

Поставленная задача
Как-то раз мне потребовалась реализация механизма аннотирования классов и их членов как в Java. Если кто не знает, это возможность присвоения дополнительных данных классам, их свойствам, методам, аргументам методов и т.д. К примеру, если нам требуется записать объект в базу данных, то необходимо связать свойства объекта с полями таблицы, а так же указать, в какую таблицу необходимо записать данный класс. В моем случае это должно быть реализовано следующим образом:
<?php
/*
TableName=PeopleTable
*/
class People{
/*
FieldName=firstNameField
*/
private $firstName;

/*
FieldName=nameField
*/
private $name;

/*
FieldName=midleNameField
*/
private $midleName;

/*
FieldName=dateBirthField
*/
private $dateBirth;
...

publid getFirstName(){
return $this->firstName;
}
...

}
Класс, аннотированный данным образом, использует таблицу PeopleTable для записи своих объектов, а его свойства записываются в поля firstNameField, nameField, midleNameField, dateBirthField. Другими словами, аннотация позволяет добавлять метаданные (данные описывающие данные) в код.
Данная задача была поставлена мной одному из моих программистов и дана неделя для разработки, реализации и тестирования.

Неудачное решение
Решение, предложенное моим программистом, меня несколько удивило. Оно выглядело следующим образом: был разработан интерфейс Described который позволял добавлять метаданные к любому, реализующему его классу
<?php
interface Described{
  // Метод возвращает все метаданные данного элемента.
  public function getAllMetadata();

  // Метод возвращает значение конкретных метаданных элемента.
  public function getMetadata($metadataName);

  // Метод устанавливает значение метаданных.
  public function setMetadata($metadataName, $metadataValue);

  // Метод проверяет, существуют ли заданные метаданные в вызываемом представлении.
  public function isMetadataExists($metadataName);
}
Так же были реализованы представления всех описываемых метаданными элементов класса:
- Type - представление класса;
- Property - представление свойства;
- Method - представление метода.
Все эти классы являлись подклассами класса ClassMember который реализовывал интерфейс Described, следовательно, все эти классы могли быть описаны с помощью метаданных.
Мой программист так же создал интерфейс Reflect, реализация которого позволяла получить соответствующие отображения:
<?php
interface Reflect{
  // Метод возвращает представление свойства класса.
  static public function &getReflectionProperty($propertyName);

  // Метод возвращает представление метода класса.
  static public function &getReflectionMethod($methodName);

  // Метод возвращает представление класса.
  static public function &getReflectionClass();

  // Метод возвращает отображения всех свойств класса.
  static public function getAllReflectionProperties();

  // Метод возвращает отображения всех методов класса.
  static public function getAllReflectionMethods();
}
Для ясности приведу пример кода, использующий аннотирование с решением, предложенным моим программистом:
<?php
// Анотируемый класс
class TestMetadata implement Reflect{
use TReflect;

private $var1;
private $var2;

public function method(){}
}

// Получаем представление переменной. Представление хранит только информацию о переменной, но не ее значение, потому инкапсуляция не нарушается.
$var1Reflect = TestMetadata::getReflectionProperty('var1');
// Добавляем аннотацию к свойству
$var1Reflect->setMetadata('FieldName', 'var1Field');
/* Теперь свойство var1 класса TestMetadata имеет аннотацию FieldName со значением var1Field, это позволит нам в будущем записать значение данного свойства в требуемое поле таблицы. Если изменится имя поля таблицы, достаточно будет изменить аннотацию */

Разбор полетов
На первый взгляд может показаться, что код вполне приличен, в нем есть и ООП, и инкапсуляция, и полиморфизм и даже наследование, но на деле это не так. В действительности представления, написанные моим программистом: Type, Method, Property - уже существуют в стандартной библиотеке PHP и называются ReflectionClass, ReflectionMethod и ReflectionProperty. Существуют и дополнительные представления, реализованные в стандартной библиотеке и отсутствующие в данной реализации, а так же эти стандартные решения имеют дополнительные методы, которые придется реализовать нам самостоятельно. Пропадает повторное использование кода!
Если вы хорошо знакомы с ОО моделью в PHP, то знаете сколько полезных классов она включает, потому попробуем использовать существующее решение и решить задачу с помощью наследования.

Более хорошее решение
Мы не отказались от некоторых решений, предложенных моим программистом, в частности мы оставили интерфейс Described, его Trait, а так же Reflect и его Trait, так как аналога в стандартных решениях у нас нет. Мы выбросили классы Type, Method, Property и воспользовались следующими подклассами:
<?php
class ReflectionClass extends \ReflectionClass implements \PPHP\patterns\metadata\Described{
  use \PPHP\patterns\metadata\TDescribed;
}

class ReflectionMethod extends \ReflectionMethod implements \PPHP\patterns\metadata\Described{
  use \PPHP\patterns\metadata\TDescribed;
}

class ReflectionProperty extends \ReflectionProperty implements \PPHP\patterns\metadata\Described{
  use \PPHP\patterns\metadata\TDescribed;
}
Эти три класса расширяют стандартные классы PHP для работы с отображениями и дополняются новым функционалом - возможностью аннотирования.
Посмотрите, как сейчас реализован механизм получения отображений:
<?php

trait TReflect{
  // Отражение класса.  
  static private $reflectionClass;

  // Множество отражений свойств класса.
  static private $reflectionProperties;

  // Множество отражений методов класса.
  static private $reflectionMethods;

  // Метод возвращает представление свойства класса.
  static public function &getReflectionProperty($propertyName){
    if(!is_string($propertyName) || empty($propertyName) || !property_exists(get_class(), $propertyName)){
      throw new \InvalidArgumentException();
    }
    if(empty(self::$reflectionProperties)){
      self::$reflectionProperties = [];
    }
    if(!array_key_exists($propertyName, self::$reflectionProperties)){
      self::$reflectionProperties[$propertyName] = new ReflectionProperty(get_class(), $propertyName);
    }
    return self::$reflectionProperties[$propertyName];
  }

  // Метод возвращает представление метода класса.
  static public function &getReflectionMethod($methodName){
    if(!is_string($methodName) || empty($methodName) || !method_exists(get_class(), $methodName)){
      throw new \InvalidArgumentException();
    }
    if(empty(self::$reflectionMethods)){
      self::$reflectionMethods = [];
    }
    if(!array_key_exists($methodName, self::$reflectionMethods)){
      self::$reflectionMethods[$methodName] = new ReflectionMethod(get_class(), $methodName);
    }
    return self::$reflectionMethods[$methodName];
  }

  // Метод возвращает представление класса.
  static public function &getReflectionClass(){
    if(empty(self::$reflectionClass)){
      self::$reflectionClass = new ReflectionClass(get_class());
    }
    return self::$reflectionClass;
  }

  // Метод возвращает отображения всех свойств класса.
  static public function getAllReflectionProperties(){
    $reflectionProperties = new \SplObjectStorage();
    $namesAllProperties = get_class_vars(get_class());
    foreach($namesAllProperties as $k => $v){
      $reflectionProperties->attach(self::getReflectionProperty($k));
    }
    return $reflectionProperties;
  }

  // Метод возвращает отображения всех методов класса.
  static public function getAllReflectionMethods(){
    $reflectionMethods = new \SplObjectStorage();
    $namesAllMethods = get_class_methods(get_class());
    foreach($namesAllMethods as $v){
      $reflectionMethods->attach(self::getReflectionMethod($v));
    }
    return $reflectionMethods;
  }
}
Как можно заметить, класс будет возвращать одно и то же отображения для одного свойства, а не несколько. Это позволяет сохранить аннотации.
Посмотрите, как изменится код:
<?php
// Аннотируемый класс
class TestMetadata implement Reflect{
use TReflect;

private $var1;
private $var2;

public function method(){}
}

// Получаем представление переменной. Представление хранит только информацию о переменной, но не ее значение, потому инкапсуляция не нарушается.
$var1Reflect = TestMetadata::getReflectionProperty('var1');
// Добавляем аннотацию к свойству
$var1Reflect->setMetadata('FieldName', 'var1Field');
/* Теперь свойство var1 класса TestMetadata имеет анотацию FieldName со значением var1Field, это позволит нам в будущем записать значение данного свойства в требуемое поле таблицы. Если изменится имя поля таблицы, достаточно будет изменить аннотацию */
Как можно заметить, код не изменился вообще. Дело в том, что правильная архитектура пакета позволила нам инкапсулировать изменения в классах, потому код, использующий данные классы не претерпел изменений, но зато у нас отпала необходимость реализовывать три дополнительных класса и один абстрактный класс. Все благодаря наследованию!

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

URL: https://visavi.net/articles/430