12/08/2018, 13:28

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ị: chart.png

Ở đâ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. Untitled drawing.jpg

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ử đó.

Screenshot from 2016-05-25 23:02:41.png

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.

0