Статьи

Исповедь новообращенного разработчика PHP: злоупотребление животными

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

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

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

Ruby предоставляет ту же структуру наследования расширений, но также позволяет вам использовать различные не столь жесткие способы изменения классов — как мы видели в прошлом со структурой mixin. Но Ruby идет еще дальше и позволяет переопределять классы. Если вы сделаете это в PHP, вы получите довольно строгое изложение.

<?PHP class foo { public function hello_world() { echo "Hi"; } } #somewhere else in my codebase class foo { public function hello_world() { echo "Hello world!"; } } 

Для начала я создал класс с именем foo, с методом hello_world . Но затем в другом месте моего кода я хочу изменить свой класс foo и его метод hello_world для вывода немного другого сообщения. К сожалению, приведенный выше код выдаст PHP Fatal error: Cannot redeclare class foo . Не хорошо. Чтобы получить такое поведение, нам нужно было бы расширить наш класс и, тем самым, внести намного большую сложность.

 <?PHP class bar extends foo { public function hello_world() { echo "Hello world!"; } } 

Вуаля, у нас теперь есть класс с именем bar, который имеет правильный метод hello_world . Это идеально, да? Нет, это не так.

Проблема этого стиля кодирования заключается в том, что для достижения желаемого результата нам нужно было создать совершенно новый класс для использования. Если у нас уже есть кодовая база, использующая класс foo, мы без проблем работаем над поиском и заменой всей кодовой базы. Хотя это может быть тривиально для небольшой кодовой базы, это может превратиться в упражнение для большой кодовой базы. Давайте посмотрим на общий сценарий с ситуацией запроса / ответа — игнорируем тот факт, что этот код вызывает функции, которые явно не существуют и не будут работать, если они это сделали!

 <?PHP class API_Request { public function get($resource) { $content = magically_get_resource_content($resource); $status_code = magically_get_resource_status_code($resource); return $this-&gt;_build_response($content, $status_code); } protected function _build_response($content, $code) { return new API_Response($content, $code); } } class API_Response { protected $_response; protected $_status_code; public function __construct($content, $status_code) { $this-&gt;_response = json_decode($content); $this-&gt;_status_code = $status_code; } public function is_successful() { return $this-&gt;_status_code == 200; } } 

Здесь мы можем вызвать get для API_Request со строкой ресурса URL и вернуть экземпляр API_Response с помощью метода is_successful . Этот метод сообщит нам, был ли код состояния запроса http 200 или нет. Теперь предположим, что мы хотели изменить функциональность класса API_Response чтобы он соответствовал определениям кода состояния HTTP / 1.1, и скажите, что любой код состояния в 200-х годах является успешным. Чтобы сделать это, нам нужно либо отредактировать базовый класс API_Response , либо расширить его, а также расширить класс API_Request для вызова нового расширенного класса API_Response . Все это чертовски много работы для такого простого изменения, особенно когда есть возможность обновления кода, который использует эти классы в вашем проекте.

Здесь я собираюсь сделать это, заменив API_Response на API_Better_Response и API_Request на API_Better_Request .

 <?PHP class API_Better_Response { public function is_successful() { return $this-&gt;_status_code &gt;= 200 &amp;&amp; $this-&gt;_status_code &lt;= 299; } } class API_Better_Request { protected function _build_response($content, $code) { return new API_Better_Response($content, $code); } } 

Теперь мы переписали метод _build_response для правильного возврата класса API_Better_Response , но нам также пришлось бы пройти через код нашего приложения и найти все экземпляры API_Request и заменить его API_Better_Request .

Рубин на помощь: Обезьянья ямочка

Ruby имеет возможность открывать класс где угодно, а также переопределять или добавлять методы. Это чрезвычайно простой процесс — вы просто объявляете класс снова. Давайте посмотрим на эквивалентные классы Request и Response в Ruby — снова игнорируя магический загадочный код, который не существует.

 module API class Request def get(resource) content = magically_get_resource_content(resource) status_code = magically_get_resource_status_code(resource) API::Response.new(content, status_code) end end class Response def initialize(content, status_code) @response, @status_code = ActiveSupport::JSON.decode(content), status_code end def is_successful @status_code == 200 end end end 

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

 module API class Response def is_successful (200...300) === @status_code end end end 

Теперь is_successful вернет true, если будет возвращен любой код состояния 2xx http. Мы спрашиваем, равен ли диапазон 200-300 (не включая 300) свободному коду статуса запроса. Обратите внимание, что мы не затрагивали ни один из других методов, и они все еще будут в их первоначальном виде — мы просто is_successful метод is_successful .

Этот тип динамического изменения класса часто называют Monkey Patching или, согласно Википедии, Duck Punching. Очевидно, наименование имеет вид [животное] [глагол причастия прошлого]. Вы можете прочитать о происхождении названия в Википедии .

Итак, мы видели, что Ruby допускает простое и гибкое динамическое изменение классов во время выполнения, что чрезвычайно полезно. Вы также можете сделать несколько патчей обезьяны, но это повлияет только на вызовы кода после того, как он будет исправлен.

Я должен отметить, что Monkey Patching часто является быстрым решением, которое может создать головную боль для будущих разработчиков (или для вас самих, если ваша память похожа на мою) по нескольким причинам:

  • Классовая логика, которая, как ожидается, будет в исходном файле, больше не там, где она должна быть — и ее трудно отследить.
  • Код, который вы исправляете, может больше не работать, если базовая библиотека или код обновлены и методы, которые вы исправляете, удалены или их поведение изменилось.

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

Используете ли вы патч обезьяны в своем коде? Избежать этого любой ценой? Не стесняйтесь комментировать ниже с вашими причинами.