[RUBY] Consideration about Rails and Clean Architecture

I had the opportunity to think about Rails application design, so I would like to summarize what I thought in the process.

background

A company that mainly uses Rails has decided to create a new web application based on 0. The company runs a fairly large app on Rails, and Rails itself has enough knowledge.

On the other hand, I feel that the traditional Rails development method has the limit of large-scale development, and there is also a desire to take this opportunity to explore Prcatice for design.

The author is confident in the design of the application, but the languages I usually use are mainly Scala and NodeJS (TypeScript). I've been developing with Rails many times, and I'm not particularly inconvenienced in development, but I don't know much about the detailed quirks of the framework.

This requirement doesn't consider replacing Rails itself with another language or framework in order to harness the skills of the original team.

The main purpose of this document is to consider how to take advantage of Rails and incorporate modern design techniques.

Basic design policy

Clean Architecture is used as the underlay.

Clean Architecture (CA) is a design method that has rapidly become popular in the last few years, and many engineers have come to mention it in Japan as a result of the publication of the Japanese version of the book.

The author himself has experience in development based on a few CAs (some are designed by himself and some are developed based on human design), and I feel that this method is very cool.

On the other hand, I also feel some issues when putting CA on the existing Web application framework, and I thought that I would like to find my own winning pattern by adding consideration to it, so I decided to bother to document this. It is one of the motives.

By the way, at the time of writing this section, there are still some points that are unpleasant, so I hope that they can be organized by writing them.

Incompatibility between CA and Rails

So how do you apply the essence of CA to Rails? I started thinking about the proposition, but before I knew it, I stumbled upon its incompatibility.

This is a matter of course when you think about it.

This is because Clean Architecture aims at "** a universal design method that works regardless of the framework used ", while application frameworks such as Rails " depend on the framework". This is because the aim is to minimize development costs ** ".

In other words, the direction you are aiming for from the beginning is exactly the opposite, so it cannot be compatible.

In particular, Rails is a monster framework that pursues "minimizing development costs by making it dependent on the framework" to the utmost limit, so compatibility can be said to be the worst.

The solution to this problem in CA is that it should be implemented inside the concentric circles (Entity and Usecase) without using the framework functionality. In fact, the projects I've been involved with have taken that approach (if not perfect).

You can take the same approach with Rails, but I can't honestly say it's good because it completely spoils the goodness of Rails.

In fact, if Rails Way does a process that requires only 3 lines, it may require dozens of lines of code across multiple files according to CA's method, so is it worth the cost? Is doubtful.

I mean, if you develop by yourself, you definitely choose Rails Way, in this case. .. .. ..

Perhaps many Rails programmers in the world feel the same way. As expected, a fully CA-compliant approach is likely to be unacceptable to Rails engineers.

In other words, the conclusion of this section is that in this case, we need to think of a new way to incorporate the essence of CA while retaining the goodness of Rails. (If you adopt the CA-compliant method, I think you need a strong reason to convince it, but I can't think of it for a moment. As mentioned above, the goals are different, so list the advantages in each axis. But I don't think it's a kind of religious war and I can convince the other, and I personally don't think one is right and the other is wrong. )

CA Entity and ActiveRecord

Rails is ActiveRecord. The convenience of Rails is largely due to the existence of ActiveRecord, and it is no exaggeration to say that there is no point in using Rails if you do not use ActiveRecord.

According to CA, Entity should be redefined in a way that is independent of ActiveRecord, but in Rails I don't think it's a good idea to eliminate ActiveRecord here.

In Ruby, which is a dynamically typed language, you can't create an interface, you just handle Objects in the DuckTyping newsletter, so in the case of a simple model, even if you redefine it, it seems likely that it will eventually become an object that can be exchanged with ActiveRecord. That's right. .. ..

If the object you pass as an argument to the method works, whether it's a CA Entity or ActiveRecord, and there's no way to limit it, redefining can be a daunting task. I'm afraid that.

If so, I think it would be ant to use the ActiveRecord model as it is for Entity that can do simple table mapping at this time.

However, I don't think it's a good idea to define methods that are directly related to business logic in ActiveRecord, so if you need to define such methods, redefine them.

Also, I think it is better to redefine things that are stored across multiple tables on the DB, such as invoices, but are inseparable as business entities.

