+3

Rails Autoloading and Reloading Constants

Giới thiệu

Với code Ruby bình thường bạn cần phải khai báo các file chứa các class phụ thuộc với lệnh require. Việc này cũng khá quen thuộc bởi vì hầu hết các ngôn ngữ đều như vậy

require 'application_controller'
require 'post'

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Còn với Rails đa số trường hợp bạn đều không thấy từ khóa require nào cả. Có thể thấy Rails đã tối giản đi rất nhiều và giảm công sức cho người lập trình, co thể nói là thông mình hơn với auto load. Với Rails ta chỉ cần viết như sau

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Vì một lí do nào đó mà bạn gọi đến các constants hoặc class ở ngoài mà không cần require một cách thủ công. Chúng ta sẽ cùng tìm hiểu cơ chế tự động load các hằng số (constants) trong rails như thế nào.

Hằng số (Constants)

Nesting

Các class hoặc module có thể được định nghĩa theo cấu trúc phân cấp (nested) hay còn gọi là namspaces

module XML
  class SAXParser
    # (1)
  end
end

Trong ví dụ trên ruby sẽ tự động tạo ra một module là XML đồng thời tạo thêm một class SAXParser thuộc module này

[XML::SAXParser, XML]

Đoạn code sau cũng khá giống tuy nhiên có một chút khác biệt là XML cần được định nghĩa từ trước. Khi đó ta sẽ thêm một class mới là SAXParser thuộc về đối tượng XML

class XML::SAXParser
  # (2)
en

Dưới đây là một trường hợp khá là đặc biệt về module

# 1
module X
  module Y
  end
end

# 2
module A
end

# 3
module X::Y
  module A::B
  end
  module C
  end
end

Bạn có thể thấy nó khá kì quái. Bạn nghĩ là có thể gọi module B theo cách sau X::Y::A::B nhưng sự thực không phải vậy. Thậm chí bạn gọi X::Y::A cũng thấy báo lỗi còn X::Y::C thì gọi bình thường. Như vậy là sao?

Thực ra cũng không có gì là khó hiểu. Ta sẽ phân tích chi tiết như sau. Sau khi chạy xong # 1 ta có 2 module là X và X::Y sau # 2 ta có thêm A. Ở đoạn code #3 ta mở module X::Y ra để thay đổi module này. Trong phần thân của module này ta có đoạn code

module A::B
end

Đoạn code này có nghĩa ta sẽ mở module A ra và thêm vào đó một module là B (bởi vì module A chưa có module B). module A này không liên quan gì đến module X::Y nên ta sẽ không thể truy cập nó với namespace từ module X::Y theo kiểu X::Y::A::B mà sẽ truy cập bằng cách gọi A::B. Có thể nói ngắn gọn hơn thực chất đây là lệnh khai báo thêm mới một module B vào module A được gọi ở trong phần thân của module X::Y

Định nghĩa class và module

class C
end

Ruby sẽ tạo ra một hằng số là C tham chiếu đến một đối tượng được lưu trữ dưới dạng class. Tên của class này cũng là C

class Project < ApplicationRecord
end

Sẽ tương đương với

Project = Class.new(ApplicationRecord)

Có thể bạn sẽ thắc mắc theo cách thứ 2 thì Project.name sẽ có giá trị là gì?

Project.name # => "Project"

Như vậy theo cách thứ 2 Project.name vẫn ra kết quả đúng nhưng mong đợi là Project. Nguyên nhân là do gán giá trị cho một hằng số có một quy luật đặc biệt xảy ra là: nếu một object bị gán cho một hằng số là một class hoặc module chưa có tên thì tên của nó chính là tên của hằng số.

A = Class.new
A.name # => A
B = A
B.name # => A
C = A.dup
C.name # => C

Với module cũng tương tự

A = Module.new
A.name # => A
B = A
B.name # => A
C = A.dup
C.name # => C

Rails autoloading

Rails tự động load các files với Kernel#load khi config.cache_classes là false và dùng Kernel#require trong trường hợp config.cache_classes là true (môi trường production)

Với Kernel#load cho phép Rails cập nhật lại file mỗi khi nội dung file thay đổi.

Cơ chế load của Rails

Ruby tìm kiếm file cài đặt cho các class module ở trong thư mục $LOAD_PATH. Nó tìm các file có đuôi rb, so, o, dll ở trong các thư mục trong $LOAD_PATH và dùng lại ngay khi tìm thấy file tương ứng, trong trường hợp không tìm thấy file tương ứng nó sẽ raise ra lỗi LoadError

Bạn có thể thay đổi $LOAD_PATH với config.autoload_paths. Ví dụ bạn thêm thư mục lib, muốn code ở trong thư mục này được tự động load thì bạn cần thêm vào config/application.rb như sau

config.autoload_paths << "#{Rails.root}/lib"

Bạn có thể kiểm tra giá trị hiện tại của autoload_paths với lệnh sau

bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'
Cơ chế autoload của Rails

Ta lấy ví dụ một đoạn code như sau

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Ta có 3 hằng số là PostsController, ApplicationControllerPost

Hằng số ở sau từ khóa module hoặc class trong trường hợp này là PostsController. Ruby sẽ kiểm tra hằng số này đã có chưa nếu chưa có Ruby sẽ tạo mới hằng số này còn có rồi thì sẽ mở lại hằng số này ra và sửa lại nó.

Các hằng số phụ thuộc khác như ApplicationController, Post. Trong trường hợp hằng số này chưa được load (chưa tồn tại), Rails sẽ tự động load hằng số này từ file hiện tại và các thư mục trong autoload_paths. Nếu trường hợp không tìm thấy sẽ raise ra lỗi LoadError

Ví dụ với ApplicationController ở trong file này không hề định nghĩa ApplicationController và hằng số này cũng chưa được load trước đó. Rails sẽ tự động tìm kiếm hằng số này trong các file cài tương ứng ở các đường dẫn cài đặt trong config.autoload_paths ví dụ (app/assets/application_controller.rb và app/controllers/application_controller.rb)

Tự động load với namespaces

Ví dụ

module Admin
  class BaseController < ApplicationController
    @@all_roles = Role.all
  end
end

Hằng số Role sẽ tìm kiếm hằng số Role theo namespace với thứ tự như sau

Admin::BaseController::Role
Admin::Role
Role

Tương ứng với các hằng số theo namespace kia Rails sẽ tìm kiếm các file tương ứng với namespace như sau để load hằng số này

admin/base_controller/role.rb
admin/role.rb
role.rb

Tải lại các hằng số (Constant Reloading)

Khi config.cache_classes is false Rails sẽ tự động tải lại nội dung file mỗi khi nội dung file thay đổi.

Tuy nhiên một số trường hợp Rails không tự động load lại code, ví dụ như trong trường hợp bạn chạy rails console bạn cần chạy lệnh reload! để tải lại code mới được cập nhật.

Tham khảo

  1. utoloading_and_reloading_constants.html

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.