5 method trong Ruby mà bạn nên dùng

Object#tap

Vào một ngày đẹp trời, bạn implement code cho function login bằng Omniauth, class Use cần method như sau:

def self.from_omniauth auth
	user = find_or_initialize_by email: auth.info.email
	user.name = auth.info.name
	user.provider = auth.provider
	user.password = User.generate_unique_secure_token if user.new_record?
	user.save
	user
end

Bạn tưởng chừng đến đây là xong, nhưng trước lúc gửi pull request, bạn chạy reek và nhận ngay trái đắng:

User#from_omniauth has approx 6 statements [https://github.com/troessner/reek/blob/master/docs/Too-Many-Statements.md]

Quá nhiều statements cho một method. Và vấn đề của bạn là làm sao có thể giảm bớt số statements lại. Đây là nơi cho tap thể hiện sức mạnh. Refactor lại code bằng tap:

def from_omniauth auth
  User.find_or_initialize_by(email: auth.info.email).tap do |user|
    user.name = auth.info.name
    user.provider = auth.provider
    user.password = User.generate_unique_secure_token if user.new_record?
    user.save
  end
end

Sẽ chằng còn một lỗi reek nào nữa. Để biết nơi nào cần sử dụng tap, chúng ta chỉ cần để ý:

Method nào thay đổi các attributes của object nhưng lại không trả về object đó.

Như ví dụ trên, method user.save không trả về user mà chúng ta muốn, nó chỉ trả về true hoặc false. Vậy nên chúng ta cần sử dụng tap

Array#bsearch

Có rất nhiều hàm tìm kiếm trong Ruby mà chúng ta có thể sử dụng trên Array như: select, 'reject', 'find', nhưng khi mà Array quá lớn, chúng ta phải bắt đầu chú ý đến thời gian thực thi của các hàm trên. Ví dụ sử dụng find, nó sẽ tìm trên toàn bộ array, đến lúc nào tìm thấy thì thôi. Độ phức tạp O(n). Tuy nhiên có một cách nhanh hơn, độ phức tạp O(log n):

require "benchmark"

data = (0..50_000_000)

Benchmark.bm do |x|
  x.report(:find) { data.find {|number| number > 40_000_000 } }
  x.report(:bsearch) { data.bsearch {|number| number > 40_000_000 } }
end

         user       system     total       real
find     3.020000   0.010000   3.030000   (3.028417)
bsearch  0.000000   0.000000   0.000000   (0.000006)

Như bạn có thể thấy, bsearch nhanh hơn rất nhiều. Tuy nhiên bsearch có một nhược điểm đó là: array cần phải được sort trước khi tìm kiếm. Tuy nhiên cũng có khá nhiều trường hợp phải dùng đến nó. Ví dụ như tìm kiếm created_at chẳng bạn.

Enumerable#flat_map

Tưởng tượng, bạn phải implement một method. Bạn có danh sách post được viết vào tháng trước bởi các users, tìm những người đã comment trên các post đó. Chúng ta implement method đó như sau.

module CommentFinder
  def self.find_for_users(user_ids)
    users = User.where(id: user_ids)
    users.map do |user|
      user.posts.map do |post|
        post.comments.map |comment|
          comment.author.username
        end
      end
    end
  end
end

Kết quả trả về như sau:

[[['Ben', 'Sam', 'David'], ['Keith']], [[], [nil]], [['Chris'], []]]

Nhưng kết quả mà bạn mong muốn chỉ là tên các user thôi. Đây là nơi bạn cần sử dụng flatten

module CommentFinder
  def self.find_for_users(user_ids)
    users = User.where(id: user_ids)
    users.map { |user|
      user.posts.map { |post|
        post.comments.map { |comment|
          comment.author.username
        }.flatten
      }.flatten
    }.flatten
  end
end

Hoặc bạn cũng có thể sử dụng flat_map:

module CommentFinder
  def self.find_for_users(user_ids)
    users = User.where(id: user_ids)
    users.flat_map { |user|
      user.posts.flat_map { |post|
        post.comments.flat_map { |comment|
          comment.author.username
        }
      }
    }
  end
end

Array#new with a Block

Tưởng tượng, bạn cần một array có 1000 phần từ nil, có nhiều cách để làm nhưng cách làm đơn giản nhất là:

Array.new(1000) {nil}

<=>

Đây là method có thể chúng ta ít sử dụng trong Ruby nhưng trong các class được Ruby 'built-in' sẵn thì lại được sử dụng rất nhiều. Đây là cách nó làm việc:

4 <=> 4 # 0
5 <=> 4 # 1
4 <=> 5 # -1

Các class khi muốn include Comparable module, bạn cần implement method này. Tưởng tượng, bạn cần implement một method cho phép cộng trừ thời gian bằng phút và giờ. Việc này sẽ trở nên rất phức tạp nếu con số bạn muốn cộng trừ lớn hơn 60 minutes. Khi thời gian muốn trừ lớn hơn 60 minutes, bạn cần trừ đi 1 hour và trừ đi 60 minutes

def fix_minutes minutes
    until (0...60).member? minutes
      @hours -= 60 <=> minutes
      @minutes += 60 * (60 <=> minutes)
    end
    @hours %= 24
    self
end

Sử dụng <=> chúng ta có thể implement một function phức tạp bằng đoạn code rất ngắn. Less code less bugs

Happy Coding!