The boundary between what needs to be redefined and what doesn't is ambiguous, but for the time being, I think it's okay to use ActiveRecord directly at first, and start with the rule of redefining when you feel the need. I will. (This is a choice based on the small number of project members at the start. I think it's better to think a little more carefully for a project with many members from the beginning, but it has nothing to do with such a large-scale project in the first place. There is no further consideration here.)

When redefining, it is not necessary to completely eliminate ActiveRecord, it is enough to define it in a wrapped manner. For example, like this

#Invoice
class Bill

  initialize(ar_model)
    @ar_model = ar_model
  end

  def corp_name
    @ar_model.corp_name
  end

  #billing statement
  def bill_details
    #I don't want to expose Relation, so to_a
    #If necessary, modify it to map to the Entity redefined in the constructor.
    @ar_model.bill_details.to_a 
  end
end

The problem is that ActiveRecord has a lot of side effect methods (save, update, etc.) that can be called from anywhere, but for the time being, "Prohibit the use of side effect methods from files other than repository". I feel that it is enough to make rules. (This is also a small group choice. I think it's possible to check this rule with Lint, whether or not you actually do it.)

To be honest, I don't think I might change my mind while doing this, but for the time being, the start is like this. (Omitted for small groups or less)

Directory structure

The directory structure when I did the CA structure in a project other than Rails was as follows.

- domain
  - <Domain name 1>
    - <Entity file>
    - <Entity file>
  - <Domain name 2>
    - <Entity file>
    - <Entity file>
  - ...
- repository
  - <Domain name 1>
    - <Repository file>
  - <Domain name 2>
    - ...
- usecase
  - <Domain name 1>
    - <Usecase file>
    - <Usecase file>
  - <Domain name 2>
    - ...

The domain name is a name that largely separates the area handled by the application, such as "User" or "Order".

For Scala projects, this configuration is meaningful because you can use subprojects to make the domain layer files absolutely independent of usecases and repositories.

But what about Rails?

Unfortunately, Rails doesn't seem to be able to limit such dependencies.

There is another problem, Rails strongly recommends naming according to autoload, so in the case of the above configuration, Module name + Class name is

Entity, Repository, Usecase all end up in the same namespace. This is not very good.

Go down one step further,

However, for Japanese speakers, it is more natural for the main modifier to come first, so

Is more comfortable.

So, based on this, I think that the directory structure should be as follows.

- app
  - domain
    - <Domain name 1>
      - entity
        - <Entity file>
      - repository
        - <Repository file>
      - usecase
        - <Usecase file>
    - <Domain name 2>

Assuming this policy is taken, the problems and answers that came up in advance are as follows.

Common names like "User" and "Order" are top-level Module names.

I think it's rather good because it is easy to understand that the area name to be handled appears at the top level.

However, in Rails, it is customary to define ActiveRecord directly under models, and since "User" and "Order" are likely to have names as much as possible, ActiveRecord side is set to "AR :: User", etc. under module. Must be placed.

There is also a proposal to set the domain definition to "Domain :: User :: Entity :: User", but it is not "** Domain User " but " Domain is User **". , I feel that it is redundant as a modification. (Personally, I value the feeling that it fits nicely in Japanese, except when there are foreign members.)

Also, I think that it is advantageous to have a naming that shows that the ActiveRecord model belongs to it, so it is better to change the naming on the ActiveRecord side if possible.

By the way, in the previous section, I wrote that "ActiveRecord can be used directly as an Entity of Domain", but ActiveRecord itself will be placed under models as before with an emphasis on listability. (At the time of writing this, I'm starting to think that it's better to always define a thin wrapper as Domain / Entity ... but I'll think about it while making it.)

Usecase may work across multiple domain objects

I agree. Isn't it okay?

I think that Entity and Repository should be completed within that domain, but I think that Usecase does not have any problem even if it handles multiple domains.

If it doesn't fit well as a file location, you can define another domain without Entity or Repsitory and place only Usecase.

It's a shame, but in Repository, I think it's okay to include the Entity module of the same domain. But Usecase is no good. (In fact, if you pass various Repository in the constructor, I think that there is almost no scene where the name of Module name qualification appears.)

Simplification

The following is not a CA textbook, but I am thinking of doing it to speed up development.

Entity acquisition by ID may be module function of domain Module

Like this

module Order extend self

  order_repository
    @order_repository ||= Order::Repository::OrderRepository.new
  end

  module_function
  def get_order_by_id(order_id)
    order_repository.get(order_id)
  end
end

Entity acquisition by specifying ID is a rule that you can create a shortcut if necessary because there are scenes that you want to use even when debugging. (It is not always created for all Entity.)

If the process equivalent to GET / XXXXs /: id of routes is in time, I think that it is not necessary to define Usecase, but in most cases, Permission check (in the case of the above Order, myself I think that you will need (invisible except for Order), so in that case you need Usecase.

Define usecase as module function of domain Module

In general, Usecase takes various Repository as arguments in its constructor. This is because it is difficult to write a test unless the Repository is replaced at the time of testing.

However, it is troublesome to specify the Repository every time you use Usecase from Controller, so create a shortcut in the domain Module.

Like this

module Order extend self

  order_repository
    @order_repository ||= Order::Repository::OrderRepository.new
  end

  module_function
  def create_order_usecase
    Order::Usecase::CreateOrder.new(
      order_repository
    )
  end
end

The user side

  val res = Order.create_order_usecase.run(...)

The content you want to do is clear.

I think it's okay to specify the default arguments on the Usecase side, but that's a matter of taste.

test

To be honest, I don't have much knowledge about testing with Rails, and I'm groping. Also, in terms of man-hours, I can't afford to write a test, so I think it will be a form of writing partially from where it is needed.

So basically,

--Entity with logic

As long as you can write a test with a focus on **, you can actually write a test later. If the Repository is a simple ActiveRecord wrapper, the test should be at the lowest priority. (If it depends on an external service, a separate test or Mock is required.)

Testing Entity shouldn't be a problem. For Usecase, writing a test may be troublesome for some things, but in general, Usecase is

--validated --Validation of input parameters --collect --collect the required Entity from Repository --execute --Execute the process you want to perform with Usecase

It often consists of 3 steps, and you may want to test only the execute part, so I think it would be good if you could test only the execute part as needed.

Like this

class Domain1::Usecase::HogeHogeUsecase

  initialize(domain1_repository)
    @domain1_repository = domain1_repository
  end

  def run(params)
    error = validate(params)
    return ErrorCase.new(error) if error

    [v1, v2, error] = collect(params)
    return ErrorCase.new(error) if error

    return execute(v1, v2)
  end

  def validate(params)
    ...
  end

  def collect(params)
    ...
  end

  def execute(v1, v2)
    ...
  end
end

You should now be able to write tests without worrying about the state of the persisted data.

Summary

So far, I have summarized what I thought about in the early stages of development. Isn't it the biggest harvest that the organization has progressed within me by writing it?

My Rails power is not high, and I think there are some pros and cons. But, well, it doesn't really matter. On-site design does not have to be accepted by everyone.

However, I think it is enough if the members of the team share the idea that this project is "made with this kind of policy" and maintain consistency.

Seeking the correct answer is a job in the academic field, and I think that if an engineer in the field asks for it, he will just get stuck.

It is impossible to take everything positively, but I hope there is something helpful.

In the process of making, if there is something to change, I will add it again. (Because this document is the very document for sharing "I'm making this policy" with team members.)

Recommended Posts

Consideration about Rails and Clean Architecture
Consideration about classes and instances
About Rails 6
About Rails routing
About Rails controller
About RSpec (Rails)
[Rails / ActiveRecord] About the difference between create and create!
A note about the Rails and Vue process
[Rails] I learned about the difference between resources and resources
[Rails] About migration files
[Rails] About active hash
Rails valid? And invalid?
About rails application server
Design and implement a breakout game with a clean architecture
About Bean and DI
About rails kaminari pagination
About classes and instances
About rails version specification
Design and implement a breakout game with a clean architecture
MEMO about Rails 6 series
About redirect and forward
[Rails] About Slim notation
About Serializable and serialVersionUID
[rails] About devise defaults
A memorandum about table data types and commands (Rails)
Rails: About partial templates
About rails strong parameters
[Beginner] About Rails Session
About for statement and if statement
About synchronized and Reentrant Lock
about the where method (rails)
[Rails] N + 1 problems and countermeasures
Rails: Difference between resources and resources
Rails Posts and User Linkage
[Rails] require method and permit method
Rails Tutorial Records and Memorandum # 0
rails path and url methods
[Ruby on Rails] about has_secure_password
Rails is difficult and painful!
About naming Rails model methods
[Java] About String and StringBuilder
Introducing Bootstrap and Font-Awesome (Rails)
About the same and equivalent
[Rails] About scss folder structure
Rails is difficult and painful! Ⅱ
About classes and instances (evolution)
About pluck and ids methods
ArchUnit Practice: Clean Architecture Architecture Testing
[Rails] About Rspec response test
[Rails] strftime this and that
About Java Packages and imports
About Ruby and object model
Consideration about the times method
About Ruby classes and instances
Rails web server and application server
About instance variables and attr_ *
About self-introduction and common errors
About Rails scraping method Mechanize
[Book Review] Clean Architecture Software structure and design learned from masters