Computer >> Máy Tính >  >> Lập trình >> Ruby

Các mẫu mô hình Ruby on Rails và các mẫu chống

Chào mừng bạn quay trở lại bài thứ hai trong loạt bài về Mẫu và Chống mẫu của Ruby on Rails. Chúng tôi cũng đã đề cập đến một số mô hình và mô hình chống nổi tiếng nhất trong thế giới Rails. Trong bài đăng trên blog này, chúng ta sẽ đi qua nhiều mẫu và mẫu chống mô hình Rails.

Nếu bạn đang gặp khó khăn với các mô hình, bài đăng trên blog này là dành cho bạn. Wewill nhanh chóng xem xét quá trình đưa người mẫu của bạn vào chế độ ăn kiêng và hoàn thành một số điều cần tránh khi viết bài di cư. Hãy bắt tay ngay vào.

Chất béo Mô hình thừa cân

Khi phát triển một ứng dụng Rails, cho dù đó là một trang web Rails hoàn chỉnh hay một API, mọi người có xu hướng lưu trữ hầu hết các logic trong mô hình. Trong bài đăng trên blog trước đây, chúng tôi đã có một ví dụ về Song Lớp học đã làm nhiều thứ. Giữ nhiều thứ trong mô hình sẽ phá vỡ Nguyên tắc Trách nhiệm Đơn lẻ (SRP).

Hãy xem xét.

class Song < ApplicationRecord
  belongs_to :album
  belongs_to :artist
  belongs_to :publisher
 
  has_one :text
  has_many :downloads
 
  validates :artist_id, presence: true
  validates :publisher_id, presence: true
 
  after_update :alert_artist_followers
  after_update :alert_publisher
 
  def alert_artist_followers
    return if unreleased?
 
    artist.followers.each { |follower| follower.notify(self) }
  end
 
  def alert_publisher
    PublisherMailer.song_email(publisher, self).deliver_now
  end
 
  def includes_profanities?
    text.scan_for_profanities.any?
  end
 
  def user_downloaded?(user)
    user.library.has_song?(self)
  end
 
  def find_published_from_artist_with_albums
    ...
  end
 
  def find_published_with_albums
    ...
  end
 
  def to_wav
    ...
  end
 
  def to_mp3
    ...
  end
 
  def to_flac
    ...
  end
end

Vấn đề với các mô hình như thế này là chúng trở thành bãi rác cho các logic khác nhau liên quan đến một bài hát. Các phương thức bắt đầu chồng chất khi chúng được thêm vào từ từ, từng phương thức một theo thời gian.

Tôi đã đề xuất tách mã bên trong mô hình thành các mô-đun nhỏ hơn, nhưng bằng cách đó, bạn chỉ cần di chuyển mã từ nơi này sang nơi khác.

Một số người thậm chí còn sử dụng các mối quan tâm của Rails và thấy rằng logic có thể được sử dụng lại trên các mô hình. Trước đây tôi đã viết về nó và một số người yêu thích nó, những người khác thì không. Dù sao, câu chuyện với các mối quan tâm cũng tương tự như các mô-đun. Bạn nên biết rằng bạn chỉ đang di chuyển mã đến amodule có thể được đưa vào bất cứ đâu.

Một giải pháp thay thế khác là tạo các lớp nhỏ và sau đó gọi chúng bất cứ khi nào cần. Ví dụ:chúng ta có thể trích xuất mã chuyển đổi bài hát thành một lớp riêng biệt.

class SongConverter
  attr_reader :song
 
  def initialize(song)
    @song = song
  end
 
  def to_wav
    ...
  end
 
  def to_mp3
    ...
  end
 
  def to_flac
    ...
  end
end
 
class Song
  ...
 
  def converter
    SongConverter.new(self)
  end
 
  ...
end

Bây giờ chúng ta có SongConverter có mục đích chuyển đổi bài hát sang định dạng khác. Nó có thể có các bài kiểm tra riêng và logic trong tương lai về việc chuyển đổi Và nếu chúng ta muốn chuyển đổi một bài hát sang MP3, chúng ta có thể làm như sau:

@song.converter.to_mp3

