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

Các mẫu bộ điều khiển Ruby on Rails và các mẫu chống

Chào mừng bạn trở lại với phần thứ tư của loạt bài viết về Mô hình đường ray và Chống mô hình.

Trước đây, chúng tôi đã đề cập đến các mẫu và phản mẫu nói chung cũng như sự liên quan đến các Mô hình và Chế độ xem Rails. Trong bài đăng này, chúng ta sẽ phân tích phần cuối cùng của mẫu thiết kế MVC (Model-View-Controller) - theController. Hãy đi sâu vào và xem xét các mẫu và phản mẫu liên quan đến Bộ điều khiển Rails.

At The Front Lines

Vì Ruby on Rails là một khung công tác web, các yêu cầu HTTP là một phần quan trọng của nó. Tất cả các loại Khách hàng tiếp cận với phần mềm phụ trợ của Rails viarequest và đây là nơi các bộ điều khiển tỏa sáng. Các bộ điều khiển ở tuyến đầu của việc tiếp nhận và xử lý các yêu cầu. Điều đó làm cho chúng trở thành một phần cơ bản của khuôn khổ Ruby on Rails. Tất nhiên, có mã đi trước bộ điều khiển, nhưng mã bộ điều khiển là thứ mà hầu hết chúng ta có thể kiểm soát.

Sau khi bạn xác định các tuyến tại config/routes.rb , bạn có thể nhấn máy chủ trên tuyến đường thiết lập và bộ điều khiển tương ứng sẽ lo phần còn lại. Đọc câu trước có thể cho ta ấn tượng rằng mọi thứ đều đơn giản như vậy. Tuy nhiên, thông thường, rất nhiều trọng lượng đổ lên vai người điều khiển, đó là mối quan tâm của xác thực và ủy quyền, sau đó là các vấn đề về cách tìm nạp dữ liệu cần thiết, cũng như vị trí và cách thực hiện logic kinh doanh.

Tất cả những mối quan tâm và trách nhiệm này có thể xảy ra bên trong bộ phận kiểm soát dẫn đến một số phản đối. Một trong những cái 'nổi tiếng' nhất là mô-típ của bộ điều khiển "béo".

Bộ điều khiển béo (béo phì)

Vấn đề với việc đặt quá nhiều logic vào bộ điều khiển là bạn đang bắt đầu vi phạm Nguyên tắc trách nhiệm duy nhất (SRP). Điều này có nghĩa là chúng ta đang thực hiện quá nhiều công việc bên trong bộ điều khiển. Thông thường, điều này dẫn đến rất nhiều mã và trách nhiệm chồng chất ở đó. Ở đây, 'fat' đề cập đến mã đắt tiền chứa trong các tệp bộ điều khiển, cũng như logic mà bộ điều khiển hỗ trợ. Nó thường được coi là một kiểu chống đối.

Có rất nhiều ý kiến ​​về những gì một kiểm soát viên nên làm. Nền tảng chung về các trách nhiệm mà kiểm soát viên phải có bao gồm những điều sau đây:

  • Xác thực và ủy quyền - kiểm tra xem thực thể (thường là người dùng) đằng sau yêu cầu có phải là thực thể mà nó nói hay không và liệu nó có được phép truy cập tài nguyên hoặc thực hiện hành động hay không. Thông thường, xác thực được lưu trong phiên hoặc cookie, nhưng bộ điều khiển vẫn nên kiểm tra xem dữ liệu xác thực có còn hợp lệ hay không.
  • Tìm nạp dữ liệu - nó sẽ gọi logic để tìm đúng dữ liệu dựa trên các tham số đi kèm với yêu cầu. Trong thế giới hoàn hảo, nó nên gọi một phương pháp thực hiện tất cả công việc. Bộ điều khiển không nên thực hiện công việc phức tạp, nó phải ủy quyền hơn nữa.
  • Kết xuất mẫu - cuối cùng, nó sẽ trả về phản hồi phù hợp bằng cách hiển thị kết quả với định dạng thích hợp (HTML, JSON, v.v.). Hoặc, nó phải chuyển hướng đến một số đường dẫn hoặc URL khác.

Làm theo những ý tưởng này có thể giúp bạn không phải thực hiện quá nhiều hành động của bộ điều khiển và bộ điều khiển nói chung. Giữ nó đơn giản ở cấp bộ điều khiển sẽ cho phép bạn ủy quyền công việc cho các khu vực khác của ứng dụng của bạn. Giao phó trách nhiệm và kiểm tra từng trách nhiệm một để đảm bảo rằng bạn đang phát triển ứng dụng của mình trở nên mạnh mẽ.

