Exploring the Structure of Ruby Gems
Bài đăng này đã không được cập nhật trong 8 năm
Khi bạn tạo một ứng dụng Rails, đặc biệt là những người không quá phân biệt về tiêu chuẩn hình thức của nó, chúng tôi thường không suy nghĩ quá nhiều về cách cấu trúc file hoặc là nó được đặt ở đâu. Models
sẽ được đặt trong folder models
, Controllers
sẽ được đặt trong folder controllers
, etc.
Nhưng bạn có biết tất cả những gì về những gems
bạn dùng được chứa trong file Gemfile
của bạn? Chúng được cấu trúc như thế nào? Liệu họ có một tiêu chuẩn nào đó để tổ chức code của họ?
Trong bài viết này, chúng ta sẽ xem xét cách mà Ruby gems
thường được cấu trúc như thế nào? Chúng ta sẽ xem xét nơi đặt mã code của chúng ta, làm sao để namespace nó, làm sao để chắc chắn rằng chúng liên kết với nhau một cách đúng đắn. Thông qua quá trình này, chúng ta sẽ có một cách nhìn phổ biến hơn về Ruby gems để xem cách họ đã thực hiện nó.
Loading code
Trước khi cúng ta xem xét đến cách code bên trong một gem được cấu trúc như thế nào? Chúng ta hãy nhìn vào những cơ chế khác nhau của Ruby để giúp chúng ta load code.
Trước khi bạn có thể sử dụng code, nó phải được load vào bộ nhớ (memory). Ruby có một biến cục bộ đặc biệt được gọi là $LOADED_FEATURES
nó cho chúng ta thấy rằng những files nào đã được xem xét và load code của chúng vào trong bộ nhớ. Với một ứng dụng Rails ngẫu nhiên, tôi đã thấy rằng có gần 2.500 mục.
Để Ruby load files, chúng ta cần sử dụng một trong 3 phương thức khác nhau có sẵn trong ngôn ngữ Ruby. Bao gồm các phương thức sau: require
, require_relative
, and autoload
. Tất cả 3 phương thức này đều là một phần của Kernel module.
require
với một đường dẫn tương đối sẽ tìm một file trong thư mục bên trong$LOAD_PATH
, một đăng ký của tất cả các nơi mà code có thể được load một cách tự động cho một ứng dụng cụ thể.require_relative
tương tự vớirequire
, nhưng nó bổ sung cho các method trongrequire
bằng cách cho phép bạn tải những files có liên quan đến file có chứa trong tuyên bốrequire_relative
autoload
sẽ không load files ngay tập tức, nhưng khi đăng kí, file sẽ được loaded lần đầu tiên khi một module được sử dụng:
autoload(:MyModule, "/usr/local/lib/modules/my_module.rb").
Matz nói rằng autoload
sẽ không được hỗ trợ và có lẽ nó sẽ không còn tồn tại trong Ruby 3.0. Tuy nhiên, tôi có một thời gian khó khăn để thấy rằng cách mã code này bị phản đối, dựa vào phán đoán rằng nó còn được sử dụng ở một trong số những gem phổ biến như rack
, nó là một gem đứng thứ 2 trong tổng số dowloads mất đi từ rake gem.
Tại sao tôi không cần phải require trong Rails?
Trong các ứng dụng Rails, bạn hiếm khi phải load những files của chính mình. Điều này là do gems được tìm thấy trong Gemfile của bạn đã được tự động required, và Rails tự xử lý và loading files cho bạn. Justin Weiss có một bài viết tuyệt vời về cách Rails xử lý việc loading tất cả gems được tìm thấy trong Gemfile của bạn.
Tổ chức code trong một Gem
Chúng ta hãy nhìn vào cách code được tổ chức trong một Ruby gem. Để minh chứng, tôi đã tạo ra một gem để làm một cái gì đó hoàn toàn không cần thiết trong Ruby: Thêm string vào bên trái hoặc bên phải một string.
Nó hoàn toàn không cần thiết trong Ruby vì không giống như JavaScript bản thân ngôn ngữ Ruby hoàn toàn có thể tự làm được nó với các methods:
rjust
vàljust
của class String.
Và đây là cách code được tổ chức:
├── Gemfile
├── Gemfile.lock
├── README.md
├── lib
│ ├── padder
│ │ ├── center.rb
│ │ ├── left.rb
│ │ ├── right.rb
│ │ └── version.rb
│ └── padder.rb
├── padder.gemspec
└── spec
├── center_spec.rb
├── left_spec.rb
├── right_spec.rb
└── spec_helper.rb
Nơi mà các mã code sống?
Thư viện code sống bên trong thư mục lib
trong phần lớn các trường hợp. Đường dẫn được add mặc định vào $LOAD_PATH
khi gem được kích hoạt. Bạn có thể ghi đè nó bằng cách thiết lập các tùy chọn require_paths
trong padder.gemspec
. Nó được bỏ qua trong gem này vì tôi đang dùng nó với các config mặc định.
Namespacing your gem
Gem của bạn cần phải có một namespace duy nhất (module) trong tất cả những chỗ mà code lives. Điều này là để trách xảy ra xung đột, va chạm namespace với những gem khác.
Thông thường bạn sẽ muốn khai báo module trong một file có cùng tên với gem của bạn bên trong thư mục lib
. Đối với gem này, chúng ta có một file với tên padder.rb
. Điều này là quan trọng bởi vì khi sử dụng trong Rails, nó sẽ nhìn vào đây để load những files trong gem của bạn.
# lib/padder.rb
module Padder
require 'padder/version'
require 'padder/left'
require 'padder/right'
require 'padder/center'
end
Một công việc khác của file này là require
(hoặc autoload
) các file khác cần thiết cho library để thực hiện công việc của mình. Những files đó thường được tìm thấy trong một thư mục có tên giống với tên module/gem.
Tôi muốn nhìn thấy những gì sẽ xảy ra với mảng $LOADED_FEATURES
, vì vậy bên trong của file left_spec.rb
tôi đã đặt vào đó một đoạn mã code nhìn vào mảng này trước và sau khi require method được gọi để chắc chắn rằng mọi thứ đang làm việc đúng như mong đợi.
before = $LOADED_FEATURES.dup
require 'padder'
($LOADED_FEATURES - before).each { |str| puts str }
Ta có những kết quả khác biệt
/Users/leigh/padder/lib/padder/version.rb
/Users/leigh/padder/lib/padder/left.rb
/Users/leigh/padder/lib/padder/right.rb
/Users/leigh/padder/lib/padder/center.rb
/Users/leigh/padder/lib/padder.rb
Và chúng ta hãy nhìn vào mảng $LOAD_PATH
lọc ra những entries đến từ các gem khác chúng ta sẽ thu được điều này:
/Users/leigh/padder/spec
/Users/leigh/padder/lib
Nếu chúng ta nhìn vào một trong những classes được tìm thấy trong gem, ví dụ class Padder::Left
trông nó sẽ như thế này:
module Padder
class Left
def self.pad(str, length, char)
str = str.to_s
return str if str.length >= length
"#{char * length}#{str}".slice(-length, length)
end
end
end
Nhìn vào thư mục lib
Rất nhiều dự án cực kì phổ biến cũng bắt đầu bằng một file trong thư mục lib, với tên giống với gem và có namespace/module nơi mà tất cả mã code sống ở đó. Hãy cùng nhìn vào một số gems nổi tiếng đã tổ chức code của họ một cách tuyệt vời để học thêm các khái niệm cũng như kỹ thuật mới.
Sidekiq
Sidekiq chỉ có một file duy nhất là sidekiq.rb
trong thư mục này. Công việc của nó là requiring tất cả các helper classes, các phụ thuộc của nó, các thiết lập mặc định, và cung cấp một loạt các phương thức helper làm việc với logging và configuration.
Nếu nhìn kỹ thậm chí bạn còn thấy một phương thức được gọi là quả trứng phục sinh:
def self.❨╯°□°❩╯︵┻━┻
puts "Calm down, yo."
end
Capybara
Capybara sử dụng file đầu tiên này không chỉ để require những phụ thuộc của nó như Sidekiq, mà nó còn định nghĩa một loạt các classes trống để sử dụng cho những trường hợp ngoại lệ (exceptions).
module Capybara
# ...
class CapybaraError < StandardError; end
class DriverNotFoundError < CapybaraError; end
class FrozenInTime < CapybaraError; end
class ElementNotFound < CapybaraError; end
# ...
end
Rack
Rack sử dụng không gian đó để định nghĩa những thứ như là hằng số sử dụng trong thư viện. Nhưng thay vì requiring những files với các require
method, thư viện này đã lựa chọn sử dụng phương thức autoload
. Đây là kiểu lazy-load
, tải các tập tin cần thiết khi module/class được tham chiếu cho lần đầu tiên.
module Rack
# ...
GET = 'GET'.freeze
POST = 'POST'.freeze
PUT = 'PUT'.freeze
PATCH = 'PATCH'.freeze
# ...
autoload :Builder, "rack/builder"
autoload :BodyProxy, "rack/body_proxy"
autoload :Cascade, "rack/cascade"
# ...
end
Rake
Rake có một file ban đầu rất thuần hóa, đơn giản chỉ là định nghĩa Rake
module cùng với phiên bản và requires các files được sử dụng bởi thư viện này.
module Rake
VERSION = '11.1.2'
end
# ...
require 'rake/linked_list'
require 'rake/cpu_counter'
require 'rake/scope'
# ...
Cùng nhìn sâu hơn vào thư mục lib
Một khi chúng ta bước vào các thư mụclib/rack
, lib/rspec
, lib/capybara
, và lib/sidekiq
, chúng ra đã chạy vào một loạt các files. Thông thường chúng ta chỉ có một class duy nhất (và tất cả đều sống chung dưới một module chính), hoặc thêm một bộ các thư mục, chúng sẽ phân chia các mã code thành các module chuyên biệt hơn để xử lý các phần cụ thể các chức năng có trong thư viện.
Kết luận
Hôm nay chúng ta đã nhìn vào một số những Ruby gem phổ biến để xem cách mà họ đã chọn để tổ chức code của họ. Và như bạn đã thấy họ đều chọn theo một cách giống nhau. Một số chọn để có một module loaded thông qua phương thức autoload
trong khi đó một số gem khác sử dụng require
để load những tập tin ngay từ đầu.
Một điều mà các gem đều như nhau: Gem có trách nhiệm loading code của chính nó, và bạn là người sử dụng gem mà không cần phải lo lắng về chúng.
All rights reserved