Đối với tôi, điều này có vẻ rõ ràng hơn một chút so với việc sử dụng một mô-đun hoặc một mối quan tâm. Có lẽ vì tôi thích sử dụng thành phần hơn là kế thừa. Tôi cho rằng nó trực quan và dễ đọc hơn. Tôi khuyên bạn nên xem xét cả hai trường hợp trước khi quyết định chọn cách nào. Hoặc bạn có thể chọn cả hai nếu bạn muốn, không ai ngăn cản bạn.

SQL Pasta Parmesan

Ai mà không yêu thích một số món mì ngon trong cuộc sống thực? Mặt khác, khi nó mở mã mì ống, hầu như không có ai là fan hâm mộ. Và vì những lý do chính đáng. Trong Railsmodels, bạn có thể nhanh chóng biến việc sử dụng Active Record của mình thành món mỳ Ý, xoay vòng trên toàn bộ codebase. Làm thế nào để bạn tránh điều này?

Có một vài ý tưởng trên mạng dường như khiến những câu hỏi dài đó biến thành những dòng mì Ý. Trước tiên, hãy xem cách mà mã liên quan đến cơ sở dữ liệu có thể ở khắp mọi nơi. Hãy quay lại Song của chúng tôi người mẫu. Cụ thể là khi ướt để lấy thứ gì đó từ nó.

class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.where(status: :published)
                .where(artist_id: artist_id)
                .order(:title)
 
    ...
  end
end
 
class SongController < ApplicationController
  def index
    @songs = Song.where(status: :published)
                 .order(:release_date)
 
    ...
  end
end
 
class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.where(status: :published)
 
    ...
  end
end

Trong ví dụ trên, chúng ta có ba trường hợp sử dụng mà Song mô hình đang được yêu cầu. Trong SongReporterService được sử dụng để báo cáo dữ liệu về các bài hát, chúng tôi cố gắng tải các bài hát đã xuất bản từ một nghệ sĩ cụ thể. Sau đó, trong SongController , chúng tôi nhận các bài hát đã xuất bản và đặt hàng trước ngày phát hành. Và cuối cùng, trong SongRefreshJob chúng tôi chỉ nhận được các bài hát đã xuất bản và một số thứ với chúng.

Tất cả đều ổn, nhưng điều gì sẽ xảy ra nếu chúng tôi đột ngột quyết định thay đổi tên trạng thái thành released hoặc thực hiện một số thay đổi khác đối với cách chúng tôi tìm nạp bài hát? Chúng tôi sẽ phải đi và chỉnh sửa tất cả các lần xuất hiện một cách riêng biệt. Ngoài ra, đoạn mã trên không phải là DRY. Nó tự hoạt động trên ứng dụng. Đừng để điều này làm bạn thất vọng. May mắn thay, có giải pháp cho vấn đề này.

Chúng tôi có thể sử dụng Phạm vi đường ray để KHÔ mã này ra. Phạm vi cho phép bạn xác định các truy vấn được sử dụng phổ biến, có thể được gọi trên các liên kết và đối tượng. Điều này làm cho mã của bạn có thể đọc được và dễ thay đổi hơn. Nhưng, có lẽ điều quan trọng nhất là kính hiển thị cho phép chúng ta xâu chuỗi các phương thức Bản ghi Hoạt động khác như joins , where ,vân vân. Hãy xem mã của chúng ta trông như thế nào với phạm vi.

class Song < ApplicationRecord
  ...
 
  scope :published, ->            { where(published: true) }
  scope :by_artist, ->(artist_id) { where(artist_id: artist_id) }
  scope :sorted_by_title,         { order(:title) }
  scope :sorted_by_release_date,  { order(:release_date) }
 
  ...
end
 
class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.published.by_artist(artist_id).sorted_by_title
 
    ...
  end
end
 
class SongController < ApplicationController
  def index
    @songs = Song.published.sorted_by_release_date
 
    ...
  end
end
 
class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.published
 
    ...
  end
end

Của bạn đây. Chúng tôi đã cố gắng cắt mã lặp lại và đưa nó vào mô hình. Nhưng điều này không phải lúc nào cũng diễn ra tốt nhất, đặc biệt nếu bạn được chẩn đoán là mô hình béo hoặc một Vật thể của Chúa. Thêm ngày càng nhiều phương pháp và trách nhiệm vào mô hình có thể không phải là ý tưởng tuyệt vời.