Chắc chắn, bạn có thể tuân theo các nguyên tắc trên, nhưng bạn phải háo hức với một số ví dụ. Hãy đi sâu vào và xem chúng ta có thể sử dụng những mẫu nào để giảm bớt trọng lượng cho bộ điều khiển.

Đối tượng truy vấn

Một trong những vấn đề xảy ra bên trong các hành động của bộ điều khiển là truy vấn quá nhiều dữ liệu. Nếu bạn đã theo dõi bài đăng trên blog của chúng tôi về các mẫu và mẫu chống lại Mô hình onRails, chúng ta đã gặp phải một vấn đề tương tự khi các mô hình có quá nhiều logic truy vấn. Đối tượng truy vấn là một kỹ thuật tách biệt các truy vấn phức tạp của bạn thành một đối tượng duy nhất.

Trong hầu hết các trường hợp, Đối tượng Truy vấn là Đối tượng Ruby Cũ thuần túy được khởi tạo với ActiveRecord quan hệ. Một Đối tượng Truy vấn điển hình có thể trông như thế này:

# app/queries/all_songs_query.rb
 
class AllSongsQuery
  def initialize(songs = Song.all)
    @songs = songs
  end
 
  def call(params, songs = Song.all)
    songs.where(published: true)
         .where(artist_id: params[:artist_id])
         .order(:title)
  end
end

Nó được tạo ra để sử dụng bên trong bộ điều khiển như vậy:

class SongsController < ApplicationController
  def index
    @songs = AllSongsQuery.new.call(all_songs_params)
  end
 
  private
 
  def all_songs_params
    params.slice(:artist_id)
  end
end

Bạn cũng có thể thử một cách tiếp cận khác của đối tượng truy vấn:

# app/queries/all_songs_query.rb
 
class AllSongsQuery
  attr_reader :songs
 
  def initialize(songs = Song.all)
    @songs = songs
  end
 
  def call(params = {})
    scope = published(songs)
    scope = by_artist_id(scope, params[:artist_id])
    scope = order_by_title(scope)
  end
 
  private
 
  def published(scope)
    scope.where(published: true)
  end
 
  def by_artist_id(scope, artist_id)
    artist_id ? scope.where(artist_id: artist_id) : scope
  end
 
  def order_by_title(scope)
    scope.order(:title)
  end
end

Cách tiếp cận thứ hai làm cho đối tượng truy vấn mạnh mẽ hơn bằng cách tạo params không bắt buộc. Ngoài ra, hãy lưu ý rằng bây giờ chúng ta có thể gọi AllSongsQuery.new.call Nếu bạn không phải là một fan hâm mộ lớn của điều này, bạn có thể sử dụng các phương thức lớp. Nếu bạn viết lớp truy vấn của mình bằng các phương thức của lớp, nó sẽ không còn là 'đối tượng' nữa, mà đây là vấn đề của sở thích cá nhân. Với mục đích minh họa, hãy xem cách chúng ta có thể tạo AllSongsQuery đơn giản hơn để gọi trong tự nhiên.

# app/queries/all_songs_query.rb
 
class AllSongsQuery
  class << self
    def call(params = {}, songs = Song.all)
      scope = published(songs)
      scope = by_artist_id(scope, params[:artist_id])
      scope = order_by_title(scope)
    end
 
    private
 
    def published(scope)
      scope.where(published: true)
    end
 
    def by_artist_id(scope, artist_id)
      artist_id ? scope.where(artist_id: artist_id) : scope
    end
 
    def order_by_title(scope)
      scope.order(:title)
    end
  end
end

Bây giờ, chúng ta có thể gọi AllSongsQuery.call và chúng tôi đã hoàn thành. Chúng tôi có thể chuyển params với artist_id . Ngoài ra, chúng ta có thể vượt qua phạm vi ban đầu nếu chúng ta cần thay đổi nó vì một lý do nào đó. Nếu bạn thực sự muốn tránh gọi new qua một lớp truy vấn, hãy thử 'thủ thuật' này:

# app/queries/application_query.rb
 
class ApplicationQuery
  def self.call(*params)
    new(*params).call
  end
end

Bạn có thể tạo ApplicationQuery và sau đó kế thừa từ nó trong các lớp truy vấn khác:

# app/queries/all_songs_query.rb
class AllSongsQuery < ApplicationQuery
  ...
end

Bạn vẫn giữ AllSongsQuery.call , nhưng bạn đã làm cho nó thanh lịch hơn.

Điều tuyệt vời về các đối tượng truy vấn là bạn có thể kiểm tra chúng một cách riêng biệt và đảm bảo rằng chúng đang làm những gì chúng nên làm. Hơn nữa, bạn có thể mở rộng các lớp truy vấn này và kiểm tra chúng mà không cần lo lắng quá nhiều về logic trong bộ điều khiển. Một điều cần lưu ý là bạn nên xử lý các tham số yêu cầu của mình ở bất kỳ đâu, và không dựa vào đối tượng truy vấn để làm như vậy. Bạn nghĩ sao, bạn có định dùng thử đối tượng truy vấn không?

Sẵn sàng phục vụ

OK, vậy là chúng tôi đã xử lý các cách để ủy quyền việc thu thập và tìm nạp dữ liệu vào QueryObjects. Chúng ta làm gì với logic tổng hợp giữa thu thập dữ liệu và bước chúng ta kết xuất nó? Tốt mà bạn đã yêu cầu, bởi vì một trong những giải pháp là sử dụng những gì được gọi là Dịch vụ. Một dịch vụ thường được coi là PORO (Plain Old Ruby Object) thực hiện một hành động (nghiệp vụ) duy nhất. Chúng ta sẽ tiếp tục và khám phá ý tưởng này một chút bên dưới.

Hãy tưởng tượng chúng ta có hai dịch vụ. Một người tạo biên nhận, người kia gửi biên nhận cho người dùng như sau:

# app/services/create_receipt_service.rb
class CreateReceiptService
  def self.call(total, user_id)
    Receipt.create!(total: total, user_id: user_id)
  end
end
 
# app/services/send_receipt_service.rb
class SendReceiptService
  def self.call(receipt)
    UserMailer.send_receipt(receipt).deliver_later
  end
end

Sau đó, trong bộ điều khiển của chúng tôi, chúng tôi sẽ gọi SendReceiptService như thế này:

# app/controllers/receipts_controller.rb
 
class ReceiptsController < ApplicationController
  def create
    receipt = CreateReceiptService.call(total: receipt_params[:total],
                                        user_id: receipt_params[:user_id])
 
    SendReceiptService.call(receipt)
  end
end

Bây giờ bạn có hai dịch vụ thực hiện tất cả công việc và bộ điều khiển chỉ cần gọi chúng. Bạn có thể kiểm tra những điều này một cách riêng biệt, nhưng vấn đề là, không có kết nối rõ ràng giữa các dịch vụ. Vâng, về lý thuyết, tất cả chúng đều thực hiện hành động kinh doanh đơn lẻ. Nhưng, nếu chúng ta xem xét mức độ trừu tượng từ quan điểm của các bên liên quan - quan điểm của họ về hành động tạo ra một biên nhận sẽ gửi một email về nó. Mức độ trừu tượng của ai là 'đúng' ™ ️?

Để làm cho thử nghiệm suy nghĩ này phức tạp hơn một chút, chúng ta hãy thêm một yêu cầu rằng tổng số tiền trên biên nhận phải được tính toán hoặc tìm nạp từ quá trình tạo biên lai. chúng ta làm gì sau đó? Viết một dịch vụ khác để xử lý tổng của tổng tổng? Câu trả lời có thể là tuân theo Nguyên tắc phản hồi đơn (SRP) và trừu tượng hóa những thứ khác xa nhau.

# app/services/create_receipt_service.rb
class CreateReceiptService
  ...
end
 
# app/services/send_receipt_service.rb
class SendReceiptService
  ...
end
 
# app/services/calculate_receipt_total_service.rb
class CalculateReceiptTotalService
  ...
end
 
# app/controllers/receipts_controller.rb
class ReceiptsController < ApplicationController
  def create
    total = CalculateReceiptTotalService.call(user_id: receipts_controller[:user_id])
 
    receipt = CreateReceiptService.call(total: total,
                                        user_id: receipt_params[:user_id])
 
    SendReceiptService.call(receipt)
  end
end

Bằng cách tuân theo SRP, chúng tôi đảm bảo rằng các dịch vụ của chúng tôi có thể được tạo thành các phần tóm tắt intolarger với nhau, như ReceiptCreation quá trình. Bằng cách tạo lớp 'quy trình' này, chúng tôi có thể nhóm tất cả các hành động cần thiết để hoàn thành quy trình. Bạn nghĩ thế nào về ý tưởng này? Thoạt nghe có vẻ như quá trừu tượng, nhưng nó có thể mang lại lợi ích nếu bạn đang gọi những hành động này ở khắp nơi.

Tóm lại, CalculateReceiptTotalService mới dịch vụ có thể đối phó với tất cả các tiếng giòn sau đó. CreateReceiptService của chúng tôi chịu trách nhiệm ghi biên nhận vào cơ sở dữ liệu. SendReceiptService có để gửi cho người dùng emailsto về biên nhận của họ. Việc có các lớp nhỏ và tập trung này có thể kết hợp chúng trong các trường hợp sử dụng khác dễ dàng hơn, do đó dẫn đến việc kiểm tra codebase dễ dàng hơn và dễ dàng hơn.

