Memoization เป็นเทคนิคเร่งความเร็ว accessor methods โดย cache ผลลัพธ์ของ method ที่ทำงานหนัก ต้องทำแค่ครั้งเดียว ใน Rails ใช้ memoization บ่อยจนมี module สำหรับ memoize ให้
ต่อมา ถูกลบออกอย่างก่อกวน เพื่อใช้ pattern พื้นฐานที่พบบ่อย แตจริงๆ pattern พื้นฐานนี้ไม่ได้ทำงานในบางกรณี เราจะดู patterns 進階 และเรียนรู้ Ruby อย่างลึก
Memoization พื้นฐานสุดๆ
Pattern นี้เจอตลอดใน Ruby:
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 พื้นฐาน แต่วิธีโปรดของผม:
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
endbegin...end สร้าง block ใน Ruby ให้ถือเป็นสิ่งเดียว คล้าย {...} ใน C-style 所以 ||= ทำงานได้เหมือนเดิม
ถ้าเป็น nil ล่ะ?
Patterns นี้มีปัญหาแย่ซ่อนอยู่ ในตัวอย่างแรก ถ้า user ไม่มี twitter และ API คืน nil? ตัวที่สอง ถ้าไม่มี addresses และ block คืน nil?
ทุกครั้งที่เรียก method instance variable จะเป็น nil และทำ expensive fetches ซ้ำ
所以 ||= ไม่ถูกต้อง ต้องแยก nil และ undefined:
class User < ActiveRecord::Base
def twitter_followers
return @twitter_followers if defined? @twitter_followers
@twitter_followers = twitter_user.followers
end
endclass 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 เหมือนนี้ล่ะ?
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 นี้ ทำได้แบบนี้:
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 ที่น้อยคนรู้