12/08/2018, 16:23

Tạo Configuration Object trong Ruby

Ta có thể thay đổi cách một ứng dụng hoạt động một cách dễ dàng mà không phải động vào source code thông qua việc chỉnh sửa một sỗ config được thiết lập sẵn. Việc thay đổi cách hoạt động của code thông qua config mang lại nhiều lợi ích cho chúng ta. Nó hạn chế việc thay đổi source code mà vẫn đảm ...

Ta có thể thay đổi cách một ứng dụng hoạt động một cách dễ dàng mà không phải động vào source code thông qua việc chỉnh sửa một sỗ config được thiết lập sẵn.

Việc thay đổi cách hoạt động của code thông qua config mang lại nhiều lợi ích cho chúng ta. Nó hạn chế việc thay đổi source code mà vẫn đảm bảo tính mềm dẻo dễ thay đổi của hệ thống.

Vậy làm cách nào để ta tạo một config object ? Hãy đi vào bài viết.

Một Configuration Object đơn giản

Nhiều ứng dụng không cần đến một hệ thống configuration phức tạp mà đơn giản chỉ cần sử dụng một object Ruby đơn giản nhất đó là Hash.

Một kiểu dữ liệu key-value là một khái niệm đơn giản nhất mà một Configuration Object cần. Ruby Hash là nền tảng chính để implement một Configuration Object.

config = {}

config[:my_key] = :value
config[:my_key]
# => :value

Một cách implement phức tạp hơn đó là sử dụng một object thuần của Ruby và sử dụng metaprogramming để tạo các method động có thể trả về các giá trị tương ứng với các key truyền vào.

Một Configuration Object đơn thuần trong Ruby mà bao gồm cả 2 cách implement trên đó là OpenStruct. Object này sử dụng hash bên trong implement của nó và nó cũng có thể được sử dụng để định nghĩa các method mới bên trong nó:

require 'ostruct'

config = OpenStruct.new

config.my_key = :value
config.my_key
# => :value
# config[:my_key]
# # => :value

Ruby Hash sẽ luôn trả về nil nếu như key chưa được định nghĩa bên trong nó. Việc này là bình thường đối với những trường hợp đơn giản, tuy nhiên nó là một cách làm không hướng đối tượng lắm. Đôi khi ta lại muốn nó trả về một giá trị mặc định nào đấy. Hash cung cấp cho chúng ta một method là default_prc để ta có thể định nghĩa việc xử lý nếu như key chưa được định nghĩa trong Hash

Ví dụ: tạo một Hash mà có thể tạo các key lồng nhau trong một lần tạo

module MyProject
  def self._hash_proc
    ->hsh,key{
      hsh[key] = {}.tap do |h|
        h.default_proc = _hash_proc()
      end
    }
  end

  def self.config
    @config ||= begin
      hsh = Hash.new
      hsh.default_proc = _hash_proc()
      hsh
    end
  end
end


MyProject.config
# => {}

MyProject.config[:apple][:banana][:cherry] = 3

MyProject.config
# => {:apple=>{:banana=>{:cherry=>3}}}

Tuy nhiên, với phương pháp này, ta chỉ đơn giản là thay nil bằng một Hash rỗng khác. Phương pháp này chỉ phù hợp với những ứng dụng đơn giản và không phức tạp. Ta cần phải xử lý những giá trị mặc định này khác nhau tùy theo từng tính chất ứng dụng.

Ngoài ra, trong Rails còn cung cấp thêm cho chúng ta một loại Hash đó là HashWithIndifferentAccess. Nó cho phép ta sử dụng cả String và Symbol như là cùng một key.

config = HashWithIndifferentAccess.new

config["asdf"] = 4

config[:asdf]
# => 4

Configuration Object trong Rails

Rails có một module hỗ trợ ta tạo các Configuration Object đó là ActiveSupport::Configurable:

class DatabaseConfig
  include ActiveSupport::Configurable

  config_accessor :database_api do
    DummyDatabaseAPI.new
  end
end

Trong đoạn code trên, config_accessor tạo một hàm config database_api cho DatabaseConfig và sử dụng một object DummyDatabaseAPI làm giá trị mặc định. Điều này giúp ta có thể định nghĩa một số behavior mặc định nếu như chưa có một database cụ thể nào. Nếu ta cần thiết lập một database cụ thể thì chỉ cần gọi hàm database_api=.

Lưu lại các Configuration

Để lưu các Configuration thì ta có thể sử dụng file, database, biến môi trường...

Ruby hỗ trợ config thông qua file YAML. Một file YAML có cấu trúc như sau:

---
# A list of tasty fruits
fruits:
  - Apple
  - Orange
  - Strawberry
  - Mango

Và để load file này, trong Ruby ta sẽ viết như sau:

require 'yaml'

YAML.load(
  File.open('example.yml').read
)
# => {"fruits"=>["Apple", "Orange", "Strawberry", "Mango"]}

Nếu muốn bạn cũng có thể tạo file YAML từ Ruby Hash

config = {
  "default" => {
    "adapter"=>"postgres",
    "encoding"=>"utf8",
    "pool"=>5,
    "timeout"=>5000
  }
}

puts config.to_yaml
# ---
# default:
#   adapter: postgres
#   encoding: utf8
#   pool: 5
#   timeout: 5000

Biến môi trường

Thông thường ta nên hạn chế việc sử dụng trực tiếp biến môi trường trong source code. Ta chỉ nên sử dụng một hệ thống config trong code thay để dễ quản lý hơn. Nếu như đã sử dụng YAML thì ta chỉ nên sử dụng config thông qua YAML mà thôi.

Tuy nhiên, đôi khi có những giá trị mà ta buộc phải set trong biến môi trường như token hay access_key của api bên thứ 3. Điều ta cần làm là cần lấy gía trị từ biến môi trường gắn vào file YAML.

YAML không hỗ trợ trực tiếp việc lấy giá trị từ biến môi trường, tuy nhiên ta có thể sử dụng thêm ERB để có thể lấy được giá trị thông qua <%= %>.

# # YAML file
# production:
#   secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
# # End YAML file

require 'yaml'
require 'erb'

yaml_contents = File.open('config.yml').read
config = YAML.load( ERB.new(yaml_contents).result )

config
# => {"production"=>{"secret_key_base"=>"uih...943"}}

Tổng kết

Việc tạo Configuration Object trong ứng dụng không quá phức tạp. Ta chỉ cần quan tâm đến một số thứ như cách implement, scope và size của dự án để lựa chọn phương án phù hợp nhất.

0