Skip to Content

Create your own generator

Posted on

1. Tản mạn

Nếu bạn đã từng code Rails, dù master hay mới chỉ beginer, chắc hẳn bạn đã có lần dùng qua lệnh rails generator model $model_name, hoặc rails g controller $controller_name. Generators là 1 tool của Rails, giúp chúng ta có thể sinh code theo các templates có sẵn một cách dễ dàng, tiện lợi. Bạn có thể đọc về code của tính năng generator của Rails trên github: base-generator.

Mọi chuyện sẽ không có gì đáng nói nếu dự án không “to” dần lên, khiến chúng ta nghĩ tới chuyện áp dụng các design patterns vào Rails project: Service Object, Value Object, Form Object, View Object, Null Object, … Đi kèm với đó là các files sinh ra khi áp dụng theo 1 patterns (ít nhất là 1 file trong thư mục app/$pattern_name và 1 file trong thư mục spec). Việc tạo file và gõ đi gõ lại khá nhàm chán, nên ban đầu mình dùng cơm - sử dụng text snippet. Hầu hết các editor thông dụng đều hỗ trợ custom snippet, như với Vim thì mình có tạo snippet ở đây. Tuy nhiên, cách này khá thủ công, và chỉ dùng được cho 1 file, vẫn phải gõ khá nhiều =))

Và Rails Generator là 1 giải pháp khác, xịn xò hơn. Trong bài viết này mình sẽ tự tạo 1 generator cho service object. (Các loại object khác cũng có thể làm tương tự ;)

2. Triển

Trong trường hợp bạn chưa dùng service object :yaoming: thì Service Object là 1 ruby class, có nhiệm vụ thực thi 1 action nào đó. Nó được sinh ra nhằm giảm tải được logic cho controller hoặc model. Khi tách phần logic đó ra bên ngoài, chúng ta sẽ dễ đọc code hơn, dễ tái sử dụng hơn, dễ test hơn và dễ mở rộng hơn, code sẽ trông DRY hơn ;)

Example:

class TestService
  def initialize
  end

  def execute
  end

  private

  def method1
  end

  # .
  # .
  # .

  def methodN
  end
end

Có 2 cách để tạo ra 1 rails generator: làm bằng tay hoặc dùng luôn generator =)) Tức là trong Rails đã hỗ trợ sẵn cách để generate ra 1 generator =))

Để tạo ra 1 generator, bạn chỉ cần mở terminal và chạy lệnh:

$ bin/rails generate generator service
      create lib/generators/service
      create lib/generators/service/service_generator.rb
      create lib/generators/service/USAGE
      create lib/generators/service/templates
      invoke test_unit
      create test/lib/generators/service_generator_test.rb

Command trên sẽ sinh ra thư mục generator trong thư mục lib của Rails app. Nếu bạn muốn tách code của Rails generator ra chỗ khác, k để trong lib thì hãy custom + thêm extra autoload paths vào application.rb.

Cùng xem qua file được rails generator tạo ra:

class ServiceGenerator < Rails::Generators::NamedBase
     source_root File.expand_path('templates', __dir__)
end

Trong file này, generator của chúng ta đang thừa kế từ Rails::Generators::NamedBase, tức là nó cần ít nhất 1 argument vào câu lệnh tạo. Bạn có thể đọc thêm nhiều options của Rails generator tại rails-guide.

Mục tiêu của chúng ta là sẽ tạo ra 1 generator, mà khi ta gọi lệnh sinh ra files, nó sẽ nhận vào 1 list các tham số: tên service, tên các method, namespace của class, … Câu lệnh này sẽ sử dụng template được chúng ta quy định sẵn.

1. Create service generator

Sau khi đã sinh ra thư mục lib/generators/service bằng câu lệnh bên trên, chúng ta sẽ sửa code, phần lớn code sẽ nằm ở file service_generator.rb

