Interface as Business Logic — A Better Design of Form Objects

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 likeFormObject.

create!(params)intoFormObject.

new.

create!(params.

permit(:email, :first_name, :last_name))So it should only do its job if the class has the instance method with the same name:super unless (m = instance_method(name))And then we can get the formal parameters from m .

For example:class C def foo(a, b = 1, c:, d: nil); end puts instance_method(:foo).

parameters.

inspectendThe code will return:[[:req, :a], [:opt, :b], [:keyreq, :c], [:key, :d]]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.

raise "all should be keyed" if param_types.

find {|t| t !~ /key/}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.

define_singleton_method(name) { .

} # only the necessary computation needed for each callSince 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.

send(name, *args, &block)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:ps = params.

permit(*param_names).

to_hash.

transform_keys!(&:to_sym)Then we can call the instance method with a new instance:m.

bind(new).

call(ps)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.

.. More details

Leave a Reply