Lời khuyên của tôi ở đây là giữ mức sử dụng phạm vi ở mức tối thiểu và chỉ trích xuất các truy vấn phổ biến ở đó. Trong trường hợp của chúng tôi, có thể là where(published: true) sẽ có phạm vi hoàn hảo vì nó được sử dụng ở mọi nơi. Đối với mã liên quan đến SQL khác, bạn có thể sử dụng một cái gì đó được gọi là mẫu Kho lưu trữ. Hãy cùng tìm hiểu xem nó là gì.

Mẫu kho lưu trữ

Những gì chúng tôi sắp trình bày không phải là mô hình Kho lưu trữ 1:1 như được định nghĩa trong cuốn sách Thiết kế theo hướng chính mà ý tưởng đằng sau mô hình Kho lưu trữ của chúng tôi và Rails là tách logic cơ sở dữ liệu khỏi logic nghiệp vụ. Chúng tôi cũng có thể tiếp tục và tạo một kho lưu trữ để SQL thô gọi cho chúng tôi thay vì Active Record, nhưng tôi sẽ không khuyến khích những điều như vậy trừ khi bạn thực sự cần nó.

Những gì chúng ta có thể làm là tạo một SongRepository và đặt logic cơ sở dữ liệu vào đó.

class SongRepository
  class << self
    def find(id)
      Song.find(id)
    rescue ActiveRecord::RecordNotFound => e
      raise RecordNotFoundError, e
    end
 
    def destroy(id)
      find(id).destroy
    end
 
    def recently_published_by_artist(artist_id)
      Song.where(published: true)
          .where(artist_id: artist_id)
          .order(:release_date)
    end
  end
end
 
class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = SongRepository.recently_published_by_artist(artist_id)
 
    ...
  end
end
 
class SongController < ApplicationController
  def destroy
    ...
 
    SongRepository.destroy(params[:id])
 
    ...
  end
end

Những gì chúng tôi đã làm ở đây là chúng tôi tách logic truy vấn thành một lớp có thể kiểm tra được. Ngoài ra, mô hình không còn quan tâm đến phạm vi và logic nữa. Các mô hình controllerand mỏng, và mọi người đều hài lòng. Đúng? Chà, vẫn còn ActiveRecord đang thực hiện tất cả các thao tác kéo nặng ở đó. Trong trường hợp của chúng tôi, chúng tôi sử dụng find , tạo ra những điều sau:

SELECT "songs".* FROM "songs" WHERE "songs"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

Cách "đúng" là xác định tất cả những điều này bên trong SongRepository . Như tôi đã nói, tôi sẽ không khuyên bạn điều đó. Bạn không cần nó và bạn muốn có toàn quyền kiểm soát. Một trường hợp sử dụng để loại bỏ Active Record là bạn cần một số thủ thuật phức tạp bên trong SQL mà Active Record không dễ dàng hỗ trợ.

Nói về SQL thô và Active Record, tôi cũng phải đưa ra một chủ đề. Chủ đề về di cư và cách thực hiện chúng đúng cách. Hãy đi sâu vào.

Di cư - Ai quan tâm?

Tôi thường nghe thấy một cuộc tranh cãi khi viết chuyển đổi rằng mã ở đó không được tốt như trong phần còn lại của ứng dụng. Và lý lẽ đó không phù hợp với tôi. Mọi người có xu hướng sử dụng lý do này để thiết lập các mã có mùi hôi theo họ vì nó sẽ chỉ được chạy một lần và bị lãng quên. Điều này có thể đúng nếu bạn đang làm việc với một vài người và mọi người luôn đồng bộ hóa không nhất quán.

Thực tế thường khác. Ứng dụng có thể được sử dụng bởi một số lượng lớn hơn những người không biết điều gì xảy ra với các phần ứng dụng khác nhau. Và nếu bạn nhập một số mã một lần có vấn đề ở đó, bạn có thể phá vỡ môi trường phát triển của ai đó trong vài giờ vì trạng thái cơ sở dữ liệu bị hỏng hoặc chỉ là một sự di chuyển kỳ lạ. Không chắc đây có phải là một mô hình phản đối hay không, nhưng bạn nên biết về nó.

Làm thế nào để làm cho việc di chuyển thuận tiện hơn cho những người khác? Hãy xem qua một danh sách sẽ giúp mọi người trong dự án di chuyển dễ dàng hơn.

Đảm bảo rằng bạn luôn cung cấp phương pháp rút gọn

