Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ปลดล็อกโค้ด Ruby ให้เร็วขึ้น: 4 รูปแบบ Memoization ที่พิสูจน์แล้ว + 1 Gem ที่จำเป็น

Memoization เป็นเทคนิคเร่งความเร็ว accessor methods โดย cache ผลลัพธ์ของ method ที่ทำงานหนัก ต้องทำแค่ครั้งเดียว ใน Rails ใช้ memoization บ่อยจนมี module สำหรับ memoize ให้

ต่อมา ถูกลบออกอย่างก่อกวน เพื่อใช้ pattern พื้นฐานที่พบบ่อย แตจริงๆ pattern พื้นฐานนี้ไม่ได้ทำงานในบางกรณี เราจะดู patterns 進階 และเรียนรู้ Ruby อย่างลึก

Memoization พื้นฐานสุดๆ

Pattern นี้เจอตลอดใน Ruby:

app/models/order.rb
class User < ActiveRecord::Base
  def twitter_followers
    # assuming twitter_user.followers makes a network call
    @twitter_followers ||= twitter_user.followers
  end
end

||= แปลว่า @twitter_followers = @twitter_followers || twitter_user.followers เรียก network call ครั้งแรกเท่านั้น ครั้งต่อไปคืนค่า instance variable @twitter_followers

Memoization หลายบรรทัด

บางครั้งโค้ดช้าไม่เข้าในบรรทัดเดียวโดยไม่ทำให้มันแย่ มีหลายวิธีขยาย pattern พื้นฐาน แต่วิธีโปรดของผม:

app/models/order.rb
class User < ActiveRecord::Base
  def main_address
    @main_address ||= begin
      maybe_main_address = home_address if prefers_home_address?
      maybe_main_address = work_address unless maybe_main_address
      maybe_main_address = addresses.first unless maybe_main_address
    end
  end
end

begin...end สร้าง block ใน Ruby ให้ถือเป็นสิ่งเดียว คล้าย {...} ใน C-style 所以 ||= ทำงานได้เหมือนเดิม

ถ้าเป็น nil ล่ะ?

Patterns นี้มีปัญหาแย่ซ่อนอยู่ ในตัวอย่างแรก ถ้า user ไม่มี twitter และ API คืน nil? ตัวที่สอง ถ้าไม่มี addresses และ block คืน nil?

ทุกครั้งที่เรียก method instance variable จะเป็น nil และทำ expensive fetches ซ้ำ

所以 ||= ไม่ถูกต้อง ต้องแยก nil และ undefined:

app/models/order.rb
class User < ActiveRecord::Base
  def twitter_followers
    return @twitter_followers if defined? @twitter_followers
    @twitter_followers = twitter_user.followers
  end
end
app/models/order.rb
class User < ActiveRecord::Base
  def main_address
    return @main_address if defined? @main_address
    @main_address = begin
      main_address = home_address if prefers_home_address?
      main_address ||= work_address
      main_address ||= addresses.first # some semi-sensible default
    end
  end
end

ดู醜หน่อย แตทำงานกับ nil, false และทุกอย่าง (อาจใช้ Null Objects และ empty arrays หลีกเลี่ยง nil ได้ อีกเหตุผลหลีกเลี่ยง nil!)

ถ้ามี parameters ล่ะ?

มี patterns สำหรับ accessors เรียบร้อย แตถ้าอยาก memoize method ที่รับ parameters เหมือนนี้ล่ะ?

app/models/city.rb
class City < ActiveRecord::Base
  def self.top_cities(order_by)
    where(top_city: true).order(order_by).to_a
  end
end

โชคดีที่ Ruby Hash มี initializer ทำงาน สมบูรณ์ สำหรับนี้ เรียก Hash.new กับ block:

Hash.new {|h, key| h[key] = some_calculated_value }

ทุกครั้งที่เข้าถึง key ที่ยังไม่ set จะรัน block ส่ง hash และ key เข้าไป

所以 ถ้าอยาก memoize method นี้ ทำได้แบบนี้:

app/models/city.rb
class City < ActiveRecord::Base
  def self.top_cities(order_by)
    @top_cities ||= Hash.new do |h, key|
      h[key] = where(top_city: true).order(key).to_a
    end
    @top_cities[order_by]
  end
end

ไม่ว่าจะส่ง order_by อะไร ผลลัพธ์ที่ถูกต้องจะถูก memoize เนื่องจาก block เรียกตอน key ไม่มี จึงไม่ต้องกังวลผลลัพธ์เป็น nil หรือ false

น่าประหลาดใจ Hash ทำงานได้ดีกับ keys ที่เป็น arrays:

h = {}
h[["a", "b"]] = "c"
h[["a", "b"]] # => "c"

所以 ใช้ pattern นี้ได้กับ method ที่มี parameters กี่ตัวก็ได้!

ทำไมต้องยุ่งยากขนาดนี้?

เพิ่ม patterns นี้เยอะ โค้ดจะอ่านยาก ทุก method จะเป็นพิธีกรรมไม่มีเนื้อหา

ถ้า app ต้องการ memoization เยอะ ควรใช้ gem จัดการด้วย API ใช้งานง่าย Memoist ดูดี คล้ายที่ Rails เคยมี (หรือ用ความรู้ memoization ใหม่สร้างเองก็ได้)

แต่สืบสวน patterns นี้น่าสนใจ ดูโครงสร้าง จุดทำงาน จุดคม และเรียนรู้ Ruby features ที่น้อยคนรู้