Переопределение предка (dirty hack)

в 11:31, , рубрики: dirty hack, hook, php, ооп, метки: , , ,

Иногда очень хочется переопределить поведение класса родителя, не меняя его код.
К примеру поменять место хранения шаблонов из файлов в базу… или добавить кэширование…
или заменить в ORM удаление записей на пометку их как удаленные…
Да мало ли что мы можем пожелать изменить…
Если каждый программист будет лезть в ядро фреймворка или просто в чужой код, то это будет каша.
У этой задачи есть множество решений. Я хочу предложить то, которое мне нравится больше всего.
Решение основано на __autoload() а точнее на spl_autoload_register.

Большинство реализаций этой задачи подразумевают значительное количество специального кода, который присутствует в наших классах «на всякий случай» (как это делается к примеру в расширении с помощью hook. При этом часто бывает, что разработчик предусмотрел возможности переопределения везде где только можно… кроме того места которое нам нужно.
Другие решения требуют существенной переработки логики работы системы, что часто затрудняет понимание, и повышает порог вхождения. (к примеру различные вариации на событийную модель).

Хочется чтобы все было просто, и при этом максимально гибко.
И такое решение нашлось:

Вообще идея простая как тапки:

Если мы не можем переопределять поведение уже объявленных классов, то мы можем управлять процессом объявления этих самых классов. За загрузку классов у нас отвечает функция __autoload().
Таким образом если класс поведение которого мы хотим изменить вызывается через __autoload(), то изменив поведение __autoload() мы можем загрузить нужный класс из другого файла.
Собственно в одном проекте мною был реализован механизм переопределения поведения __autoload() по принципу hook:
прикладной модуль мог объявить свою функцию загруprb классов и определенным образом зарегистрировать ее на выполнение как ДО основной функции так и после…
Но мы не будем останавливаться на этой реализации потому что…

Это всё уже в прошлом!

Наконец настали те времена, когда php 5.3 стал доступен уже на большинстве хостингов, и отпала необходимость обеспечивать совместимость с более старыми ветками.
А ведь в 5.3 модуль SPL является частью ядра, и доступен по умолчанию.
Оказывается, что разработчики уже реализовали эту идею в виде функции spl_autoload_register.
Функция spl_autoload_register просто регистрирует нашу функцию автозагрузки в стек подобных функций.
Т.о. если мы пропишем функцию которая загружает измененный класс ДО загрузки основной функции, то будет загружен наш код а не стандартный. С другой стороны если мы хотим добавить какие-то варианты автогенерации кода, то мы можем добавить их ПОСЛЕ основного кода.

Хватит теории....

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

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

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

Пример

файл common.php содержащий весь общий код:

function start_module() {
// здесь идет код вызова наших модулей.... расположен в common.php чтобы не захламлять index.php
}
function mainAutoload($class) {
    // основной код автозагрузки
    $filename = './class/'.$class.'.php';
    if(file_exists()) require_once($filename);
}

По умолчанию файл index.php содержит:

require_once 'common.php';
//
// Зарегистрируем наш основной автозагрузчик
spl_autoload_register('mainAutoload');
//
// Вызовем нашу функцию вызова модулей
start_module();

Допустим мы решили, что в нашем проекте будет много желающих переопределить класс soul
Тогда мы создаем класс soul.php:

class soul extends soul_basic {
// здесь ничего нет.
}

И соответственно soul_basic.php

class soul_basic {
    public function good() {
         // здесь у нас реализация кода добра
    }
    public function evil() {
         // реализация кода зла
    }
     public function saintliness() {
          // реализация кода святости
     } 
     public function business() {
          // реализация кода бизнеса
     }
}

Ну и уже конкретные объекты которым будет нужна «душа» могут реализовывать или наследовать от soul не от soul_basic, и иметь функционал реализованный в soul_basic.

Теперь представим, что мы хотим изменить поведение всех «душ».
Очевидно, что переопределяя свойства «души» программиста или директора или любого другого класса который наследует от soul нам в этом не поможет. Поэтому мы создаем свой класс soul. Для этого мы изменяем файл index.php, например так:

require_once 'common.php';
//
// Объявим наш измененный загрузчик.
function century21Autoload($class) {
    if($class = 'soul') require_once('soul21.php');
}
//
// Зарегистрируем ИЗМЕНЕННЫЙ автозагрузчик
spl_autoload_register('century21Autoload');
// Зарегистрируем наш основной автозагрузчик
spl_autoload_register('mainAutoload');
//
// Вызовем нашу функцию вызова модулей
start_module();

Ну и соответственно soul21.php:

class soul extends soul_basic {
    public function good() {
         if(rand(0,100)>=80) parent::good();
         else parent::evil();
    }
     public function saintliness() {
         if($this->unit_name == 'Gundyaev') parent::bussiness();
         else parent::saintliness();
     } 
}

PS: Я человек верующий и над церковью не смеюсь. Просто я отделяю церковь как веру и церковь как бизнес. А с бизнеса смеяться не считаю грехом. Хотя мои друзья и приятели священники из разных конфессий со мной не согласны…

Автор: Mendel

Поделиться

* - обязательные к заполнению поля