7 pattern để cấu trúc lại ActiveRecord Models
Bài đăng này đã không được cập nhật trong 7 năm
Khi team muốn nâng cao chất lượng ứng dụng Rails, chúng ta phải tìm cách để loại bỏ thói quen làm model trở lên Fat
. Fat models
là gây ra các vấn đề về bảo trì trong các ứng dụng lớn.
Vì sao Model lại Fat ?
1. Áp dụng sai SRP
Chỉ gia tăng chứ không tập hợp các controllers vào theo miền logic chính là thể hiện của việc áp dụng sai SRP
. Bất cứ gì liên quan tới người dùng đều không phải là single responsibility
.
Ban đầu, khi áp dụng SRP thật dễ dàng. Các class ActiveRecord mới đầu chỉ xử lý các persistence
, associations
, và nói chung là không có quá nhiều xử lý. Nhưng dần dần các class này lớn lên. Các object vốn chỉ xử lý các persistence
thì thường phải xử lý thêm tất cả các logic business. Và sau một hoặc hai năm, một class User
cũng có thể vượt qua 500 dòng code với hàng trăm public method.
Khi bạn thêm xử lý phức tạp vào ứng dụng của mình, mục đích là để trải nó ra một tập hợp các đối tượng được đóng gói (ở mức cao hơn là các modules), giống như việc bạn rắc bột bánh trên chảo vậy. Lúc rắc bột bạn sẽ thấy những cục bột to, nó chính là Fat models
. Bạn sẽ cấu trúc lại để phá vỡ chúng và trải đều các logic ra. Lặp lại quá trình này, bạn sẽ thu được các đối tượng đơn giản với interface làm việc cùng nhau rõ ràng hơn.
2. Rails thiếu những convention để quản lý sự phức tạp vượt quá pattern ActiveRecord có thể xử lý
Bạn có thể nghĩ rằng Rails làm cho mọi chuyện thật khó khăn để làm đúng OOP. Tuy nhiên không phải vậy, Rails không cản trở OOP
mà nó chỉ thiếu những convention cho việc quản lý sự phức tạp vượt quá pattern ActiveRecord có thể xử lý. May mắn thay chúng ta có thể ghép OOP
cơ bản vào những nơi Rails thiếu.
Vậy làm thế nào để Model hết Fat
?
Không trích xuất Mixins từ Fat models
Việc đưa một tập các method từ một class ActiveRecord vào concerns
là một cách làm thường thấy ở các bạn DEV Ruby khi muốn làm gọn Fat model
hoặc sử dụng modules
để sau đó mix lại với một model.
Cách này không giải quyết vấn đề một cách triệt để vì hãy tưởng tượng việc làm này giống như bạn đang làm sạch một căn phòng bằng cách đổ rác vào những ngăn kéo riêng biệt và đóng chúng lại. Nhìn bề ngoài thì căn phòng trông có vẻ sạch sẽ nhưng những ngăn kéo rác sẽ thực sự khiến nó thật khó để xác định và implement những phân tách để làm rõ model.
Vì vậy, cách làm này thật sự không được khuyến khích.
1. Trích xuất ra Value Objects
a value object is a small object that represents a simple entity whose equality is not based on identity
Định nghia hơi khó hiểu, nhưng nôm na có thể hiểu: 2 object là equality is not based on identity
khi chúng có cùng giá trị nhưng chúng vân không phải là cùng một object
Ví dụ: trong Rails có các lib value object
như Date
, URI``,
Pathname`
date1 = Date.new 2017,9,26
date2 = Date.new 2017,9,26
date1 == date2 => true
date1 === date2 => false
Trong Rails, Value Objects
se được sử dụng khi bạn có một thuộc tính hoặc nhóm các thuộc tính có logic gắn liền với chúng.
has a Value Object named Rating that represents a simple A - F grade that each class or module receives
Ví dụ: chúng ta có một Value Object tên là Rating
đại diện cho cấp A - F, tương ứng với môi class hoặc module nhận được
class Rating
include Comparable
def self.from_cost(cost)
if cost <= 2
new("A")
elsif cost <= 4
new("B")
elsif cost <= 8
new("C")
elsif cost <= 16
new("D")
else
new("F")
end
end
def initialize(letter)
@letter = letter
end
def better_than?(other)
self > other
end
def <=>(other)
other.to_s <=> to_s
end
def hash
@letter.hash
end
def eql?(other)
to_s == other.to_s
end
def to_s
@letter.to_s
end
end
class ConstantSnapshot < ActiveRecord::Base
# …
def rating
@rating ||= Rating.from_cost(cost)
end
end
2. Trích xuất ra Service Objects
Mỗi một hành động trong hệ thống đảm bảo một Service Object được đóng gói thành hoạt động của chúng. Một Service Object cần đảm bảo các tiêu chí sau:
- Hành động phức tạp, ví dụ đóng sổ sách vào cuối kỳ kiểm toán
- Hành động cần thông qua nhiều model: việc mua hàng e-commerce cần sử dụng các đối tượng Order, CreditCard và Customer.
- Hành động tương tác với dịch vụ bên ngoài: post bài lên mạng xã hội
- Hành động không phải mối quan tâm chính của model: xóa những data đã quá hạn sau một khoảng thời gian dài.
- Có nhiều cách để thực hiện hành động: xác thực người dùng qua access token hoặc password.
class UserAuthenticator
def initialize(user)
@user = user
end
def authenticate(unencrypted_password)
return false unless @user
if BCrypt::Password.new(@user.password_digest) == unencrypted_password
@user
else
false
end
end
end
class SessionsController < ApplicationController
def create
user = User.where(email: params[:email]).first
if UserAuthenticator.new(user).authenticate(params[:password])
self.current_user = user
redirect_to dashboard_path
else
flash[:alert] = "Login failed."
render "new"
end
end
end
3. Trích xuất Form Objects
Khi nhiều model có thể được cập nhật qua một form, Form Objects
có thể tập hợp chúng lại.
Lưu ý: không nên sử dụng accepts_nested_attributes_for
class Signup
include Virtus # gem https://github.com/solnic/virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :company
attribute :name, String
attribute :company_name, String
attribute :email, String
validates :email, presence: true
# … more validations …
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
@company = Company.create!(name: company_name)
@user = @company.users.create!(name: name, email: email)
end
end
class SignupsController < ApplicationController
def create
@signup = Signup.new(params[:signup])
if @signup.save
redirect_to dashboard_path
else
render "new"
end
end
end
Cách này sẽ hiệu quả với những trường hợp đơn giản, nhưng nếu logic của việc persist
quá phức tạp, ta có thể kết hợp với Service Objects
. Ngoài ra ta có thể gặp trường hợp validation
theo ngữ cảnh vì vậy việc validation
có thể được định nghĩa ở một object khác thay vì validation
ngay trong bản thân model.
4. Trích xuất Query Objects
Với những truy vấn SQL phức tạp (có thể nằm trong scopes hoặc class method) chúng ta nên sử dụng Query Object
.
class AbandonedTrialQuery
def initialize(relation = Account.scoped)
@relation = relation
end
def find_each(&block)
@relation.
where(plan: nil, invites_count: 0).
find_each(&block)
end
end
AbandonedTrialQuery.new.find_each do |account|
account.send_offer_for_support
end
5. View Objects
Nếu logic cần dùng chỉ nhằm mục đích hiển thị và không thuộc về models thì ta có thể xem xét sử dụng helper hoặc tốt hơn là View Objects
.
class DonutChart
def initialize(snapshot)
@snapshot = snapshot
end
def cache_key
@snapshot.id.to_s
end
def data
# pull data from @snapshot and turn it into a JSON structure
end
end
6. Trích xuất Policy Objects
Thi thoảng các hoạt động đọc phức tạp ta có thể tách ra thành Policy Object
.
class ActiveUserPolicy
def initialize(user)
@user = user
end
def active?
@user.email_confirmed? &&
@user.last_login_at > 14.days.ago
end
end
Policy Object
khả giống với Service Object
ta cần phân biệt là Service Object
dùng cho hoạt động ghi, còn Policy Object
dùng cho việc đọc.
7. Trích xuất Decorators
Decorators
cho phép bạn tạo một lớp bên trên chức năng cho các hoạt động đã tồn tại, và do đó nó phục vụ cho mục đích tương tự callbacks
. Khi logic callbacks
chỉ cần gọi trong một vài tính huống hoặc bao gồm nó trong model tạo cho model quá nhiều nhiệm vụ thì ta nên dùng Decorator
.
class FacebookCommentNotifier
def initialize(comment)
@comment = comment
end
def save
@comment.save && post_to_wall
end
private
def post_to_wall
Facebook.post(title: @comment.title, user: @comment.author)
end
end
class CommentsController < ApplicationController
def create
@comment = FacebookCommentNotifier.new(Comment.new(params[:comment]))
if @comment.save
redirect_to blog_path, notice: "Your comment was posted."
else
render "new"
end
end
end
Decorators
khác với Service Object
vì chúng tạo lớp dựa trên interface có sẵn. Hơn nữa, trong trường hợp này, FacebookCommentNotifier
instance vẫn được coi là một Comment
.
Tham khảo
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
All rights reserved