Interface as Business Logic — A Better Design of Form Objects

Before talking about Form Objects, let’s talk about refactor first. Many times we refactor the code because we’ve changed the interface of a function. The pain of modifying multiple places is understandable because the implementation and the usages of the function are meant to be coupled by its interface. By John Ousterhout’s definition from the book A Philosophy of Software Design, this is called “Information Leakage”.

Information leakage occurs when the same knowledge is used in multiple places.

And the leakage of the interface is unavoidable because interfaces are meant to be used from the outside. However, the fact that we can’t decouple usages from the definition does not mean we can’t improve our design. The key is to wisely decide what should be the interface.

The idea of improving the interface design comes from my practice of adopting Form Objects to our Rails application. First, let’s look at what a normal Rails controller looks like.

Nothing special. However, as the application evolves, the logic of the create action may become more complicated than a simplecreate! call. To handle all the complexity of constructing a record from many parameters with error handling, we may want to introduce Form Objects.

This way we keep the controller file small. But from the code above, we can see we also introduced information leakage — the parameters of params.permit and UserFormObject#initialize must keep the same.

To solve it, we can hide this information in one place. And where is the best place? The insight is that the purpose of factoring a Form Object out of a controller is actually factoring the business logic out of a framework. And normally the business logic changes faster than the framework. So it’s better to hide that information in the Form Object.

Now the interface of the UserFormObject is unified as params . This way, the change of the business logic will not affect the controller code but only the Form Object code.

Looks good. But there’s one problem: the parameters required for different actions are different. To handle this we can either create different Form Objects for different actions (e.g. UserCreateFormObject , UserRenameFormObject ). We can do that, but the problem is those Form Objects may share some common information and hence we introduced information leakage again.

Ruby as a language with reflection, allows us to use the information of the code structure to further DRY out the code. Consider a complete UserFormObject like this:

We can see there’re some parts can be shared between #create! and #rename! such as “validate names” and “process names”. And we can tell that the functionalities being used inside each action are actually dictated by the parameters of params.permit . And the params.permit line also can be shared across actions.

What if we make the parameters of params.permit as the parameters of the action, and let all other code depends on this information to query the method parameter itself?

The instance method #create! and #rename! now know nothing about the params . They only care about business logic. And once the business logic changed, they can just simply change the function interfaces without worrying about also updating the usages of the methods. The FormObject will be in charge of calling instance methods with proper permitted parameters.

Here’s a complete example:

To understand how it works. Let’s break down the FormObject line by line.

The goal of this method_missing is to convert a method call like

into

So it should only do its job if the class has the instance method with the same name:

And then we can get the formal parameters from m . For example:

The code will return:

Since the second field of each entry is the formal parameter name, now we can call permit on the params with the formal name list and call the instance with proper method arguments.

Theoretically, we can let the method signature be of any kind. But to simplify the code and also to enforce the readability, we require that all the method parameters should be keyed.

Then, a common practice of method_missing is to create a function as the cache so the next call of the same name will not trigger the method_missing computation a second time.

Since the method_missing is triggered by a method call attempt, so after the method definition, don’t forget to also call that function for this time.

And inside the defined method is the actual logic we want to run every time. We’ve already known the formal name list, so we can just simply convert that as the actual parameter list. Note that Ruby requires the keyed parameters’ keys should be of type Symbol . But ActionController::Parameters#to_hash contains String keys. So we will need to do a conversion here:

Then we can call the instance method with a new instance:

To conclude, the Form Object is only one example to demonstrate how should we recognize information leakage and resolve it. Also, we can further avoid the leakage problem if we can get the information from the code structure itself via reflection.

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store