Rails: Triển khai đối tượng Policy
- Tram Ho
Bài viết này yêu cầu bạn cần có kiến thức về hướng đối tượng, Ruby và Ruby on Rails.
Mẫu thiết kế policy object là gì?
Các đối tượng Policy đóng gói và thể hiện một quy tắc nghiệp vụ duy nhất.
Trong các ứng dụng của mình, chúng ta có thể có các quy tắc nghiệp vụ khác nhau được mã hóa chủ yếu dưới dạng if – else hoặc câu lệnh switch. Các quy tắc này đại diện cho các khái niệm trong miền (domain) của bạn, ví dụ như:
- Liệu “khách hàng có đủ điều kiện để được giảm giá hay không?”
- Liệu “email có được gửi hay không?”
- Liệu “người chơi có được tặng điểm hay không?”
Nguồn: Bài viết này
Bắt đầu thôi nào! (Tác giả sử dụng Rails API làm ví dụ nhưng bài viết này cũng có thể được triển khai trong Rails bình thường)
1. Đặt vấn đề
Giả sử chúng ta có bộ điều khiển (controller) sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <span class="token comment"># app/controllers/discounts_controller.rb</span> <span class="token keyword">class</span> <span class="token class-name">DiscountsController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">create</span></span> <span class="token keyword">if</span> can_user_get_discount<span class="token operator">?</span> code <span class="token operator">=</span> <span class="token constant">GenerateDiscountVoucherCode</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token variable">@current_user</span><span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">.</span>call render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"OK"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"Your Discount Voucher Code: <span class="token interpolation"><span class="token delimiter tag">#{</span>code<span class="token delimiter tag">}</span></span>"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">201</span> <span class="token keyword">else</span> render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"Failed"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"You are not allowed to get Discount Voucher"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">422</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">can_user_get_discount</span></span><span class="token operator">?</span> is_premium<span class="token operator">?</span> <span class="token operator">&&</span> last_discount_more_than_10_days_ago<span class="token operator">?</span> <span class="token operator">&&</span> high_buyer<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">is_premium</span></span><span class="token operator">?</span> <span class="token variable">@current_user</span><span class="token punctuation">.</span>premium<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">last_discount_more_than_10_days_ago</span></span><span class="token operator">?</span> <span class="token variable">@current_user</span><span class="token punctuation">.</span>last_discount_sent_at <span class="token operator"><</span> ten_days_ago <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">ten_days_ago</span></span> <span class="token builtin">Time</span><span class="token punctuation">.</span>now <span class="token operator">-</span> <span class="token number">10.</span>days <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">high_buyer</span></span><span class="token operator">?</span> <span class="token variable">@current_user</span><span class="token punctuation">.</span>total_purchase_this_month <span class="token operator">></span> <span class="token number">5</span>_000 <span class="token keyword">end</span> <span class="token keyword">end</span> |
Bạn đặt tất cả các logic policy (quy tắc nghiệp vụ) trong controller của mình. Đây không phải là một giải pháp tốt. Đặc biệt trong trường hợp bạn phải xử lý nhiều logic phức tạp (controller nên chỉ sử dụng để điều hướng, tránh xử lý nhiều logic phức tạp).
Nhưng, ta có thể bỏ qua class GenerateDiscountVoucherCode
. Ta chỉ cần biết là lớp này chịu trách nhiệm tạo ra mã voucher giảm giá.
2. Di chuyển logic Policy vào trong model
Well, chúng ta có thể chuyển logic policy vào trong model. Vì thế, nó chuyển thành như vầy:
app/controllers/discounts_controller.rb
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">class</span> <span class="token class-name">DiscountsController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">create</span></span> <span class="token keyword">if</span> <span class="token variable">@current_user</span><span class="token punctuation">.</span>can_get_discount<span class="token operator">?</span> code <span class="token operator">=</span> <span class="token constant">GenerateDiscountVoucherCode</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token variable">@current_user</span><span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">.</span>call render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"OK"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"Your Discount Voucher Code: <span class="token interpolation"><span class="token delimiter tag">#{</span>code<span class="token delimiter tag">}</span></span>"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">201</span> <span class="token keyword">else</span> render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"Failed"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"You are not allowed to get Discount Voucher"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">422</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
app/models/user.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> enum membership<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">'regular'</span><span class="token punctuation">,</span> <span class="token string">'premium'</span><span class="token punctuation">]</span> <span class="token constant">MINIMUM_PURCHASE</span> <span class="token operator">=</span> <span class="token number">5</span>_000 <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">can_get_discount</span></span><span class="token operator">?</span> <span class="token keyword">self</span><span class="token punctuation">.</span>premium<span class="token operator">?</span> <span class="token operator">&&</span> <span class="token keyword">self</span><span class="token punctuation">.</span>last_discount_more_than_10_days_ago<span class="token operator">?</span> <span class="token operator">&&</span> <span class="token keyword">self</span><span class="token punctuation">.</span>high_buyer<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">last_discount_more_than_10_days_ago</span></span><span class="token operator">?</span> <span class="token keyword">self</span><span class="token punctuation">.</span>last_discount_sent_at <span class="token operator"><</span> ten_days_ago <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">ten_days_ago</span></span> <span class="token builtin">Time</span><span class="token punctuation">.</span>now <span class="token operator">-</span> <span class="token number">10.</span>days <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">high_buyer</span></span><span class="token operator">?</span> <span class="token keyword">self</span><span class="token punctuation">.</span>total_purchase_this_month <span class="token operator">></span> <span class="token constant">MINIMUM_PURCHASE</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Nhưng đây vẫn chưa phải giải pháp tối ưu. Việc thêm logic policy object mặc dù đã giúp tối ưu controller nhưng lại làm phồng model. Bây giờ, hãy triển khai mẫu thiết kế policy object!
3. Tạo một lớp riêng biệt
Bây giờ, ta sẽ tạo một lớp riêng biệt chứa các logic policy như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <span class="token comment"># app/lib/discount_voucher_policy.rb</span> <span class="token keyword">class</span> <span class="token class-name">DiscountVoucherPolicy</span> <span class="token constant">MINIMUM_PURCHASE</span> <span class="token operator">=</span> <span class="token number">5</span>_000 <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span><span class="token punctuation">(</span>user<span class="token punctuation">)</span> <span class="token variable">@user</span> <span class="token operator">=</span> user <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">allowed</span></span><span class="token operator">?</span> is_premium<span class="token operator">?</span> <span class="token operator">&&</span> last_discount_more_than_10_days_ago<span class="token operator">?</span> <span class="token operator">&&</span> high_buyer<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">is_premium</span></span><span class="token operator">?</span> <span class="token variable">@user</span><span class="token punctuation">.</span>premium<span class="token operator">?</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">last_discount_more_than_10_days_ago</span></span><span class="token operator">?</span> <span class="token variable">@user</span><span class="token punctuation">.</span>last_discount_sent_at <span class="token operator"><</span> ten_days_ago <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">ten_days_ago</span></span> <span class="token builtin">Time</span><span class="token punctuation">.</span>now <span class="token operator">-</span> <span class="token number">10.</span>days <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">high_buyer</span></span><span class="token operator">?</span> <span class="token variable">@user</span><span class="token punctuation">.</span>total_purchase_this_month <span class="token operator">></span> <span class="token constant">MINIMUM_PURCHASE</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Như vậy, controller và model sẽ trông như sau:
app/controllers/discounts_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token keyword">class</span> <span class="token class-name">DiscountsController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">create</span></span> <span class="token keyword">if</span> policy<span class="token punctuation">.</span>allowed<span class="token operator">?</span> code <span class="token operator">=</span> <span class="token constant">GenerateDiscountVoucherCode</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token variable">@current_user</span><span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">.</span>call render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"OK"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"Your Discount Voucher Code: <span class="token interpolation"><span class="token delimiter tag">#{</span>code<span class="token delimiter tag">}</span></span>"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">201</span> <span class="token keyword">else</span> render json<span class="token punctuation">:</span> <span class="token punctuation">{</span> status<span class="token punctuation">:</span> <span class="token string">"Failed"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> <span class="token string">"You are not allowed to get Discount Voucher"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> status<span class="token punctuation">:</span> <span class="token number">422</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">policy</span></span> <span class="token constant">DiscountVoucherPolicy</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span><span class="token variable">@current_user</span><span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
app/models/user.rb
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> enum membership<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">'regular'</span><span class="token punctuation">,</span> <span class="token string">'premium'</span><span class="token punctuation">]</span> <span class="token keyword">end</span> |
4. Tổng kết
So với controller ban đầu của chúng ta, việc này “chỉ là” đặt logic policy vào một class riêng. Đó là sự thật, vì mục đích chính của chúng ta là chuyển logic policy ra khỏi controller và model.
Các đối tượng policy đóng gói và thể hiện một quy tắc nghiệp vụ duy nhất.
Bạn sẽ thấy mẫu thiết kế này hữu ích trong trường hợp bạn có logic policy phức tạp. Ví dụ mà tác giả đưa ra là một logic policy đơn giản.
Tài liệu tham khảo
Bài viết trên được dịch từ nguồn Rails: Policy Objects Implementation
Cảm ơn các bạn đã đọc bài, chúc các bạn có một ngày làm việc hiệu quả