Иногда функциональность библиотеки или набора классов, с которыми вы работаете, на 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->_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->_response = json_decode($content); $this->_status_code = $status_code; } public function is_successful() { return $this->_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->_status_code >= 200 && $this->_status_code <= 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 часто является быстрым решением, которое может создать головную боль для будущих разработчиков (или для вас самих, если ваша память похожа на мою) по нескольким причинам:
- Классовая логика, которая, как ожидается, будет в исходном файле, больше не там, где она должна быть — и ее трудно отследить.
- Код, который вы исправляете, может больше не работать, если базовая библиотека или код обновлены и методы, которые вы исправляете, удалены или их поведение изменилось.
Я думаю, что я пытаюсь сказать здесь: использовать на свой страх и риск! Вы можете обнаружить, что, немного подумав и проведя рефакторинг, вы сможете достичь тех же результатов более чистым способом и избавить будущих вас или будущих других разработчиков от нескольких головных болей на этом пути.
Используете ли вы патч обезьяны в своем коде? Избежать этого любой ценой? Не стесняйтесь комментировать ниже с вашими причинами.