Ways to write better Ruby
Bài đăng này đã không được cập nhật trong 3 năm
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à nil
và 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 $10
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.
Tài liệu tham khảo
https://codebrahma.com/ruby-decorators/
https://play.google.com/store/books/details/Peter_J_Jones_Effective_Ruby?id=PvNzBAAAQBAJ
All rights reserved