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

    UPD: Лучше конечно такого избегать. Все это страшно, ужасно, и воняет. Но воняет чуть меньше чем VQMOD, и если уж приходится патчить «живой» и обновляемый, но жуткий легаси, то такой подход имеет право на существование. Но НИКОГДА не делайте так в проектах которые вы только начинаете или можете изменить архитектуру на более расширяемую. Статью оставляю как есть. «На память».




    Иногда очень хочется переопределить поведение класса родителя, не меняя его код.
    К примеру поменять место хранения шаблонов из файлов в базу… или добавить кэширование.
    или заменить в 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: Я человек верующий и над церковью не смеюсь. Просто я отделяю церковь как веру и церковь как бизнес. А с бизнеса смеяться не считаю грехом. Хотя мои друзья и приятели священники из разных конфессий со мной не согласны…

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 60

      +1
      А почему бы не унаследовать класс, от того, поведение которого мы хотим изменить и использовать уже дочерний?
        +1
        Ну если хочется изменить поведение тридцати классов которые наследуют от одного класса, то ваше решение не выглядит тривиальным :) Особенно когда это разбросанно по десяткам модулей. особенно когда ты в принципе не знаешь где оно используется. (К примеру ты пишешь не модуль для своего конкретного проекта, а универсальный модуль который будет добавляться в любой проект).
          0
          Вообще конечно такие ситуации в принципе печаль. Но так да, костыли и хаки.
            0
            Ну почему костыль? Вот в данный момент я пишу фреймворк, который в первую очередь будет заточен под электронный документооборот. В тестовом приложении у меня будет около двух десятков видов документов, все они будут наследовать от класса document.
            Допустим у нас будет стоять задача вести учет версий документов с сохранением автора версии.
            По большому счету такая задача решается довольно тривиальным изменением в CRUD модели.
            Можно добавить еще дополнительный view, но можно уже и без этого…

            Или на этапе эксплуатации системы вдруг оказывается, что значительная нагрузка на базу приходится на запросы типа «какие пары кладовщик/продавец работающие в один день создают максимальное количество документов относительно количества проданных товаров» (вполне реальная эвристика для поиска некоторых афер со скидками и т.п.). И мы хотим для ускорения таких отчетов добавить к методу создания документа (ЛЮБОГО) чтобы он изменял некоторые предварительно рассчитанные коэффициенты. Это просто решается — мы просто добавляем нужный код в метод create нашего универсального класса document.

            Если вы предложите более красивую архитектуру которая позволит решать такие задачи, то я с удовольствием выслушаю вашу идею. Но сколько я не искал — всё скатывается к банальным hook или событийной модели с перехватами событий…
              0
              А как у вас создается новый документ?
              $doc = new Document();
              $doc->create();
              

              Так?
                0
                Ну примерно. Document вообще-то абстрактный. И с ORM чуть иначе работаем, но не суть, пусть будет:
                class Prikaz extends Document {
                // допольнительная реализация
                }
                $doc = new Prikaz();
                $doc->create();
                
                  +2
                  Да, понятно что сам Document не используется на прямую. Я просто о сути. Так вот, я думаю что поскольку объект описывает документ, то имеет смысл логику создания документа завернуть в конструктор. То есть после
                  $doc = new Prikaz();
                  

                  Мы уже получаем документ и работаем с этим объектм, без вызова дополнительного метода. А поскольку у нас предполагается возможность вносить какие-либо модификации и/или предварительные расчеты в процессе его создания, то можно передвать в конструктор объект-мутатор (так оно кажется назвается =) ), который будет уметь работать с определенным типом документа(-ов).
                  Только нужно место, где будет происходить определение того какие мутаторы использовать. Таким образом просто меняем объеткы в зависимости от ситуации и документы нужным нам образом модифицируются или еще какието действия производятся в процессе создания.
                    0
                    мутаторы вместо классов?
                    т.е. для каждого вида документа делать мутаторы а работать чисто с документами?
                    Ну так там и работа с базой, и структура и куча вьювов и куча бизнеслогики… и опять таки это частный случай. Я должен специально усложнить структуру ветки классов «Документы» для того чтобы потом их можно было бы переопределять… но не всегда заранее можно предсказать что другой человек на другой стороне планеты спустя пять лет после выхода проекта захочет переопределить.

                    Если же использовать мутаторы для наших изменений, то опять таки это не сильно далеко уйдет от первоначального варианта — переопределять потомков. Все тридцать. В чужих модулях. Которых я в глаза не видел. ага.
                      0
                      То есть проблема в том, что уже существует большая система и затраты на такой рефакторинг слишком велики?
                        0
                        Ну так кстати говоря 30 класс это вобщем-то не много и современные IDE вполне безболезненно справились бы с изменением сигнатуры конструкторов.
                          0
                          А какого хрена какая-то сволочь будет править конструкторы в моих классах? Он что охренел чтоль? А потом ко мне еще за поддержкой обращаться будет. Ага.
                          Ну да фиг с ним. Если у него руки растут из того места и он осилил ваши трехэтажные решения то думаю он особенно ничего не испортит. Но вот кого я не пущу в свой код (ладно свой, но еще дюжины независимых разработчиков разных модулей) так это системного администратора клиента которому пришлют вместо изящного модуля и инструкции прописать две строки в одном файле — инструкцию о том как делать рефакторинг в IDE. Увольте…
                            0
                            Я это к тому, что затраты не такие и большие на внедрение данного решения… ладно.
                              0
                              Каждого конкретного решения? Не кажется ли вам что именно это и является костылями — разрастание кода, усложнение процесса установки и настройки ради «правильности» кода?
                              У меня на этапе проектирования фреймворка уже есть пару дюжен идей что и как стоило бы расширить. А сколько еще таких вещей придумают другие?
                                0
                                Да в чем усложнение то? Просто отдельные объекты со своим поведением, котроые ни от кого не зависят и от них тоже никто не зависит. Передали в конструктор — хорошо, используем. Не передали — ну и ладно, просто создаем документ.

                                Да и вообще любая модификация архитектуры требует определенных рефакторингов, это часть процесса разработки, которая как раз и позволяет не прибегать к костылям, мусору и копипасте.
                                  0
                                  Если это ОДИН проект под контролем одного ПМ (или группы ПМ) то я с этим согласен.
                                  Но когда речь идет об универсальном решении, которое будет использоваться и модифицироваться толпами неконтролируемых кодеров — тут уж простите хочется избежать рефакторинга любой ценой.
                                    0
                                    Понятненько…
                              0
                              При необходимости внесения изменений в логику создания документа(ов) нужно будет только добавить новый класс-мутатор в систему и всего делов.
                                0
                                Куда добавить?
                                Как его передать?
                                В каком месте?

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

                                Молодцы блин! Документация выросла в два раза, код в полтора, кодеры нихрена не понимают что это и зачем… Все ждут очередного стопятисотого релиза в котором наконец появится возможность мутировать класс разбора ссылок, чтобы модуль мультиязычности не требовал правки кода ядра…
                                Все равно десяток мест не предусмотрели где подготовить под мутации, и их хакают по черному. Но зато у нас "правильный код". И при этом мы можем хвастать что мол у нас «нет проблем с архитектурой».

                                Мне вас не понять — я месяц ходил локти кусал прежде чем решился таки на плейсхолдеры в конструкторе запросов. Банальные плейсхолдеры. А тут хуки, обозреватели, мутации… и без проблем.
                                  0
                                  Даже боюсь спрашивать что плохого в плейсхолдерах))
                                  Но причем тут ОРМ, вьюхи и прочее описаное. Речь ведь всего лишь о документах и некотрой динамической логике во время их создания…
                                    0
                                    Нет, если вы внимательно читали заметку, то в самом начале речь шла как раз об универсальном решении поскольку заранее совершенно не понятно что именно захочется изменять.
                                    Вот есть у меня класс dummy_view который выполняется если вдруг не было найдено никаких вьювов для класса и он не может быть обработан даже универсальным вьювом для итераторов. Он выводит информацию очень коряво, поскольку строго говоря это нештатная ситуация и такой вывод нужен скорее для отладки.
                                    Но кто-то вдруг решил изменить его поведение. Ну пусть даже в лог при этом что-то писать… откуда я знаю что он там себе захотел? Его тоже мутировать?))))

                                    В плейсхолдерах плохо то, что это форма записи которая не интуитивнопонятна. Мне пофиг в какой форме писать, мне и обратная польская запись вполне родная, благо МК-52 в детстве был… Но другим не понятна, и я с болью в сердце пошел на это усложнение синтаксиса в пользу безопасности и надежности.
                                      0
                                      Эм… это же просто подстановки. Да и PDO уже не первый день существует, если конечно речь о нем.
                            0
                            Проблема в том, что я сейчас в 2012 не могу (да и не хочу) предсказывать ВСЕ возможные рефакторинги которые могут понадобится в 2032.
                        0
                        > Prikaz

                        Ну я прошу… ну не надо.
                          0
                          В реальном примере вообще Nakaz :)
                          Но я решил не травмировать психику…
                        0
                        Если упрощённо:

                        $doc = new Document();
                        $doc->onCreate(array($docCache, 'recalculateSmth'), array($doc));
                        $doc->create();
                        


                        А вообще, если хотите подменить какой-то класс — используйте DI. Если фреймворк вам этого не позволяет — меняйте фреймворк или сеняйте архитектуру своего приложения так, чтобы он соответствовал возможностям фреймворка и не приводил к грязным хакам. Причём не только грязным, но и глобальным для всего приложения.
                        +1
                        Рассматривалась ли возможность использования паттернов Стратегия или Обозреватель?
                          0
                          Нечто подобное Наблюдателю реализовано в 1с в 8.1
                          Недостатки таких решений это сложность понимания и дополнительный код. Который опять таки увеличивает сложность понимания. Поскольку у 1с свой язык и свой движок, то они могут скрыть реализацию таких вещей не запутывая кодера которому не нужен этот функционал. Но это тоже грязновато…

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

                          Минус конечно в том, что этим решением могут пользоваться бездумно и там где без этого можно обойтись. Ну не всех проконтролируешь — предупреждения я пишу… :)

                          ПЫСЫ: Если коротко то использование Наблюдателя, Стратегии и прочих подобных шаблонов ведут к заметному повышению порога вхождения.
                            +1
                            Лучше выше порог вхождения, чем засраная говнокодом система ;)
                              +1
                              Ну это уже холивар.
                              Есть задача — простой движок с низким порогом вхождения и высокой гибкостью.
                              Решение проблемы того, что человек решает задачи не сообразно своей квалификации планируется делать организационно.
                              0
                              Ну, не знаю, не знаю. Стратегию начал использовать ещё до того как узнал что такое стратегия. А к событиям должны были привыкнуть ещё в других языках.

                              И, вообще, почему Вы считаете, что порог вхождения в Ваше ПО ниже при использовании нераспространённых хаков ниже нежели использование распространённых шаблонов?
                                0
                                Потому что «нераспространенные хаки» не нужно будет использовать тем кто будет только входить :)
                                Изменения редки. Очень редки, и писать их (я надеюсь) будут уже опытные разработчики. А вот все эти навороты мешать будут именно тем кто только начинает разбираться.

                                Главный принцип простоты — чем меньше ты знаешь о чем-то пока ты с этим не столкнулся, тем лучше.
                    +2
                    Простите, а не проще ли было использовать Dependency Injection (Например Pimple)?
                      –1
                      Опять таки — куча лишнего кода + более высокий порог вхождения.
                      Да и частный случай тоже туда же.
                        0
                        То бишь по Вашему действительно лучше регистрировать отдельный автозагрузчик для каждого класса, которой Вам захотелось переопределить?

                        Да и о каком высоком пороге вхождения Вы говорите?
                        Куда уж проще то?

                        <?php
                        
                        require_once 'lib/Pimple.php';
                        
                        $neededClass = 'PerfectSoul'; // Имя нужного класса можно разместить в конфиге или еще где
                        
                        $container = new Pimple; // Создали контейнер
                        
                        $container['soul'] = new $neededClass; // Присвоили ячейке контейнера 'soul' объект нужного класса
                        
                        $container['soul']->oldMethod(); // PROFIT
                        
                          –1
                          Ну можно не регистрировать загрузчик а сделать просто прямой вызов. Или сделать один загрузчик на все изменения если у тебя в проекте их много. Не суть. Главное что все они в одном заметном месте.

                          Сложность в том, что все это надо будет делать В КАЖДОМ из уже более чем 70 классов которые имеют место сейчас… а это только начало разработки…
                            0
                            Захардкодить (а именно так называется то, что Вы предлагаете) имена классов в автозагрузчике(ах) ни как не гибкое решение (И понятности для новичков я в таком подходе тоже не наблюдаю, по мне так проще будет в конфиге эти классы задавать, и нагляднее и возможности сломать все к чертям особо нет). И это занятие не проще, чем переделать на DI, на мой взгляд.
                      0
                      Есть предложение к коментаторам обладающим некоторым свободным временем и умением связывать слова в предложения.
                      Здесь упомянуты несколько разных подходов к решению определенного класса задач. Может ли кто-нибудь написать топик, в котором решить одну и ту же задачу различными способами? Идеально из 2х начальных точек: «с нуля» и «из имеющейся системы».
                        0
                        Определённого? Кем? Где?
                          0
                          Своими словами: [локальное] изменение поведения существующего класса, используемого в неконтролируемых модулях, без внесения их [конкретных изменений] непосредственно в код этого класса.
                          Место действия: PHP5.3+ (5.4+).
                          Можете использовать собственную, быть может, более корректную и практичную формулировку.
                            0
                            Мне кажется, что Вы говорите о такой штуке как Mixins, для примера, в Yii это называется Behavior. Или я не так понял?
                              0
                              Почти. Я пытаюсь сказать о проблеме, которую они решают.
                                0
                                Да, я понял. Просто тут все могут разойтись во мнениях, какую именно технику использовать, и нет среди них лучшей, серебряной пули, так сказать :) Где-то проще будет использовать Mixins, где-то лучше подойдет Runkit.
                                  0
                                  Собственно поэтому я и предложил сделать сравнение. Runkit-стремная штука, судя по его багтрекеру.
                            0
                            Лучше расскажите о тех модулях, которые Вы встречали, с которыми вот такие вот проблемы.
                              0
                              Отсутствие вертолетного винта у гоночного болида не является его проблемой.
                              Ограниченность и сложность модернизации структуры большинства фреймворков это лишь особенность.
                              Хватит уже мыслить проблемами.
                              Раздражает эта ограниченная психология — будет проблема, будем решать. Создавать что-то новое, самому создавать ниши а не заполнять чужие — нет, это не для «наших людей».
                                0
                                > Отсутствие вертолетного винта у гоночного болида не является его проблемой.

                                Понятное дело, потому что гоночный болид называется гоночным болидом, а не конструктором или фреймворком. Если хотите делать гоночный болид, то делайте, никто ничего Вам не скажет. Но, если Вы сделав гоночный болид, назовёте его конструктором и предложите в качестве инструментов доработки сварочную горелку, зубило и молоток, сверло и токарный станок, то на Вас будут косо смотреть.

                                А Ваш подход «будет проблема, будем решать» очень распространён. Поставят шах — защитим короля. Стукнут машину — подумаем о страховке. Собьёт пьяный на дороге 7-х — будем думать над законом. Грянет гром — перекрестимся.
                                  0
                                  Ну так я вроде как и примерный список возможных задач накидал, и вроде как против проблем писал а не за ;)
                                    0
                                    Тогда ладно: ) Проблемы оставлять на потом не стоит, но и придумывать не надо.: )
                          0
                          Здесь было мельком затронуто три подхода (помимо моего) — хуки, события и мутации.
                          Умножить на новая/старая система это 6 примеров.
                          Довольно много.
                          Я на эту заметку часа полтора потратил.
                          Имея четкое понимание того о чем пишу.
                          Но было бы неплохо.....)

                          ПЫСЫ: Если вдруг кто-то решится на такой обзор, то просьба от меня сразу указывать степень удовлетовренности таких критериев:
                          1 — степень усложнения кода в чистой системе для ее подготовки к возможности переопределения (для меня важно что мой код чистый остался)
                          2 — универсальность (мне важно, что мое решение позволяет изменять любые классы а не только те которые было запланировано модифицировать заранее).
                          3 — «правильность» кода (для меня не так важно, но как вижу многим это очень важно).
                          +1
                            0
                            Да, но «Это расширение PECL не поставляется вместе с PHP.».
                            А значит пока не станет частью ядра как SPL я его не использую.
                            Я и на SPL перешел только после того как 5.3 стал достаточно распространенным.
                            Раньше эммулировал эту функцию через издевательство над __автолоад
                              0
                              использование VPS или полноценного сервера решает эти архаичные проблемы.
                                0
                                Я в статье не нашёл упоминания о shared хостинге.
                                  0
                                  Может потому что его там не было? :)
                                  Я опубликовал способ решения задачи.
                                  Мало того я даже пометил его как "dirty hack" в названии заметки. Чтоб было меньше вопросов.
                                  А дальше каждый решает сам что ему больше подходит: простое и универсальное «неправильное» решение или сложное решение которое работает не везде и может решить не все задачи, но при этом «правильное».
                                0
                                pecl.php.net/package/runkit
                                См. даты коммитов и список багов.
                              0
                              Посмотрите на фреймворк Kohana, там Вашу проблему решили давно ;-)
                                0
                                Сформулируйте пожалуйста поконкретнее, в чем именно моя проблема и как они ее решили. Если сложно указать к какому из уже обсужденных решений оно относится, то просто ссылку дайте плиз, чтобы не рыться только для того чтобы убедиться что ничего нового там нет…

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

                                ПЫСЫ: Не сочтите за троллинг или самомнение, но уже надоели придуманные проблемы и повторы решений за пределами ТЗ.
                                ПЫПЫСЫ: Я пока вообще другое решение реализовал — расставил приоритеты загрузки модулей из разных папок. Теперь модули сайта стали приоритетнее чем стандартные модули и модули ядра, так что можно просто создать нужные классы там, и ничего настраивать не нужно будет. Не стал уже апать тему, чтобы опять свора «правильных архитекторов» не загрызла, а в качестве примера того как работает spl_autoload_register статья остается полезной. К тому же у этого решения все равно есть некоторые преимущества.

                              Only users with full accounts can post comments. Log in, please.