Cơ sở dịch vụ

Trong thế giới Ruby, cách tiếp cận sử dụng các lớp dịch vụ còn được gọi là các hành động, hoạt động và tương tự. Ý tưởng đằng sau mẫu Lệnh là một đối tượng (hoặc trong ví dụ của chúng tôi là aclass) đang đóng gói tất cả thông tin cần thiết để thực hiện một hành động kinh doanh hoặc kích hoạt một sự kiện. Thông tin mà người gọi lệnh nên biết là:

  • tên của lệnh
  • tên phương thức để gọi trên đối tượng / lớp lệnh
  • giá trị được chuyển cho các tham số phương thức

Vì vậy, trong trường hợp của chúng ta, người gọi lệnh là một bộ điều khiển. Điều này gần giống nhau, chỉ là tên trong Ruby là 'Dịch vụ'.

Chia nhỏ công việc

Nếu bộ điều khiển của bạn đang gọi một số dịch vụ của bên thứ ba và chúng đang chặn kết xuất của bạn, có lẽ đã đến lúc trích xuất các cuộc gọi này và hiển thị riêng biệt với một hành động điều khiển khác. Một ví dụ về điều này có thể là khi bạn cố gắng hiển thị thông tin của một cuốn sách và lấy xếp hạng của nó từ một số dịch vụ khác mà bạn không thể thực sự ảnh hưởng (như Goodreads).

# app/controllers/books_controller.rb
 
class BooksController < ApplicationController
  def show
    @book = Book.find(params[:id])
 
    @rating = GoodreadsRatingService.new(book).call
  end
end

Nếu Goodreads bị lỗi hoặc điều gì đó tương tự, người dùng của bạn sẽ phải đợi yêu cầu đến máy chủ Goodreads để hết thời gian. Hoặc, nếu có điều gì đó chậm trên máy chủ của họ, trang sẽ tải chậm. Bạn có thể trích xuất lệnh gọi của dịch vụ bên thứ ba để thực hiện một hành động khác như sau:

# app/controllers/books_controller.rb
 
class BooksController < ApplicationController
  ...
 
  def show
    @book = Book.find(params[:id])
  end
 
  def rating
    @rating = GoodreadsRatingService.new(@book).call
 
    render partial: 'book_rating'
  end
 
  ...
end

Sau đó, bạn sẽ phải gọi rating đường dẫn từ chế độ xem của bạn, nhưng này, chương trình của bạn không có trình chặn nữa. Ngoài ra, bạn cần phần 'book_rating'. Để thực hiện việc này dễ dàng hơn, bạn có thể sử dụng gem render_async. Bạn chỉ cần đặt câu lệnh sau vào nơi hiển thị xếp hạng sách của mình:

<%= render_async book_rating_path %>

Trích xuất HTML để hiển thị xếp hạng vào book_rating một phần và đặt:

<%= content_for :render_async %>

Bên trong tệp bố cục của bạn, đá quý sẽ gọi book_rating_path với AJAXrequest khi trang của bạn tải và khi xếp hạng được tìm nạp, nó sẽ hiển thị nó trên trang. Một lợi ích lớn trong việc này là người dùng của bạn có thể xem trang sách nhanh hơn bằng cách tải các xếp hạng riêng biệt.

Hoặc, nếu muốn, bạn có thể sử dụng Turbo Frames từ Basecamp, ý tưởng cũng tương tự nhưng bạn chỉ sử dụng <turbo-frame> phần tử trong đánh dấu của bạn như vậy:

<turbo-frame id="rating_1" src="/books/1/rating"> </turbo-frame>

Dù bạn chọn tùy chọn nào, ý tưởng là tách công việc nặng nhọc hoặc dễ hỏng khỏi hành động của bộ điều khiển chính của bạn và hiển thị trang cho người dùng càng sớm càng tốt.

Lời kết

Nếu bạn thích ý tưởng giữ cho các bộ điều khiển mỏng và hình dung chúng chỉ là 'bộ điều khiển' của các phương pháp khác, thì tôi tin rằng bài đăng này đã mang lại một số thông tin chi tiết về cách giữ chúng theo cách đó. Tất nhiên, một vài mẫu và phản mẫu mà chúng tôi đã đề cập ở đây không phải là một danh sách đầy đủ. Nếu bạn có ý tưởng về những gì tốt hơn hoặc những gì bạn thích, vui lòng liên hệ trên Twitter và chúng ta có thể thảo luận.

Chắc chắn hãy theo dõi loạt bài này, chúng tôi sẽ thực hiện ít nhất một bài đăng trên blog khác, nơi chúng tôi tổng hợp các sự cố Rails phổ biến và những điều rút ra từ loạt bài này.

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!