ActiveRecord refactoring (P1) - Concerns
Bài đăng này đã không được cập nhật trong 3 năm
Mở đầu
Trong Ruby, ActiveRecord
cung cấp cho ta rất nhiều sức mạnh. VỚi sức mạnh đó thì ta có thể thêm vào các logic để thực hiện những công việc của mình để tạo ra những model
lớn.
Tuy nhiên, càng ngày với càng nhiều xử logic hơn thì đồng nghĩa với việc model
của chúng ta cũng sẽ dần phình to ra (mà người ta hay gọi là Fat models
) và trở nên cồng kềnh, chậm chạp.
Cũng có nhiều cách để nhằm khắc phục vấn đề này, để giúp giảm bớt công việc cho các model
.
Bạn có thể đọc thêm về bài viết 7 Patterns to Refactor Fat ActiveRecord Models.
Sau đây, mình xin gửi đến các bạn chuỗi bài viết ActiveRecord Refactoring của tác giả Luke Morton.
Tác giả có đề cập đến 3 hướng tiếp cận :
-
Concerns
-
Services
(hay còn gọi làInteractors
). -
Presenters
Và ở bài viết này, mình xin đề cập đến hướng tiếp cận thứ nhất.
Phần 1. Concerns
Design pattern
đầu tiên để nhằm giảm tải cho model ActiveRecord
đó chính là concerns
. Concerns
có thể giúp giảm bớt việc lặp đi lặp lại logic thông qua các model
. Nó cũng giúp nhóm các vấn đề cụ thể của logic cùng với nhau bằng cách di chuyển logic vào một file khác.
Hãy tưởng tượng là bạn có 2 model
là Blog::Post
và Blog::Comment
. Cả hai đều có một hàm long_date
để hiển thị ngày như sau :
# app/models/blog/post.rb
module Blog
class Post < ActiveRecord::Base
def long_date
date.strftime("%A, #{date.day.ordinalize} %B %Y")
end
end
end
# app/models/blog/comment.rb
module Blog
class Comment < ActiveRecord::Base
def long_date
date.strftime("%A, #{date.day.ordinalize} %B %Y")
end
end
end
Rõ ràng là ở đây đã vi phạm nguyên tắc DRY
- không lặp lại chính mình. DRY
là một nguyên tắc tốt và tôi không bất ngờ nếu như bạn đã quen thuộc với việc sử dụng mixins
trong trường hợp này.
# app/models/concerns/blog/date_concern.rb
module Blog
module DateFormattable
def long_date
date.strftime("%A, #{date.day.ordinalize} %B %Y")
end
end
end
# app/models/blog/post.rb
module Blog
class Post < ActiveRecord::Base
include Blog::DateFormattable
end
end
# app/models/blog/comment.rb
module Blog
class Comment < ActiveRecord::Base
include Blog::DateFormattable
end
end
Và ở đây, bạn đã loại bỏ sự trùng lặp bằng cách di chuyển hàm long_date
vào trong Blog::DateFormattable
và sau đó sẽ sử dụng nó bằng cách include
nó vào trong cả 2 model
Blog::Post
và Blog::Comment
.
Nhóm method
một cách hợp lý và đặt tên cho các concern
Có một vài điểm cần bàn về concern
.
Thứ nhất là nhóm các method
một cách hợp lý. Ở ví dụ trên, chúng ta đã tạo ra module Blog::DateFormattable
, module này tất nhiên sẽ chứa các method cụ thể về date
. Nếu mà ví dụ bạn muốn thêm một loạt các method để định dạng tên tác giả cho cả 2 model Blog::Post
và Blog::Comment
thì bạn sẽ sử dụng một concern
khác như sau :
# app/models/concerns/blog/authorable.rb
module Blog
module Authorable
def author_full_name
"#{author.first_name} #{author.last_name}"
end
end
end
# app/models/blog/post.rb
module Blog
class Post < ActiveRecord::Base
include Blog::DateFormattable
include Blog::Authorable
end
end
# app/models/blog/comment.rb
module Blog
class Comment < ActiveRecord::Base
include Blog::DateFormattable
include Blog::Authorable
end
end
Giờ thì chúng ta vừa mới thêm method author_full_name
vào cả 2 model của chúng ta. Vẫn đảm bảo DRY
và concern
của chúng ta được đặt tên một các sinh động.
Testing concern
Điểm thứ 2 tôi muốn bàn đến là trong lúc test concern
. Bạn có 2 cách làm
- Test trực tiếp cho từng
concern
riêng rẽ. - Test chức năng của
concern
đó trong mỗiclass
mà đãinclude
đến nó.
Nếu như test trực tiếp cho từng concern
như sau :
# spec/models/concerns/blog/authorable_spec.rb
describe Blog::Authorable do
let(:authorable) {Class.new.extend(described_class)}
before(:each) do
authorable.stub(author: double(first_name: "Luke", last_name: "Morton"))
end
context "#author_full_name" do
it "should return the full name of the author" do
authorable.author_full_name.should eq("Luke Morton")
end
end
end
Trong ví dụ này, ta tạo mới một class ẩn danh với Class.new
và mixins
concern
của chúng ta. Sau đó ta stub
một author
tự trả về method first_name
và last_name
trong class ẩn danh đó. Và ta trả về giá trị của author_full_name
.
Cách viết trên cũng tốt rồi nhưng cá nhân tôi thấy test concern
này chỉ hữu ích khi sử dụng test để hướng dẫn thiết kế của bạn. Điều này rất khó xảy ra. Thông thương thì khi ta đã xác định các method
trong model
và sau đó, trong quá trình sử dụng mới nhận ra là chúng ta cần sử dụng nó ở một nơi khác nữa. Tại thời điểm này, bạn đã viết test cho việc thực hiện đầu tiên do đó viết test concern
để thực hiện lại chức năng dường như là không cần thiết.
Ta có thể thay thế bằng cách sử dụng shared_examples_for
. Hãy bắt đầu với test cho Blog::Post
:
# spec/models/blog/post_spec.rb
require "spec_helper"
describe Blog::Authorable do
let(:post) {described_class.new}
context "#author_full_name" do
it "should return the full name of the author" do
post.stub(author: double(first_name: "Luke", last_name: "Morton"))
post.author_full_name.should eq("Luke Morton")
end
end
end
Bây giờ bạn muốn sử dụng lại method này cho cả Blog::Comment
, do đó đầu tiên chúng ta nên di chuyển testcase
này vào shared_examples
như sau :
# spec/support/shared_examples/authorable_example.rb
shared_example_for Blog::Authorable do
before(:each) do
authorable.stub(author: double(first_name: "Luke", last_name: "Morton"))
end
context "#author_full_name" do
it "should return a full name of the author" do
authorable.author_full_name.should eq("Luke Morton")
end
end
end
Và cập nhật lại rspec của Blog::Post
để sử dụng :
# spec/models/blog/post_spec.rb
require "spec_helper"
describe Blog::Post do
let(:post) {described_class.new}
it_should_behave_like Blog::Authorable do
let(:authorable) {post}
end
end
Bạn cũng có thể tái sử dụng shared_examples
trong lúc test Blog::Comment
.
# spec/models/blog/post_spec.rb
describe Blog::Comment do
let(:comment) {described_class.new}
it_should_behave_like Blog::Authorable do
let(:authorable) {comment}
end
end
shared_examples
không thêm nhiều thứ phức tạp vào test của bạn và đảm bảo rằng method làm việc trong đối tượng inlcude chúng hơn là làm việc độc lập. Thông thường, bạn muốn test độc lập một đơn vị của công việc. Nhưng với mixins
trong Ruby là một cách cơ bản để sao chép và dán các method vào class thì tôi tin rằng chúng nên được test ở mức độ mà chúng đang được sử dụng.
Nếu bạn không thể sử dụng trực tiếp một method concern
nào đó thì chúng ta viết test trực tiếp cho chúng để làm gì?
Lần tới, chúng ta sẽ thảo luận một ví dụ việc sử dụng services
như là một cách thay thế cho concern
Tham khảo
Bài viết liên quan
Cảm ơn bạn đã theo dõi bài viết
tribeo
All rights reserved