0

Exploring the Structure of Ruby Gems

https://blog.codeship.com/exploring-structure-ruby-gems/

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ới require, nhưng nó bổ sung cho các method trong require 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: rjustljust 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí