팩워크 에피소드 1을 통한 Ruby on Rails 모듈화
인간은 많은 시간과 노력을 들이지 않고는 문제의 큰 그림을 보기가 어렵습니다. 이는 특히 크고 복잡한 애플리케이션을 작업할 때 발생합니다....
A common problem while working with Rails is to decide where to place the logic from our features.
The logic is often placed in the Controllers, Models, or if we are lucky in a Service Object. So if we have Service Objects then why do we need Use Cases?
Follow me in this article to discover the benefits of this pattern.
A use case is a list of actions or event steps typically defining the interactions between a role and a system to achieve a goal.
It’s worth mentioning that this pattern is applied in many different ways and has alternative names. We can find it as Interactors, Operators 또는 Commands, but in the Ruby community we stick with Use Case. Every implementation is different but with the same purpose: to serve a user’s use case of the system.
Even if in our 프로젝트 we are not defining the requirements using Use Cases and UML this pattern is still useful to structure the business logic in a practical way.
우리의 Use Cases must be:
Let’s take the example of a user that wants to purchase something in our system.
module UseCases
module Buyer
class Purchase
def initialize(buyer:, cart:)
@buyer = buyer
@cart = cart
end
def call
return unless check_stock
return unless create_purchase
notify end
private
attr_reader :buyer, :cart
def check_stock
Services::CheckStock.call(cart: cart)
end
def create_purchase
Services::CreatePurchase.call(buyer: buyer, cart: cart).call
end
def notify
Services::NotifyBuyer.call(buyer: buyer)
end
end
end
end
As you may see in this 코드 example, we created a new Use Case called Purchase. We defined only one public method 통화. Inside the call method, we find pretty basic steps to make a purchase, and all the steps are defined as private methods. Every step is calling a Service Object, this way our Use Case is only defining the steps to make a purchase and not the logic itself. This gives us a clear picture of what can be done in our system (make a purchase) and the steps to achieve that.
Now we are ready to call our first Use Case from a controller.
class Controller
def purchase
UseCases::Buyer::Purchase.new(
buyer: purchase_params[:buyer],
cart: purchase_params[:cart]
).call
...
end
...
end
From this perspective, the Use Case looks pretty much like a Service Object but the purpose is different. A Service Object accomplishes a low-level task and interacts with different parts of the system like the Database while the Use Case creates a new high-level abstraction and defines the logical steps.
Our first Use Case works but could be better. How could we improve it? Let’s make use of the dry gems. In this case we are going to use dry-transaction.
First let’s define our base class.
class UseCase
include Dry::Transaction
class << self
def call(**args)
new.call(**args)
end
end
end
This will help us to pass attributes to the UseCase transaction and use them. Then we are ready to re-define our Purchase Use Case.
module UseCases
module Buyer
class Purchase
def initialize(buyer:, cart:)
@buyer = buyer
@cart = cart
end
def call
return unless check_stock
return unless create_purchase
notify
end
private
attr_reader :buyer, :cart
def check_stock
Services::CheckStock.call(cart: cart)
end
def create_purchase
Services::CreatePurchase.call(buyer: buyer, cart: cart).call
end
def notify
Services::NotifyBuyer.call(buyer: buyer)
end
end
end
end
With the new changes, we can see in a clear way how our steps are defined and we can manage the result of every step with Success() and Failure().
We are ready to call our new Use Case in the controller and prepare our response depending on the final result.
class Controller
def purchase
UseCases::Buyer::Purchase.new.call(
buyer: purchase_params[:buyer],
cart: purchase_params[:cart]
) do |result|
result.success do
...
end
result.failure do
...
end
end
...
end
...
end
This example could be improved even more with validations but this is enough to show the power of this pattern.
Let’s be honest here, the Use Case pattern is pretty simple and looks a lot like a Service Object but this level of abstraction can make a big change in your application.
Imagine a new developer joining the project and opening the folder use_cases, as a first impression he will have a list of all the features available in the system and after opening one Use Case he will see all the necessary steps for that feature without going deep in the logic. This sense of order and control is the major benefit of this pattern.
Take this in your toolbox and maybe in the future you will make good use of it.