0

7 Patterns to Refactor Fat ActiveRecord Models

  1. 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
  1. 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
  1. 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
  1. 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)
  1. 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
  1. 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
  1. 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí