Ways to write better Ruby
Trong quá trình tiếp xúc và làm việc với ngôn ngữ Ruby chắc hẳn ai trong chúng ta cũng cảm nhận được sự tinh gọn của ngôn ngữ này. Ruby cung cấp rất nhiều những hàm tiện ích nhưng đôi khi chúng sẽ khiến chúng ta phân vân, liệu dùng như vậy đã thực sự tối ưu hay chưa ? hay đơn cử là việc sử dụng ...
Trong quá trình tiếp xúc và làm việc với ngôn ngữ Ruby chắc hẳn ai trong chúng ta cũng cảm nhận được sự tinh gọn của ngôn ngữ này. Ruby cung cấp rất nhiều những hàm tiện ích nhưng đôi khi chúng sẽ khiến chúng ta phân vân, liệu dùng như vậy đã thực sự tối ưu hay chưa ? hay đơn cử là việc sử dụng standard library của ruby như thê nào cho đúng.
1. Sử dụng set để tăng hiệu suất
Cũng giống như Array và Hash, Set là 1 thư viện của Ruby, tuy nhiên nó ko được required default giống như Array hay Hash. Set là một tập hợp trong đó các phần tử của nó ko trùng nhau (unique). Trước khi tìm hiểu vì sao nên sử dụng set, hãy nhìn vào ví dụ dưới đây:
require "benchmark/bigo" require "set" Benchmark.bigo do |x| x.generator {|size| array = (0..size).to_a.shuffle { :array => array, :set => Set.new(array), } } x.steps = 10 x.step_size = 20 x.min_size = 1 x.report("Array#include?") { |data, size| data.fetch(:array).include?(rand(size)) } x.report("Set#include?") { |data, size| data.fetch(:set).include?(rand(size)) } x.chart! 'chart_array.html' end
Kết quả thu được trên đồ thị:
Ở đây chúng ta dễ dàng nhận ra thời gian để look up các elements khi dùng Array lâu hơn khi dùng Set. Vậy nguyên nhân ở đây là gì ?
Đối với method Array#include?(5), con trỏ sẽ lần lượt trỏ đến các vùng ô nhớ, kiểm tra và trả về giá trị true/false.
Với Set thì khác, trước khi một phần tử được insert vào collection, nó sẽ chạy qua một Hash function, Hash function này sẽ chỉ định address memory cho phần tử đó.
Khi gọi method Set#include? request sẽ gọi đến hash function này và nó sẽ look up đến address location và trả ra kết quả tương ứng.
2. Sử dụng default hash value
Trong Ruby một hash sẽ mặc định trả về nil trong trường hợp keys không tồn tại. Tuy nhiên không phải lúc nào chúng ta cũng mong muốn giá trị mặc định là nilvà chúng ta muốn thay đổi giá trị default này. Cách đơn giản nhất để làm việc đó là
Hash.new(value)
Ở đây value là giá trị default mà chúng ta mong muốn trong trường hợp none-existent keys. Hoặc cũng có một cách khác, đó là :
Hash.new { |hash, key| ...}
Xét ví dụ dưới đây:
contents = "Checking the correct content" result = {} contents.split(" ").each do |word| result[word] ||= 0 result[word] += 1 end p result
Thay vào đó ta có thể viết lại đoạn code này và sử dụng hash defaul như sau:
result = Hash.new(0) contents.split(" ").each do |word| result[word] += 1 end
Kết quả ra giống nhau, điểm khác là nội dung bên trong vòng lặp đã được giản lược, chúng ta không cần quan tâm đến việc phải set giá trị defaul value nữa.
3. Duplication Collections
Trong Ruby #dup trả về một 'bản sao' của đối tượng cần thao tác, có nghĩa là bạn tạo ra một đối tượng mới mang đầy đủ tính chất của đối tượng ban đầu. Tại sao phải dùng #dup, chúng ta xét ví dụ dưới đây:
class ValidatesData def initialize(invalid_array) @invalid_array = invalid_array.dup raise ArgumentError.new("Array is not valid") unless array_valid? end def transform invalid_array.map { |x| x.upcase }.join(",") end private attr_reader :invalid_array def array_valid? invalid_array.all? { |x| String === x } end end array = ["string", "string", "string"] vca = ValidatesData.new(array) array << 1 p vca.transform
Nếu như không sử dụng .dup trong hàm initialize sẽ có lỗi xảy ra, bởi sau khi insert number vào trong array, method tranform sẽ bị lỗi do không hiểu method upcase cho number. Với dup bạn có thể thêm hoặc loại bỏ một hay nhiều phần tử ra ngoài tập hợp(collection) mà không ảnh hưởng đến tập hợp ban đầu (original collection).
4. Sử dụng Decorators
Decorators cho phép chúng ta chèn thêm behaviour cho các đối tượng mà không làm ảnh hưởng tới các đối tượng khác trong cùng class, nó cũng rất hữu ích cho việc tạo ra các subclasses. Xét ví dụ dưới đây: Hãy hình dung chúng ta có một class Hamberger bên trong là một method cost
class Hamberger def cost 50 end end
Và bây giờ cần thêm 1 loại Hamberger kẹp thêm phomat và giá đắt hơn $$0
class HambergerWithCheese < Hamberger def cost 60 end end
Hay một loại Hamberger có kích thước lớn hơn
class LargeHamberger < Hamberger def cost 65 end end
Với cách tiếp cận này số lượng class sẽ tăng gấp đôi (6 classes) nếu như kết hợp thêm cả khoai tây chiên hay một phụ gia nào khác.
Sẽ có ý tưởng sử dụng module thay vì khai báo quá nhiều class như hiện tại
module HambergerWithCheese def cost super + 10 end end module LargeHamberger def cost super + 15 end end
Bây giờ chỉ việc extend object bằng cách sử dụng các module trên
hamberger = Hamberger.new # cost = 50 hamberger.extend(HambergerWithCheese) # cost = 60 hamberger.extend(LargeHamberger) #cost = 65
Với cách này nếu mở rộng thêm cả set khoai tây chiên thay vì phải sử dụng 6 classes thì nay chỉ cần 1 class và 3 module.
Hãy xem xét bài toán khi sử dụng decorators
class LargeHamberger def initialize(hamberger) @hamberger = hamberger end def cost @hamberger.cost + 15 end end
Lúc này việc thêm mới 1 loại hamberger mới: extra_large_hamberger, ta chỉ việc
hamberger = Hamberger.new large_hamberger = LargeHamberger.new(hamberger) extra_large_hamberger = LargeHamberger.new(large_hamberger)
Tương tự ta cũng tạo một decorator cho đối tượng HambergerWithCheese.Như vậy chỉ cần 3 thay vì 6 classes như cách tiếp cận ban đầu.
Việc sử dụng decorators thay thế cho inherit sẽ giúp giảm bớt các subclasses cần xây dựng. Nó cũng được sử dụng để trích xuất logic tử một class phức tạp lên những classes nhỏ hơn.
Hi vọng những chia sẻ trên đây sẽ phần nào giúp các bạn trong việc cải thiện việc viết code ruby, dễ dàng maintain hơn, thích nghi được những thay đổi về sau. Bài viết sau mình sẽ đề cập đến vấn đề làm thế nào để sử dụng, khai thác tốt hơn các thư viện tiêu chuẩn (standard library) và các tính năng của Ruby nhằm đạt năng suất cao hơn.