Bạn không bao giờ biết khi nào một cái gì đó sẽ được quay trở lại. Nếu quá trình di chuyển của bạn không thể hoàn nguyên, hãy đảm bảo tăng ActiveRecord::IrreversibleMigration ngoại lệ như vậy:

def down
  raise ActiveRecord::IrreversibleMigration
end

Cố gắng tránh ghi hiện hoạt khi di chuyển

Ý tưởng ở đây là giảm thiểu các phụ thuộc bên ngoài ngoại trừ trạng thái của cơ sở dữ liệu tại thời điểm quá trình di chuyển sẽ được thực thi. Vì vậy, sẽ không có xác thực Bản ghi Hoạt động nào làm hỏng (hoặc có thể tiết kiệm) ngày của bạn. Bạn đang sử dụng SQL thuần túy. Ví dụ:hãy viết một quá trình di chuyển sẽ xuất bản tất cả các bài hát từ một nghệ sĩ nhất định.

class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0]
  def up
    execute <<-SQL
      UPDATE songs
      SET published = true
      WHERE artist_id = 46
    SQL
  end
 
  def down
    execute <<-SQL
      UPDATE songs
      SET published = false
      WHERE artist_id = 46
    SQL
  end
end

Nếu bạn có nhu cầu lớn về Song mô hình, một gợi ý sẽ là xác định bên trong quá trình di chuyển. Bằng cách đó, bạn có thể bảo vệ quá trình di chuyển của mình khỏi bất kỳ thay đổi tiềm năng nào trong mô hình Bản ghi Hoạt động thực tế bên trong app/models .Nhưng, tất cả đều ổn và đẹp chứ? Hãy đi đến điểm tiếp theo của chúng ta.

Di chuyển lược đồ riêng biệt từ di chuyển dữ liệu

Xem qua Hướng dẫn Rails về di chuyển, bạn sẽ đọc những điều sau:

Di chuyển là một tính năng của Active Record cho phép bạn phát triển lược đồ cơ sở dữ liệu của mình tăng ca. Thay vì viết các sửa đổi lược đồ trong SQL thuần túy, việc di chuyển cho phép bạn sử dụng Ruby DSL để mô tả các thay đổi đối với bảng của bạn.

Trong phần tóm tắt của hướng dẫn, không có đề cập đến việc chỉnh sửa dữ liệu thực tế của bảng cơ sở dữ liệu, chỉ có cấu trúc. Vì vậy, việc chúng tôi sử dụng các bài hát cập nhật migrationto thường xuyên ở điểm thứ hai là không hoàn toàn đúng.

Nếu bạn cần thường xuyên làm điều gì đó tương tự trong dự án của mình, hãy xem xét sử dụng data_migrate đá quý. Đó là một cách tốt để tách việc di chuyển dữ liệu khỏi di chuyển lược đồ. Chúng ta có thể dễ dàng viết lại ví dụ trước của chúng ta bằng nó. Để tạo quá trình di chuyển dữ liệu, chúng ta có thể làm như sau:

bin/rails generate data_migration update_artists_songs_to_published

Và sau đó thêm logic di chuyển vào đó:

class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0]
  def up
    execute <<-SQL
      UPDATE songs
      SET published = true
      WHERE artist_id = 46
    SQL
  end
 
  def down
    execute <<-SQL
      UPDATE songs
      SET published = false
      WHERE artist_id = 46
    SQL
  end
end

Bằng cách này, bạn đang giữ tất cả các lần di chuyển giản đồ của mình bên trong db/migrate thư mục và tất cả các di chuyển liên quan đến dữ liệu bên trong db/data thư mục.

Lời kết

Đối phó với các mô hình và giữ cho chúng dễ đọc trong Rails là một cuộc đấu tranh liên tục. Rất may, trong bài đăng trên blog này, bạn có thể thấy những cạm bẫy có thể xảy ra và giải pháp cho các vấn đề thường gặp. Danh sách các mô hình và mô hình chống mô hình chưa đầy đủ trong bài đăng này, nhưng đây là những mô hình đáng chú ý nhất mà tôi tìm thấy gần đây.

Nếu bạn quan tâm đến các mẫu Rails và mẫu chống khác, hãy theo dõi phần tiếp theo của loạt bài này. Trong các bài viết sắp tới, chúng ta sẽ xem xét các vấn đề phổ biến và giải pháp cho phía chế độ xem và bộ điều khiển của Rails MVC.

Cho đến lần sau, chúc mừng!

Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!