add content about controllers and models

ruby/structure-of-web-app
Thomas Riboulet 4 years ago
parent 10b60c8f29
commit 49aaa3a4c7

@ -1 +1,101 @@
# Structure of a Ruby web app
Web application in Ruby have one main model : RubyOnRails.
Sinatra is a little bit different but roughly follow a very similar pattern out of the box.
Hanami follows a different pattern on several levels.
The question we are trying to address here is the complexity we tend to have in RubyOnRails applications. Controllers tend to get fat, models tend to get fat. Both those symptoms cause our code to be overly complex, disregard single responsibility principles and get increasingly difficult to handle.
## Controller and Actions
Hanami is separating each action into its own class. This is great to keep the overall action code limited inherently. But we don't really have that option with RubyOnRails. We could totally do it, but it's not supported out of the box.
### The symptoms
One issue is often the addition of non REST actions to cover some additional use cases. This is usually a symptom of modeling choices to fit a forced UI principle or idea. We shall consider that this can be adressed with a better modeling of the action needed to fall back to a Create, Update, Delete or Show action.
The main symptom we see is the growth of one or more of the 5 actions into big blobs of code doing many things.
### The aim
Let's start from the point of view that actions should stay as light as possible. They should be limited to received parameters, filtering them, triggering one call, getting a quick response from that call and passing the result to the render helper with appropriate local variables passed within.
```
def show
good_params = filter(params)
result = do_something(good_params)
render :show, locals: result
end
```
The idea is that the action should only care about getting params in, passing them to something doing the work and then rendering the view with the data returned.
### How we go there
This is probably the main point of this discussion : how do we handle this "do_something" ?
I have seen several times code bases rely on the Command, Operation or Actor pattern :
- https://mkdev.me/en/posts/a-couple-of-words-about-interactors-in-rails6
- https://refactoring.guru/design-patterns/command / https://refactoring.guru/design-patterns/command/ruby/example#lang-features
- https://github.com/collectiveidea/interactor (a popular library for this)
As pointed out in the first link the goal is to encapsulate the business logic into abstractions. This meet our needs : we want to abstract away the complexity (the business logic) of what happens behind the scene out of the controller action. This is based on Domain Driven Design principles.
The controller action should not care how the object is retrieved it should just get it and pass it to the view for displaying. The controller action should not care how an order is placed, it should just get the details of the order from the params, pass them on to what handles it and then display the result of that action.
The interactor library gives an example of implementation of such a pattern. It includes a way to pass context along a chain of interactors and a way to know the state and result of the chain. This might be a bit too much magic for some and it's possible to rely on plain old Ruby objects to do something that is enough for our needs.
The idea though is to be able to abstract in one or more layers our complexity by sticking to things such as the Single Responsibility Principle. Those Command are still objects so there is no free pass there to just move the blob into one and claim victory.
There should be some structure to this effort too. All Command objects should follow the same pattern and should be called, or triggered in the same way. The naming should suggest that they are Command and will do something through Namespacing or their name itself.
```
class Command::PrepareOrder
def initialize(needed_data)
end
def call(extra_data)
end
end
```
> This is one main point of inquiry here : do we consider this type of class and objects to be a desirable way to abstract complexity from the controller action ?
Another question might quickly pop up when one starts to use such classes : do we use instance methods or class methods ?
I have seen the two different takes on this. One relies on instance methods like in the previous example. The other one relies on a class method named "perform" or "call" :
```
class Command::PrepareOrder
def self.call(data)
# do something
end
end
```
This could actually rely on instances of the class but usually it's used as is.
If one is handling instances of the class and instance methods then it's probably not difficult to rely on attributes of that instance for state and context observation.
> This is another point of inquiry here : instance methods vs class methods. Or maybe there are cases where one is better than the other and vice versa.
## What about the model
The obvious next step is to look at how the model is handled and how it can become fat.
Models can also become bloated with business logic. As they do, it becomes difficult to see the limit between what is related to the handling of the data itself to the business logic part.
The idea is that it's a lot clearer to work with classes that have less responsibilities. There should be a class handling just the database modeling it is meant to express, then one that relies on it but just takes care of the extra stuff on top.
That way, if the underlying data store is swapped the extra logic is less impacted or actually does not see a change.
Hanami relies on such separations with the concepts of Entity and Repository. The Entity is the data representation, the Repository is the abstraction in between the Entity and the data store.
Can we use something similar in RubyOnRails ? I think this is a tricky one as ActiveRecord does a great job handling most of the Repository part in the shadows. The result though is that it's difficult to alter the data storage layer for one reason or another without having to replace the parent class of the model with something custom.
This could be something to look into : add an abstraction in between the ActiveRecord model and its use.
> Is that actually an issue? The main problems we see are related to infiltration of business logic within the Controller actions and the Models. Can the Command and Decorator patterns help enough to move most if not all business logic out of the Controller actions and Models ?

Loading…
Cancel
Save