class ServiceGenerator < Rails::Generators::NamedBase
	source_root File.expand_path('../templates', __FILE__)

	argument :methods, type: :array, default: [], banner: "method method"
	class_option :module, type: :string

	def create_service_file
		@module_name = options[:module]

		service_dir_path = "app/services"
		generator_dir_path = service_dir_path + ("/#{@module_name.underscore}" if @module_name.present?).to_s
		generator_path = generator_dir_path + "/#{file_name}.rb"

		Dir.mkdir(service_dir_path) unless File.exist?(service_dir_path)
		Dir.mkdir(generator_dir_path) unless File.exist?(generator_dir_path)

		template "service.erb", generator_path
	end

end

Dòng source_root File.expand_path('../templates', __FILE__) sẽ trỏ tới thư mục templates, nơi chúng ta tạo các template mặc định cho service.

argument :methods, type: :array, default: [], banner: "method method"
class_option :module, type: :string

Đoạn code trên định nghĩa các tham số sẽ truyền vào. Method argument sẽ parse các tham số trên command line thành các themdos, đưa chúng thành attr_accessor, còn class_option sẽ parse command line options và lưu chúng vào trong biến options.

Phần code bên dưới khá đơn giản, chúng ta sẽ phải tạo 1 method create_service_file, trong đó sẽ chứa toàn bộ logic. Do có sử dụng method class_option bên trên, nên option module của chúng ta đã được lưu vào biến options, do đó, ta có thể gọi ra module_name từ `options[:module].

Bên dưới, chúng ta chỉ định nơi sẽ lưu file service mới. Thông thường là trong thư mục app/services, nếu có module_name thì sẽ là app/services/module_name/. Giá trị file_name sẽ được định nghĩa trong NameBase mà ta kế thừa. Và cuối cùng, chúng ta dùng method template để lấy template cần dùng. Nếu bạn muốn generate nhiều file hơn, làm tương tự: định nghĩa file_path, sau đó gọi method template để gen ra file.

2. Create template file

<%# service.erb %>

<% if  @module_name.present? %> module <%= @module_name.camelize %>  <% end  %>
class <%= class_name %>

def initialize
end

def call
end

<% if  methods.present? %> private <% end %>
<% for  method in methods %>
  def <%= method %>
  end
<% end  %>
end
<% if @module_name.present? %> end <% end  %>

Template của chúng ta viết khá đơn giản, dùng code nhúng erb để viết.

3. Fill out the USAGE file

Cuối cùng, chúng ta sẽ điền vào file USAGE để cho mọi người dễ dùng hơn, vì file USAGE này như 1 tờ hướng dẫn sử dụng vậy :v Thông tin sẽ được show ra khi người dùng sử dụng options --help hoặc -h kèm với command.

Nếu bạn gõ lệnh rails g service -h, kết quả sẽ được show ra:

4. Use

Bạn có thể sử dụng các câu lệnh dưới đây để sinh ra file service:

  • rails g service test_service

    class TestService
      def initialize
      end
    
      def call
      end
    end
  • rails g service test_service test_method1 test_method2

    class TestService
      def initialize
      end
    
      def call
      end
    
      private
    
      def test_method1
      end
    
      def test_method2
      end
    end
  • rails g service test_service --module test_module

    module TestModule
      class TestService
        def initialize
        end
    
        def call
        end
      end
    end

Nếu bạn không muốn sinh thêm những đoạn code generator ở trong thư mục code của dự án, bạn cũng có thể sử dụng gem. Gem này được build từ những đoạn code chúng ta vừa xem ở bên trên. ;)

3. Kết

Chỉ bằng vài đoạn code đơn giản, bạn đã có thể tự tạo cho mình 1 generator mới. Có thể nó không giúp bạn tiết kiệm nhiều thời gian, nhưng chí ít, nó cũng giúp bạn hiểu được phần nào về Rails generator.

Hy vọng sau bài viết này, bạn có thể tự tạo thêm các generator khác để áp dụng cho các design pattern còn lại. ;)

Bài viết được tham khảo từ: create your own rails generator.

comments powered by Disqus