Last Updated:

Confessions of a New PHP Developer: Animal Abuse

Sometimes the functionality of the library or set of classes you're working with is 99% perfect for the job, but the last 1% requires changing some of the basic assumptions made in the code. Changing the code can lead to dissatisfaction with the maintenance, and extending the code can be immediately frustrating, but Ruby provides us with a flexible way to modify the code with less damage.

When I switched to Ruby, I realized how strict PHP is in the class hierarchy. This surprised me, as I've always viewed PHP as a very free language, mainly due to its weak typing and lack of formal guidelines for code structure. The latter, in my opinion, is due to the fact that many developers learn the language. I find that most PHP developers start learning by using the built-in PHP as a low entry point into dynamic web languages, and then move on to more complete and more complex applications.

With PHP, your class hierarchy is a linear progression with random implementation from the outside. By this I mean that you'll start with a class, perhaps declared abstract, and continue your journey to the final class with extension by extension, and along the way you can implement some interfaces (this is the implementation from the side bit). It's a very straightforward structure, with no way to dynamically change this process.

Ruby provides the same structure for inheriting extensions, but also allows you to use a variety of not-so-rigid ways to modify classes — as we've seen in the past with the mixin structure. But Ruby goes even further and allows you to override classes. If you do this in PHP, you'll get a pretty rigorous presentation.

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

To get started, I created a class named foo, with the hello_world method. But then elsewhere in my code, I want to change my foo class and its method hello_world to output a slightly different message. Unfortunately, the above code will give PHP Fatal error: Cannot redeclare class foo . Not good. To get this behavior, we would need to expand our class and thereby introduce much more complexity.

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

Voila, we now have a class named bar that has the correct method hello_world. That's perfect, huh? No, it's not.

The problem with this style of coding is that to achieve the desired result, we needed to create a completely new class to use. If we already have a codebase that uses the foo class, we have no problem finding and replacing the entire codebase. While this may be trivial for a small codebase, it can turn into an exercise for a large codebase. Let's look at the general scenario with a request/response situation – ignore the fact that this code calls functions that clearly don't exist and won't work if they do!

<?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; } }

Here, we can call get to API_Request with the URL resource string and return an instance of the API_Response using the is_successful method. This method will tell us whether the status code of the request was http 200 or not. Now suppose we wanted to change the functionality of the API_Response class to match the HTTP/1.1 status code definitions, and say that any status code in the 200s is successful. To do this, we need to either edit the base class API_Response or extend it, as well as extend the API_Request class to call the new extended class API_Response. It's all a heck of a lot of work for such a simple change, especially when there's an option to update the code that uses these classes in your project.

Here, I'm going to do that by replacing API_Response with API_Better_Response and API_Request with 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); } }

Now we've rewritten the _build_response method to properly return the API_Better_Response class, but we would also have to go through our application code and find all instances of the API_Request and replace it with API_Better_Request.

Ruby to the Rescue: Monkey Dimple

Ruby has the ability to open a class anywhere and override or add methods. It's an extremely simple process – you just announce the class again. Let's take a look at the equivalent Request and Response classes in Ruby – again ignoring magical cryptic code that doesn't exist.

module API class Request def get(resource) content = magically_get_resource_content(resource) status_code = magically_get_resource_status_code(resource), 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 .

This type of dynamic class change is often referred to as Monkey Patching or, according to Wikipedia, Duck Punching. Obviously, the name has the form [animal] [verb of the participle of the past]. You can read about the origin of the name on Wikipedia.

So far, we've seen that Ruby allows for simple and flexible dynamic changes to classes at run time, which is extremely useful. You can also do a few monkey patches, but this will only affect code calls after it's fixed.

I should point out that Monkey Patching is often a quick fix that can create a headache for future developers (or for yourself if your memory is similar to mine) for several reasons:

  • The class logic that is expected to be in the source file is no longer where it should be – and it is difficult to track.
  • The code you fix may no longer work if the underlying library or code is updated and the methods you are fixing are removed or their behavior has changed.

I think I'm trying to say here: use at your own risk! You may find that by thinking a bit and doing a refactoring, you can achieve the same results in a cleaner way and save future you or future other developers a few headaches along the way.