Tìm hiểu một vài cách Refractor trong Controller
Tìm hiểu một vài cách Refractor trong Controller I. Giới thiệu Tiếp nối phần trước kỹ thuật Refractor model trong sách Rails Antipatterns, trong phần này chúng ta sẽ tập trung tìm hiểu một vài kỹ thuật Refractor được áp dụng trong Controller. Controller là 1 trong 3 thành phần cơ bản của ...
Tìm hiểu một vài cách Refractor trong Controller
I. Giới thiệu
Tiếp nối phần trước kỹ thuật Refractor model trong sách Rails Antipatterns, trong phần này chúng ta sẽ tập trung tìm hiểu một vài kỹ thuật Refractor được áp dụng trong Controller.
Controller là 1 trong 3 thành phần cơ bản của Rails. Tuy nhiên, trong nhiều ứng dụng Rails, controller là thành phần xử lý phức tạp nhất, và ngày càng trở nên phình to, rất khó trong việc tối ưu code và bảo trì sau này.
Cuốn sách Rails Antipatterns sẽ đề cập đến một vài cạm bẫy mà chúng ta hay gặp phải trong các controllers.
II. Các Antipatterns
1. Antipattern: Fat Controller
Fat Controller là một trong những vấn đề phổ biến nhất trong cộng đồng Rails cũng như là vấn đề hay gặp nhất ảnh hưởng đến nhiều ứng dụng Rails
Fat Controller bao gồm các xử lý logic mà đáng lẽ nên nằm trong model. Thêm vào đó, nếu chuyển các xử lý đấy vào model thì ta có thể dễ dàng thực hiện unit test, điều đấy sẽ tuyệt hơn là các function test cho controller. Và từ đấy dẫn tới một khái niệm mới skinny controller.
Các giải pháp để refractor controller, chuyển xử lý logic vào model đã được hỗ trợ bời Active Record ví dụ như callbacks, setters, database default, hay là các pattern khác như Present Pattern.
Sử dụng Active Record Callbacks và Setters
Ví dụ 1 đoạn code trong controller không được xử lý tốt
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version! params[:version], current_user end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid render :action => :index and return false end redirect_to article_path(@article) end
với hàm create_version được định nghĩa:
def create_version!(attributes, user) if self.versions.empty? return create_first_version!(attributes, user) end if self.current_version.relateds.any? self.current_version.relateds.each do |rel| rel.update_attribute :current, false end end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.current_verison.version + 1 self.save! self.update_attribute :current_version_id, version.id version end
create_first_version
def create_first_version!(attributes, user) version = self.versions.build attributes version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute :current_version_id, version.id version end
Trên đây là đoạn code để tạo object và lưu vào trong database. Tuy nhiên, nó được thiết kế quá tồi, vi phạm vào các quy ước MVC, lạm dụng quá nhiều kỹ thuật không đúng hay thiếu thực tế, có quá nhiều code bị DRY.
Trong đoạn code trên đã sử dụng transaction. Database transaction thường được dùng để đảm bảo tất cả các statements được thực thi sẽ rollback, hay quay trở lại trạng thái ban đầu nếu như có một statement bị lỗi.
Transaction không nên nằm trong controller. Thứ nhất, không nên có quá nhiều statements phụ thuộc lẫn nhau nằm trong controller. Thứ hai là các hàm thông thường trong Active Record đã có sẵn transaction của chính nó. Ví dụ như trên là hàm save.
Giải pháp đưa ra là sử dụng callback và setters trong model.
version.state ||= "Raw" được dùng để set giá trị cho state, và chỉ sử dụng cho version đầu tiên, các lần khác sẽ sử dụng lại giá trị đó. Do vậy, ta nên set giá trị default bằng cách sử dụng database default.
class AddRawDefaultToState < ActiveRecord::Migration def self.up change_column_default :article_versions, :state, "Raw" end def self.down change_column_default :article_versions, :state, nil end end
Chuyển các logic vào callback trong model Version
class Version < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current unless article.versions.empty? if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end
-
Các collection trong quan hệ Active Records của model không bao giờ trả về nil nếu không có record được tìm thấy, mà sẽ trả về collection rỗng. Chúng ta có thể bỏ đi
-
article.current_version đã được lặp quá nhiều lần, vi phạm nguyên tắc DRY, ta nên tách ra
def current_version article.current_version end
với những đoạn code này @article.reporter_id = current_user.id nên được thay thế bằng quan hệ trong Active Record hơn là dùng _id, do vậy ta có: @article.reporter = current_user
Với một vài bước refractor, loại bỏ các DRY, remove các code không cần thiết, kết hợp sử dụng điều kiện trong callback hợp lý ta có kết quả cuối cùng:
Model Version
class Version < ActiveRecord::Base before_validation :set_version_number, :on => :create before_create :mark_related_links_not_current, if: :current_version after_create :set_current_version_on_article private def set_current_version_on_article article.update_attribute :current_version_id, self.id end end
và Controller đã trở thành 1 controller theo chuẩn thông thường
class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.reporter = current_user @article.new_version.writer = current_user if @article.save render :action => :index else redirect_to article_path(@article) end end end
2 Antipattern: Monolithic Controllers
Rails dựa theo cấu trúc RESTful, ánh xạ các action cơ bản trong controller như index, new, create, edit, update, destroy tương ứng với các động từ HTTP như POST, GET, PUT và DELETE
Có 2 dấu hiệu mà một ứng dụng không theo chuẩn RESTful là thứ nhất các bổ sung các param trong URLs để thêm các hành động mới được thực hiện bởi controller. Ngoài ra, đó là các action không theo chuẩn, không nằm trong 7 action của RESTful.
Ví dụ ta có 1 phần của monolithic controller _ controller chỉ có 1 khối
AdminController
def users per_page = Variable::default_pagination_value @users = User.find(:all) if not params[:operation].nil? if (params[:operation] == "reset_password") user = User.find(params[:id]) user.generate_password_reset_access_key user.password_confirmation = user.password user.email_confirmation = user.email user.save! flash[:notice] = user.first_name + " " + user.last_name + "'s password has been reset." end end end
chỉ có một action user, và sử dụng parameter operation để phân biệt các chức năng xử lý URLs sẽ trông giống như thế này
/admin/users?operation=reset_password?id=x /admin/users?operation=delete_user?id=x /admin/users?operation=activate_user?id=x /admin/users?operation=show_user?id=x /admin/users
Giải pháp cho vấn đề này chỉ đơn thuần là chuyển về chuẩn RESTful, tách các chức năng khác nhau cho các controller khác nhau, UserController chỉ xử lý các chức năng liên quan để User, PasswordsController, và ActivationsController thực hiện các chức năng theo đúng ý nghĩa tên gọi của nó
3. AntiPattern: Controller of Many faces
Khi ứng dụng lớn dần, thì các RESTful controller sẽ phải thực hiện một vài action non-RESTful.
Điều đó khó tránh khỏi, để giải quyết vấn đề này, giải pháp được đưa ra là tách các action non-RESTful thành các controller riêng biệt
Ví dụ controller về authentication
class UsersController < ApplicationController def login if request.post? if session[:user_id] = User.authenticate(params[:user][:login], params[:user][:password]) flash[:message] = "Login successful" redirect_to root_url else flash[:warning] = "Login unsuccessful" end end end def logout session[:user_id] = nil flash[:message] = 'Logged out' redirect_to :action => 'login' end end
Một vấn đề xảy là resource của UsersController là gì, login logout chỉ liên quan đến user session và không có quan hệ trực tiếp với một Active Record model.
Để đảm bảo chuẩn RESTful, ta sẽ tách riêng một controller session riêng biệt
class SessionsController < ApplicationController def create if session[:user_id] = User.authenticate(params[:user][:login], params[:user][:password]) flash[:message] = "Login successful" redirect_to root_url else flash.now[:warning] = "Login unsuccessful" render :action => "new" end end def destroy session[:user_id] = nil flash[:message] = 'Logged out' redirect_to login_url end end
Thiết kế này sẽ dễ bảo trì hơn bằng cách nhóm các hành động liên quan đến session vào controller riêng.
4. Antipattern: Rat's Nest Resources
Tình huống đặt ra là một ứng dụng lấy ra tất cả các messages được tạo bởi tất cả user, các thể lấy ra cả các messages được tạo bởi 1 user, nếu 2 danh sách đấy được thực hiện bởi 1 controller ví dụ như:
class MessagesController < ApplicationController def index if params[:user_id] @user = User.find(params[:user_id]) @messages = @user.messages else @messages = Message.all end end end
thì routes cho controller đó sẽ là
resources :messages resources :users do resources :messages end
và trong view để có thể hiện thị chính xác từng danh sách thì phải sử dụng các điều kiện logic, giả sử như nếu các messages được nhóm trong Project thì một user sẽ có view cho tất cả message trong project, message cho một project, tất cả message của một user trong tất cả project hay message của chỉ một user trong một project. Khi đó điều kiện logic trong view và controller sẽ trở nên rất phức tạp.
Giải pháp đưa ra là tách các controller cho mỗi một nest resource
ví dụ như
controllers/messages_controller.rb controllers/users/messages_controller.rb
ta sẽ có route tương ứng:
resources :messages resources :users do resources :messages, :controller => ‘users/messages’ end
công việc của view và controller sẽ trở nên đơn giản hơn rất nhiều
III Tổng kết
-
Trên đây là vài vấn đề hay gặp phải trong controller và một vài giải pháp tương ứng. Nhìn chung, để xây dựng một controller đảm bảo hợp logic, đơn giản, dễ hiểu, dễ bảo trì thì nguyên tắc cơ bản là chia nhỏ vấn đề và xử lý riêng từng vấn đề đấy.
-
Thông tin tham khảo: sách AntiPatterns_ Best practice Ruby on Rails Refractoring