7 cách để phòng tránh fat activerecord model
"Fat models" gây ra rất nhiều vấn đề cho việc bảo trì các app lớn. Chúng phản ánh sai lầm lớn khi áp dụng nguyên tắc Single Responsibility Principle (SRP). Khi có nhiều hơn 1 mục đích mà 1 user cần làm , thì khi đó nó không còn thoả mãn single responsibility Ban đầu, SRP dễ dàng được áp dụng. Các ...
"Fat models" gây ra rất nhiều vấn đề cho việc bảo trì các app lớn. Chúng phản ánh sai lầm lớn khi áp dụng nguyên tắc Single Responsibility Principle (SRP). Khi có nhiều hơn 1 mục đích mà 1 user cần làm , thì khi đó nó không còn thoả mãn single responsibility
Ban đầu, SRP dễ dàng được áp dụng. Các lớp ActiveRecord lưu giữ các quan hệ, sự chặt chẽ và không nhiều hơn thế. Nhưng dần dần, model lớn hơn. Các đối tượng chịu trách nhiệm cho sự chặt chẽ trở thành nơi tiếp nhận tất cả các logic. Và vài năm sau, bạn sẽ có một lớp User với hơn 500 dòng code, và hàng trăm method được public.
Và khi đấy, ta sẽ tự hỏi: "Thật khó để áp dụng OOP đúng cách trong Rails"
Ban đầu, tôi thường tin như vậy. Nhưng sau một vài tìm kiếm, và thực hành, tôi nhận thấy Rails không cản trở việc áp dụng OOP. Mà chỉ do convention của Rails thiếu sót trong việc quản lý độ phức tạp dựa vào các khả năng của Active Record cung cấp. Thật may mắn, chúng ta có thể áp dụng các nguyên tắc 00P và các thực hành trong khi Rails bị thiếu.
Đừng lấy mixin từ Fat model Tôi không khuyến khích việc đưa các method ra ngoài lớp ActiveRecord lớn vào trong concerns hay module , và sau đó trộn chúng vào một model.
Tốt hơn là dùng tổ hợp / composition so với kế thừa / inheritance.
Và sau đây là một vài cách refactor
1. Tạo ra các value object
Value Object là các đối tượng đơn giản. Chúng luôn luôn bất biến. VD như Date, URI hay Pathname là các ví dụ của thư viện Ruby, nhưng ta cũng có thể tự định nghĩa ra các Value Object như vậy.
Trong Rails, Value Objects rất tuyệt khi bạn có một thuộc tính hoặc một nhóm các thuộc tính có logic gắn liền với chúng.
Ví dụ, một ứng dụng nhắn tin văn bản tôi đã làm việc đã có một PhoneNumber Value Object. Một ứng dụng thương mại điện tử cần một lớp Money. Hoắc một Value Object là Rating đại diện cho một lớp điểm A-F đơn giản mà mỗi lớp hoặc mô-đun nhận được. Tôi có thể (và ban đầu đã làm) sử dụng một thể hiện của một chuỗi Ruby, nhưng Rating cho phép tôi kết hợp hành vi với dữ liệu:
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
khi đấy , ta có thể sử dụng giao diện public của Rating
class ConstantSnapshot < ActiveRecord::Base # … def rating @rating ||= Rating.from_cost(cost) end end
Ta có một vài thuận lợi khi sử dụng theo cách này:
Hàm #worse_than? và #better_than? cung cấp một cách diễn đạt rõ ràng hơn để so sánh Rating so với các toán tử tích hợp của Ruby (ví dụ: < và >).
Định nghĩa #hash và #eql? làm cho nó có thể sử dụng một Rating như là một khóa băm. Và ta có thể sử dụng cách này để nhóm các Rating bằng cách Enumberable # group_by.
Hàm #to_s cho phép tôi thêm Rating vào 1 chuỗi mà ko cần phải làm gì thêm.
Định nghĩa lớp cung cấp một vị trí phú hợp cho hàm factory method để định nghĩa Rating cho phù hợp dựa vào “remediation cost”
2. Tạo ra các Service Objects
Khi có một vài hành động trong một hệ thống bảo đảm, một Service Object sẽ đóng gói hoạt động của chúng. Tôi sử dụng Service Object trong các trường hợp sau:
-
Hành động rất phức tạp (ví dụ: đóng sách vào cuối kỳ kế toán)
-
Tác vụ đạt được phải thông qua nhiều model (ví dụ như mua hàng qua thương mại điện tử bằng cách 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 (ví dụ như đăng lên mạng xã hội)
-
Hành động không phải là mối quan tâm cốt lõi của một model cơ bản (ví dụ như xoá dữ liệu đã lỗi thời sau một khoảng thời gian nhất định).
-
Có nhiều cách để thực hiện hành động (ví dụ: xác thực bằng mã truy cập hoặc mật khẩu truy cập). Đây là mẫu Gang of Four Strategy.
Ta có VD sau:
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
và trong SessionsController ta sẽ sử dụng như sau:
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
Tạo các Form Objects
Khi nhiều model ActiveRecord có thể được cập nhật bằng cách gửi một form, một Form Object có thể gói gọn tập hợp đó. Điều này còn gọn hơn việc sử dụng accepts_nested_attributes_for, theo quan điểm của tôi, accepts_nested_attributes_for nên bị deprecated, bởi vì sẽ làm cho code rời rạc, khó theo dõi . Ví dụ là một form signup sẽ tạo ra cả Company và User:
class Signup include 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
Điều này hoạt động tốt cho các trường hợp đơn giản như trên, nhưng nếu logic trong form quá phức tạp, bạn có thể kết hợp cách tiếp cận này với một Service Object. Và lợi ích của việc này là, vì logic validation thường định nghĩa theo bối cảnh, nên nó có thể được định nghĩa ở chính xác nơi nó có vấn đề thay vì phải định nghĩa validation trong chính ActiveRecord
4. Tạo ra các Query Objects
Đối với các truy vấn SQL phức tạp, ta thường định nghĩa các ActiveRecord subclass (hoặc là các scope hay các class methods), tuy nhiên, ta có thể xem xét cách tiếp cận mới là Query objects. Mỗi Query object có trách nhiệm trả lại một bộ kết quả dựa trên các quy tắc logic. Ví dụ: Query Object để tìm các thử nghiệm bị bỏ rơi có thể như sau:
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
Khi đó , ta có thể sử dụng:
AbandonedTrialQuery.new.find_each do |account| account.send_offer_for_support end
Nhờ có ActiveRecord::Relation, ta có thể kết hợp nhiều query
old_accounts = Account.where("created_at < ?", 1.month.ago) old_abandoned_trials = AbandonedTrialQuery.new(old_accounts)
5. Sử dụng View Objects
Nếu logic được yêu cầu cho mục đích hiển thị, nó không thuộc về model. Hãy tự hỏi mình "Nếu tôi đang triển khai một giao diện thay thế cho ứng dụng này, như giao diện người dùng bằng giọng nói, tôi có cần điều này không?". Nếu không, hãy xem xét đặt nó trong một helper hoặc (thường là tốt hơn) một View object.
Ví dụ:
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. Tạo ra Policy Object
Policy Object cho phép bạn giữ logic tiếp tuyến, user nào được phép tiếp cần các mục đích phân tích, hay các hàm cốt lõi của đối tượng Ví dụ:
class ActiveUserPolicy def initialize(user) @user = user end def active? @user.email_confirmed? && @user.last_login_at > 14.days.ago end end
Policy Object đóng gói một quy tắc nghiệp vụ, người dùng được coi là active nếu họ có địa chỉ email đã được xác nhận và đã đăng nhập trong vòng hai tuần vừa qua. Bạn cũng có thể sử dụng Policy Object cho một tập quy tắc kinh doanh như Authorizer, điều chỉnh dữ liệu người dùng có thể truy cập.
Policy Objects tương tự như các Service Objects, nhưng tôi sử dụng thuật ngữ "Service Object" để viết các hoạt động và "Policy Object" để đọc. Chúng cũng tương tự như Query Objects, nhưng Query Objects tập trung vào việc thực hiện SQL để trả về một tập hợp kết quả, trong khi Objects Policy hoạt động trên các models đã được nạp vào bộ nhớ.
7. Tạo ra decorator
Decorators cho phép bạn tạo tầng các chức năng dựa trên các hoạt động hiện tại, và do đó phục vụ cho mục đích tương tự như callback. Đối với trường hợp Logic callback chỉ cần chạy trong một số trường hợp hoặc trong model có quá nhiều trách nhiệm, một Decorator rất hữu ích.
Đăng một nhận xét về một bài viết trên blog có thể kích hoạt một bài đăng lên tường Facebook của một ai đó, nhưng điều đó không có nghĩa là logic nên được viết vào lớp Comment. Một dấu hiệu bạn đã thêm quá nhiều trách nhiệm trong callback là viết test quá lâu hay thêm các trường hợp không liên quan
VD:
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
Và controller có thể sử dụng chúng:
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
Decorator khác Service Object vì nó liên quan nhiều hơn đến tầng interface. Khi cần thêm logic trong việc hiển thị ra view, thì decorator là 1 giải pháp hữu ích nhất.
Trong Rails, ta có thể sử dụng gem draper để áp dụng decorator.