Tại Gigster, chúng tôi là fan cuồn của DRY. Bởi lẽ đó, chúng tôi đang liên tục phát triển và duy trì một lượng lớn thư viện và framework tái sử dụng được cho nhiều project khác nhau. Đôi khi, chúng chỉ là một vài công cụ nhỏ giúp chúng chúng tôi giải quyết một số khó khăn, nhưng chúng tôi cũng có nhiều template cho các kiểu project nổi tiếng hơn.
Gần đây, tôi được yêu cầu phát triển một template tái sử dụng được cho các website crowdfunding. Template này sẽ ở dạng framework có thể được “cắm” vào chương trình có sẵn để mang dến các tính năng giống với Kickstarter.
TopDev Techtalk #51: Golang, Ruby: Hướng đi nào trong tương lai?
*Hồ Chí Minh: 18h00 – 21h00 ngày 24/11/2016.
Tôi có biết về Rails engines (và, như phần lớn lập trình viên Rails, tôi dùng rất nhiều engine mỗi ngày) nhưng tôi lại chả bao giờ có thời gian hay nhu cầu tự tạo engine cả. Vì vậy, với những kiến thức liên quan từ Rails guide, tôi đã quyết định bắt tay tạo ra Rails engine xác định được đầu tiên của mình.
Dưới đây là một loạt các thách thức kỹ thuật tôi gặp phải trong quá trình phát triển Crowdster và cách tôi giải quyết chúng. Đây chả phải những thủ pháp gì mới: tất cả đều có thể tìm được ở các Rails engines nổi tiếng.
- TOP việc làm Ruby on Rails by
- [HCM] Senior Ruby on Rails | 1,000 – 2,500 USD — SUCCESS SOFTWARE SERVICES
- [TTS] Lập Trình Viên Phần Mềm Ruby – VAREAL K.K tuyển dụng
- [HN, ĐN, HCM] Ruby on Rails Developer – CMC Global tuyển dụng
- [HCM] Senior Ruby on Rails Engineer | 2,000 – 5,000 USD – Money Forward tuyển dụng
Phương thức “configuration block”
Điều đầu tiên tôi cần làm là cung cấp cách thiết đặt engine cho người dùng.
Hầu như tất cả engine thiết đặt được đều có xuất hiện API như API được dùng trong Rail initializer sau:
1
2
3
4
|
MyEngine.configure do |config|
# Description of the setting here…
config.setting1 = :value
end
|
Vẫn có một số biến thể, nhưng ý tưởng chính vẫn là: bạn sẽ sử dụng method để tạo object thiết đặt. Object này sau đó sẽ được người dùng sử dụng để thiết đặt cho engine.
Bắt đầu từ kết quả tôi muốn đạt được, tôi tạo thêm một class thiết đặt, class này chỉ đơn giản đóng vai trò container cho tất cả tùy chọn trong phần thiết đặt:
1
2
3
4
5
|
module Crowdster
class Configuration
attr_accessor :setting1, :setting2
end
end
|
Giờ thì sau khi có class, tôi chỉ cần thêm point of entry (điểm nhập thôi):
1
2
3
4
5
6
7
8
9
10
|
module Crowdster
class << self
attr_reader :config
def configure
@config = Configuration.new
yield config
end
end
end
|
Tôi cũng có thêm vào một vài logic để xác thực thông tin nhập vào.
Base controller inheritance
Vấn đề này hơi hóc búa hơn một chút. Vì Crowdster không xử lý xác minh người dùng, tôi cần phải tiếp cận được với một số helper trong base controller của ứng dụng mẹ.
Sau nhiều giải pháp bất thành, và có phần thái quá, tôi đã quyết định chỉ cho phép đặt tên của base controller thông qua object thiết đặt thôi, và sau đó mở rộng ra trong ApplicationController của engine, giống như sau:
1
2
3
4
5
6
|
module Crowdster
module Api
class ApplicationController < Crowdster.config.base_controller.constantize
end
end
end
|
Sau đó, trong initializer:
1
2
3
4
|
Crowdster.configure do |config|
# The :: is needed to access the root namespace.
config.base_controller = ‘::ApplicationController’
end
|
Tất cả engine controller mở rộng ApplicationController, vì vậy giờ tôi đã có thể tiếp cận được helper từ ứng dụng mẹ.
Tôi vẫn không chắc liệu đây đã là giải pháp tối nhất hay liệu có gây xung đột nào hay không, nhưng đây là phương pháp đến nay vẫn hoạt động rất tốt trong trường hợp của chúng tôi.
Mở rộng classes (via class_eval)
Sau khi đã lo xong phần cơ bản, tôi có thể thực sự lập trình nghiêm túc rồi.
Tôi tạo một model trong engine thuộc về user class trong ứng dụng me. Tên class, một lần nữa, có thể thiết đặt được thông qua initializer, nên mọi thứ vẫn làm việc tốt.
Tuy nhiên, tôi cần tạo thêm qua hệ has_many tương ứng trên user model trong ứng dụng me. Tôi phải làm sao đây?
Cuối cùng, tôi tìm đến giải pháp khá đơn giản:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
module Crowdster
class Engine < ::Rails::Engine
isolate_namespace Crowdster
config.to_prepare do
if Crowdster.config
Crowdster.config.user_class.constantize.class_eval do
has_many :campaigns, class_name: ‘Crowdster::Campaign’
end
end
end
end
end
|
Mở rộng classes (via acts_as_*)
Phương thức acts_as là phương thức nổi tiếng trong nhiều engine.
Như vậy, bạn sẽ patch ActiveRecord::Base (hoặc đối tượng tương đượng trong ORM) để cung cấp class method giúp bơm hành vi mong muốn và class hiện tại.
Trong trường hợp của tôi, tôi tạo method acts_as_campaigner, yêu cầu phải bơm logic liên quan vào user model:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
module Crowdster
module ActsAsCampaigner
def included(klass)
klass.extend ClassMethods
end
module ClassMethods
def acts_as_campaigner
has_many :campaigns, class_name: ‘Crowdster::Campaign’
end
end
end
end
|
1
2
3
4
5
6
7
8
9
|
module Crowdster
class Engine < ::Rails::Engine
isolate_namespace Crowdster
config.to_prepare do
ActiveRecord::Base.include Crowdster::ActsAsCampaigner
end
end
end
|
1
2
3
|
class User < ActiveRecord::Base
acts_as_campaigner
end
|
Với giải pháp acts_as, bạn hoàn toàn có thể xác định thiết đặt cho mỗi model, giúp engine của chúng ta được linh hoạt hơn.
Extending classes (via modules)
Bạn có thấy ta dùng module được để mở rộng chức năng ActiveRecord::Base thay vì mở class trực tiếp chứ?
Bạn có thể đi theo hướng tương tự để mở rộng class User:
1
2
3
4
5
6
7
8
9
10
11
|
module Crowdster
module Models
module Campaigner
def included(klass)
klass.class_eval do
has_many :campaigns, class_name: ‘Crowdster::Campaign’
end
end
end
end
end
|
1
2
3
|
class User < ActiveRecord::Base
include Crowdster::Models::Campaigner
end
|
Các này sẽ rút gọn số lượng code cần gõ, và có thể cô lập hơn, vì chúng ta không pollute ActiveRecord::Base. Nhưng cách này lại không cài đặt linh hoạt được như giải pháp “acts_as”.
Test engine với RSpec
Theo mặc định, Rails generator dùng MiniTest để test engine.
Nếu bạn thích dùng RSpec (giống tôi), bạn sẽ phải thực hiện một số thay đổi để có thể chạy spec cho phù hợp.
Sau khi cài đặt RSpec, hãy chuyển thư mục test/dummy sang spec/dummy, sau đó xóa thư mục test. Tiếp đến, hãy chỉnh sửa rails_helper.rb giống như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
ENV[‘RAILS_ENV’] ||= ‘test’
ENGINE_ROOT = File.join(File.dirname(__FILE__), ‘../’)
# Load environment.rb từ dummy app.
require File.expand_path(‘../dummy/config/environment’, __FILE__)
abort(“The Rails environment is running in production mode!”) if Rails.env.production?
require ‘spec_helper’
require ‘rspec/rails’
# Load RSpec helpers.
Dir[File.join(ENGINE_ROOT, ‘spec/support/**/*.rb’)].each { |f| require f }
# Load migrations từ dummy app.
ActiveRecord::Migrator.migrations_paths = File.join(ENGINE_ROOT, ‘spec/dummy/db/migrate’)
ActiveRecord::Migration.maintain_test_schema!
RSpec.configure do |config|
config.infer_spec_type_from_file_location!
end
|
Như vậy, RSpec sẽ load môi trường và database migration từ dummy app, thay vì tìm kiếm trong engine.
Khi tạo một database migration, bạn sẽ phải copy vào dummy app trước khi thực thi:
1
2
3
|
$ cd spec/dummy
$ rake my–engine:install:migrations
$ rake db:migrate
|
Để chạy được controller spec, bạn cũng sẽ cần thông báo RSpec cần phải dùng tập hợp route nào:
1
2
3
4
5
6
7
|
require ‘rails_helper’
RSpec.describe MyEngine::UsersController do
routes { MyEngine::Engine.routes }
# …
end
|
Vậy là xong rồi, các bạn giờ đã có thể dùng testing framework tùy thích rồi đấy!
Để có thể hiểu rõ hơn về những điểm mạnh, điểm yếu và biết được nhiều hơn nữa về Ruby & Golang bạn hãy nhanh chóng đăng ký tham gia buổi TopDev Techtalk: Golang, Ruby- Hướng đi nào cho tương lai? để lắng nghe những đánh giá và chia sẻ kinh nghiệm chuyên sâu của các chuyên gia, những người đang làm việc thực tế và sử dụng các ngôn ngữ này.
*Hồ Chí Minh
Thời gian: 18h00 – 21h00 ngày 24/11/2016.
Địa điểm: ĐH Hoa Sen, 08 Nguyễn Văn Tráng, Quận 1.
Mọi thông tin hỗ trợ vui lòng liên hệ:
Tel: 08 6273 3497
Hotline : 0944 685 243 – Ms. Ngọc | 0963 651 587 – Ms. Nguyên