7 Patterns to Refactor Fat ActiveRecord Models
Bài đăng này đã không được cập nhật trong 8 năm
- Extract Value Objects
Value Objects là các đối tượng đơn giản mà bình đẳng phụ thuộc vào giá trị của họ chứ không phải là một bản sắc. Họ thường không thay đổi. Date, URI, và Pathname là những ví dụ từ thư viện chuẩn của Ruby, nhưng ứng dụng của bạn có thể (và gần như chắc chắn) nên xác định Value Objects tên miền cụ thể là tốt. Trích xuất chúng từ ActiveRecords là treo thấp quả tái cấu trúc.
Trong Rails, Value Objects là tuyệt vời khi bạn có một thuộc tính hoặc một nhóm nhỏ các thuộc tính mà có logic liên kết với chúng. Bất cứ điều gì nhiều hơn các lĩnh vực văn bản cơ bản và quầy là ứng cử viên cho khai thác Value Object.
Ví dụ, một ứng dụng nhắn tin văn bản tôi đã làm việc trên đã có một PhoneNumber Value Object. Một ứng dụng thương mại điện tử cần một Money class. Mã khí hậu có Value Object named Rating đại diện cho một A đơn giản - F lớp mà mỗi lớp hoặc module nhận. Tôi có thể (và ban đầu đã làm) sử dụng một thể hiện của một Ruby String, nhưng điểm số cho phép tôi kết hợp hành động 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
Every ConstantSnapshot then exposes an instance of Rating in its public interface:
class ConstantSnapshot < ActiveRecord::Base
# …
def rating
@rating ||= Rating.from_cost(cost)
end
end
- Extract Service Objects
Một số hành động trong một hệ thống đảm bảo một đối tượng dịch vụ để đóng gói hoạt động của họ. Tôi với đối tượng dịch vụ khi một hành động đáp ứng một hoặc nhiều các tiêu chí:
Hành vi được phức tạp (ví dụ khoá sổ vào cuối kỳ kế toán) Các hoạt động đạt trên nhiều mô hình (ví dụ một mua thương mại điện tử sử dụng theo thứ tự, CreditCard và khách hàng đối tượng) Các hành động tương tác với dịch vụ bên ngoài (ví dụ như gửi bài đến các mạng xã hội) Các hành động không phải là một mối quan tâm cốt lõi của mô hình cơ bản (ví dụ quét 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 các hành động (ví dụ chứng thực với một truy cập token hoặc mật khẩu). Đây là Gang của mô hình Bốn chiến lược. Như một ví dụ, chúng ta có thể kéo một User#authenticate method ra thành một UserAuthenticator:
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à SessionsController sẽ trông như thế này:
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
- Extract Form Objects
Khi có nhiều mô hình ActiveRecord có thể được cập nhật bởi một hình thức trình đơn, một đối tượng Form có thể đóng gói các tập hợp. Điều này là xa sạch hơn so với sử dụng accepts_nested_attributes_for, trong đó, theo ý kiến của tôi khiêm tốn, nên bị phản đối. Một ví dụ phổ biến sẽ là một mẫu đăng ký mà kết quả trong việc tạo ra cả một công ty và một thành viên:
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
Tôi đã bắt đầu sử dụng Virtus trong các đối tượng này để có được chức năng thuộc tính ActiveRecord giống. Mẫu Object quacks như một ActiveRecord, vì vậy bộ điều khiển vẫn quen thuộc:
class SignupsController < ApplicationController
def create
@signup = Signup.new(params[:signup])
if @signup.save
redirect_to dashboard_path
else
render "new"
end
end
end
- Extract Query Objects
Đối với các truy vấn SQL phức tạp xả rác định nghĩa của lớp con ActiveRecord của bạn (như phạm vi hoặc các phương pháp lớp học), hãy xem xét giải nén đối tượng truy vấn. Mỗi đối tượng truy vấn có trách nhiệm trả lại một tập hợp kết quả dựa trên các quy tắc kinh doanh. Ví dụ, một truy vấn đối tượng để tìm thử nghiệm bị bỏ rơi có thể trông như thế này:
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
Bạn có thể sử dụng nó trong một công việc nền để gửi email:
AbandonedTrialQuery.new.find_each do |account|
account.send_offer_for_support
end
Kể từ trường hợp ActiveRecord :: Relation instances là first class của Rails 3, họ làm cho một đầu vào tuyệt vời để một đối tượng truy vấn. Điều này cho phép bạn kết hợp sử dụng các truy vấn thành phần:
old_accounts = Account.where("created_at < ?", 1.month.ago)
old_abandoned_trials = AbandonedTrialQuery.new(old_accounts)
- Introduce View Objects Nếu logic là cần thiết hoàn toàn cho mục đích hiển thị, nó không thuộc trong mô hình. Hãy tự hỏi mình, "Nếu tôi đã thực hiện một giao diện thay thế cho ứng dụng này, giống như một giao diện người dùng bằng giọng nói, tôi sẽ cần điều này?". Nếu không, bạn nên đặt nó trong một helper hoặc (thường tốt hơn) một đối tượng xem.
Ví dụ, các bảng xếp hạng donut trong luật khí hậu phá vỡ xếp hạng lớp dựa trên một bản chụp của codebase (ví dụ Rails trên luật khí hậu) và được đóng gói như một
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
- Extract Policy Objects i khi các hoạt động đọc phức tạp có thể xứng đáng đối tượng của riêng họ. Trong những trường hợp này tôi tiếp cận với một đối tượng chính sách. Điều này cho phép bạn để giữ logic tiếp tuyến, như mà người dùng được xem là hoạt động cho mục đích phân tích, trong số các đối tượng miền cốt lõi của bạn. Ví dụ:
class ActiveUserPolicy
def initialize(user)
@user = user
end
def active?
@user.email_confirmed? &&
@user.last_login_at > 14.days.ago
end
end
- Extract Decorators Trang trí cho phép bạn lớp trên chức năng để hoạt động hiện tại, và do đó phục vụ một mục đích tương tự như callbacks. Đối với trường hợp gọi lại lý chỉ cần chạy trong một số trường hợp hoặc bao gồm nó trong mô hình sẽ cung cấp cho các mô hình quá nhiều trách nhiệm, một nhà trang trí rất hữu ích.
Đăng một bình luận trên một bài đăng blog có thể kích hoạt một bài viết trên tường Facebook của một ai đó, nhưng điều đó không có nghĩa là logic nên khó khăn dây vào các Comment class. Một dấu hiệu bạn đã thêm quá nhiều trách nhiệm trong callbacks là kiểm tra chậm và dễ vỡ hoặc một sự thôi thúc để còn sơ khai ra tác dụng phụ trong trường hợp thử nghiệm hoàn toàn không liên quan.
Đây là cách bạn có thể trích xuất gửi bài luận Facebook vào một 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
And how a controller might use it:
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
All rights reserved