Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Code Rails đặt đâu? Tổ chức logic non-CRUD

Sau khi xong tutorial Rails và bắt đầu app riêng, mọi thứ trở nên rối. Ví dụ logic non-CRUD, logic chung để ở đâu? Việc lấy follower từ Twitter hòa vào MVC như thế nào? Hỏi hai người, được bốn câu trả lời. Hoặc thread biến thành chửi bới nhau hàng giờ, trong khi bạn đập đầu vào bàn. Cách nào, bạn đau đầu.

Bạn không thể xây app mơ ước nếu không có một chút logic chung, non-Rails. Vậy đặt code ở đâu, vẫn giữ đơn giản?

Nơi dễ bắt đầu

Khi logic cảm thấy liên quan đến ActiveRecord model hiện có, tôi bắt đầu đặt nó vào model đó. Ví dụ, có model Game và muốn import game từ CSV, tôi đặt method đó ngay vào class Game:

app/models/game.rb
class Game < ActiveRecord::Base
  def self.parse_from_csv(csv_string)
    games = []
    CSV.parse(csv_string, quote_char: "'") do |row|
      games << Game.from_csv_row(row) if (row[0] == 'G')
    end
    games
  end
  def self.from_csv_row(row)
    Game.new({
      dgs_game_id: row[1],
      opponent_name: row[2],
      created_at: row[4],
      updated_at: row[4],
    })
  end
end

Bạn có tất cả thông tin method cần ngay tại chỗ. Dễ test. Và có lẽ là nơi contributor mới tìm logic đó đầu tiên.

Nhưng nếu tiếp tục thêm và sửa model đó, nó sẽ to và phức tạp. Các phần khác nhau của model sẽ tương tác cách lạ. Càng sửa nhiều, càng khó sửa.

Lúc đó, bạn muốn refactor code đó ra non-ActiveRecord model.

Non-ActiveRecord models

Chỉ vì ở Rails app, không có nghĩa là phải kế thừa Active/Action-whatever.

Bạn có thể viết code Ruby thuần, trong plain Ruby object, và dùng trong Rails app. Những object này vẫn gọi là models, vì chúng model một phần vấn đề. Chỉ không có database ActiveRecord lưu data.

Lần sau làm game CSV parser, class Game hơi to. Tôi chuyển logic parser vào class GameCSVParser riêng.

Toàn bộ commit ở đây, nhưng non-ActiveRecord class trông như sau:

app/models/game_csv_parser.rb
class GameCSVParser
  def initialize(csv_string)
    @rows = CSV.parse(csv_string, quote_char: "'")
  end
  def games
    game_rows.map { |row| Game.new(game_attributes(row)) }
  end
  private
  def game_rows
    @rows.select { |row| is_game_row?(row) }
  end
  def game_attributes(row)
    {
      dgs_game_id: row[1],
      opponent_name: row[2],
      created_at: row[4],
      updated_at: row[4],
    }
  end
  def is_game_row?(row)
    row[0] == 'G'
  end
end

Tôi tạo plain Ruby object ngay nếu logic thêm không liên quan đến ActiveRecord model cụ thể. Hoặc code giống như nên là phần của thứ chưa tồn tại trong app. Ngược lại, chúng thường nảy ra qua refactoring.

Với plain Ruby object, bạn viết gì cũng được. Nhưng biết viết bất cứ gì không giúp định hướng. Method nào cần? Object mới tương tác như thế nào?

Nhiều Rails app dùng chung các loại plain Ruby object. Những category này là pattern bạn có thể theo để viết code dev khác nhận ra. Bạn có thể đã nghe vài cái.

Service objects, presenters, và jobs

Không có gì đặc biệt về service objects, presenters, jobs. Chúng chỉ là plain Ruby object hành động theo cách nhận diện được.

Ví dụ, Resque job là plain Ruby class có method perform và @queue:

app/workers/fetch_games_for_player.rb
class FetchGamesForPlayer
  @queue = :default
  def self.perform(player_id)
    player = Player.scoped_by_id(player_id).ready_for_fetching.first
    player && player.fetch_new_games!
  end
end

perform được gọi khi job chạy.

Presenter là plain Ruby object với code chỉ có ý nghĩa trong view:

app/presenters/user_presenter.rb
class UserPresenter
  def show_related_users?
    @user.related.count > 3
  end
end

Nó cũng có thể include Rails view helpers, hoặc gom vài object khác nhau thành một object thống nhất cho view tiện lợi.

Service object là plain Ruby object đại diện cho quy trình bạn muốn thực hiện. Ví dụ, viết comment trên bài viết có thể:

  1. Để lại comment.
  2. Gửi mail thông báo cho tác giả bài viết.

Service object làm cả hai, giữ logic đó ra khỏi controller.

Có bài viết hay về service objects ở đây. Đầy ví dụ.

Với quy trình đơn giản, tôi không dùng service object. Nhưng nếu controller bắt đầu quá nặng, chúng là nơi tốt để bỏ logic thêm.

Bạn có thể dùng pattern này tổ chức business logic riêng. Chúng chỉ là plain Ruby object, nhưng là object có "hương vị" nhất định, có tên, và bạn có thể nói với dev khác về chúng.

Bắt đầu từ đâu?

Nhiều nơi logic non-Rails có thể đi. Khó chọn. Vậy tôi làm thế này:

  1. Nếu logic chủ yếu liên quan đến class hiện có, kể cả ActiveRecord model, tôi đặt vào class đó.
  2. Nếu không vừa class hiện có, tôi tạo plain Ruby class mới chứa logic.
  3. Nếu cảm thấy logic nên là phần của thứ chưa tồn tại, tôi tạo plain Ruby class mới cho nó.
  4. Nếu quay lại code sau, model quá phức tạp, hoặc code không còn hợp lý trong model, tôi refactor ra plain Ruby class riêng.
  5. Nếu code chỉ có ý nghĩa trong view, tôi thêm vào helper hoặc tạo presenter.
  6. Nếu code không cần chạy trong HTTP request, hoặc phải chạy background, nó vào job.
  7. Nếu tôi lúng túng vài model hoặc stage của quy trình khác nhau, làm controller khó hiểu, tôi đặt vào service object.

Còn bạn? Code bạn đặt đâu? Có pattern khác hữu ích không? Comment cho tôi biết.

Nếu chưa có quy trình, thử quy trình của tôi. Xem nó vừa bạn như thế nào. Không có cách viết code hoàn hảo, nhưng khi bị kẹt, quy trình như này giúp bạn bắt đầu.