The Problem
Refactoring is hard, but I believe looking at the code that is hard to read is even poisonous to the team. Code smell happens all the time, and how we refactor it not only improve the readability but also provide a good sample for incoming juniors’ career development in the company.
Let’s looks that this piece of code:
1 | module Api |
Even the functions are separated, but let’s refresh the responsibility of the Rails controller
1 | It coordinates the interaction between the user, the views, and the model. The controller is also a home to a number of important ancillary services. It is responsible for routing external requests to internal actions. |
Simple and elegant. However, what we did here kind of violates the first SOLID principles which is Single Responsibility.
Look at the line 6 to 7, basically it’s doing the job besides the controller routing but to validate errors. If we want to customize and even extend more validations, might end up all the private methods in controller and action in front of action definition.
Also, line 8 to 11 also comes across a request of a third party handler, even a RestClient or HTTParty request being put here looks out of place. If we want to extend more methods like post or form requests, definitely will bloat up the private functions being defined in the controller here.
Of course, line 13 to 18 looks out of place too. We have a decorator/serializer here but strongly depend on the single attribute of the response. And other responses seem not to fit in this decorator. What if we want to change the format when the endpoint change, this looks like a violation of Open/Close Principle for me.
Let’s refresh the definition of the Open/Close Principle
1 | software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification |
What I Did Here
Let’s first extract the request handler to its own module
1 | module Anaylytics |
Looks good, right? Now the request has its own handler and we can simply define methods like GET, POST of our own and extend easily.
And the singleton class with method_added being delegated, allows us not to write .instance all the time but to have the property of instance like class. This is a better thread safe management and allow us to extend some ActiveSupport methods that are specifically for instance methods.
Now let’s define our ErrorsHandler
1 | module Anaylytics |
Now we have customized errors and rescuer being defined in a single class. And adopt the use of few ActiveSupport modules to make it even cleaner like rescue_from allow us to define rescue function to its own method name.
How about ParamsMissing and DateInvalid in the original controller action? Seems not being placed here, and that’s right! The logic sounds more like a model to me and I think should deserve to have its own validator here.
1 | module Analytics |
I sometimes find ActiveModel::Validations is very useful when we want to define something that’s not actually in current scope of Rails database model. And we can still adopt the use the simple and elegant validation solution to its own class.
And last we see a format service that is running wild and not unified for each response in the original action. Let’s rename it as presenter to delegate the response object.
1 | module Analytics |
as_json allows the object in render json: to be automatically called and transform to JSON string. It’s an example of Liskov Subsitution Principle or Duck Typing technique since we want the response-like object to be act like the response resulting from RestClient
Let’s look at our refreshed request handler
1 | module Anaylytics |
Since we do not need all the presenters at once, and probably only need to present/decorate the response at each request. The Open/Close Principle is quite suitable here which gives us the idea to inject the module class we want.
Finally, our end result controller action will be like
1 | ... |
I think this example demestrate some of the SOLID principles and design patterns we can use in daily basis. Thanks for reading!