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

ปลดล็อค Ruby's Hidden Gems: Mastering Delegator & Forwardable

ในวันนี้การสำรวจ hidden gems ใน standard library ของ Ruby เราจะมาดูเรื่อง delegation

น่าเสียดายที่ term นี้ — เหมือนอีกมากมาย — กลายเป็น somewhat muddled ผ่าน السنوات และหมายถึงสิ่งต่างกันสำหรับคนต่างคน ตาม Wikipedia:

Delegation หมายถึงการ evaluate a member (property หรือ method) ของออบเจ็กต์หนึ่ง (the receiver) ในบริบทของอีกออบเจ็กต์หนึ่งที่เป็นต้นทาง (the sender) Delegation สามารถทำได้ explicitly โดยการส่ง sending object ไปยัง receiving object ซึ่งทำได้ในภาษา object-oriented ใดๆ หรือ implicitly โดย member lookup rules ของภาษา ซึ่งต้องการ language support สำหรับ feature นี้

อย่างไรก็ตาม บ่อยกว่านั้น คนใช้ term นี้เพื่ออธิบายออบเจ็กต์ที่เรียกเมธอดที่ตรงกันของอีกออบเจ็กต์โดยไม่ส่งตัวเองเป็น argument ซึ่งอธิบายได้แม่นยำกว่าว่า "forwarding"

ด้วยข้อที่กล่าวมา เราจะใช้ "delegation" เพื่ออธิบายทั้งสอง patterns นี้ต่อไปในบทความนี้

Delegator

มาเริ่มสำรวจ delegation ใน Ruby โดยดูที่คลาส Delegator ของ standard library ซึ่งให้ delegation patterns หลายแบบ

SimpleDelegator

ตัวที่ง่ายที่สุด และตัวที่ผมเจอบ่อยที่สุดในธรรมชาติ คือ SimpleDelegator ซึ่ง wrap ออบเจ็กต์ที่提供ผ่าน initializer และ delegate เมธอดที่หายไปทั้งหมดไปยังมัน มาดูใน action กัน

require 'delegate'
User = Struct.new(:first_name, :last_name)

class UserDecorator < SimpleDelegator
  def full_name
    "#{first_name} #{last_name}"
  end
end

ก่อนอื่น เราต้อง require 'delegate' เพื่อให้ SimpleDelegator ใช้งานได้ในโค้ดของเรา เรายังใช้ Struct สร้างคลาส User เรียบง่ายกับ accessors first_name และ last_name เราจึงเพิ่ม UserDecorator ซึ่ง define เมธอด full_name รวมส่วนชื่อแต่ละส่วนเป็น string เดียว นี่คือที่ที่ SimpleDelegator เข้ามาเล่น角色: เนื่องจากไม่ได้ define first_name หรือ last_name บนคลาสปัจจุบัน พวกมันจะถูกเรียกบนออบเจ็กต์ที่ wrapped แทน:

decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name #=> "John Doe"

SimpleDelegator ยังอนุญาตให้เรา override เมธอดที่ delegated ด้วย super เรียกเมธอดที่ตรงกันบนออบเจ็กต์ที่ wrapped เราสามารถใช้สิ่งนี้ในตัวอย่างเพื่อแสดงแค่ initial แทน first name เต็ม:

class UserDecorator < SimpleDelegator
  def first_name
    "#{super[0]}."
  end
end
decorated_user.first_name #=> "J."
decorated_user.full_name #=> "J. Doe"

Delegator

ขณะอ่านตัวอย่างข้างต้น คุณเคย wonder ไหมว่า UserDecorator ของเรารู้อย่างไรว่าจะ delegate ไปยังออบเจ็กต์ไหน? คำตอบอยู่ที่ parent class ของ SimpleDelegator — Delegator นี่คือ abstract base class สำหรับกำหนด custom delegation schemes โดย提供 implementations สำหรับ __getobj__ และ __setobj__ เพื่อ get และ set delegation target ตามลำดับ ด้วยความรู้นี้ เราสามารถสร้างเวอร์ชันของ SimpleDelegator เองเพื่อการสาธิตได้ง่าย:

class MyDelegator < Delegator
  attr_accessor :wrapped
  alias_method :__getobj__, :wrapped

  def initialize(obj)
    @wrapped = obj
  end
end

class UserDecorator < MyDelegator
  def full_name
    "#{first_name} #{last_name}"
  end
end

สิ่งนี้แตกต่างจาก implementation จริงของ SimpleDelegator เล็กน้อย ซึ่งเรียก __setobj__ ในเมธอด initialize ของมัน เนื่องจาก custom delegator class ของเราไม่ต้องการมัน เราจึงเว้นเมธอดนั้นไว้โดยสิ้นเชิง

สิ่งนี้ควรทำงานเหมือนตัวอย่างก่อนหน้าทุกประการ และจริงๆ แล้วมันก็ทำงาน:

UserDecorator.superclass #=> MyDelegator < Delegator
decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name #=> "John Doe"

DelegateMethod

Delegation pattern สุดท้ายที่ Delegate 提供ให้เราคือ Object.DelegateClass method ที่ named อย่างน่าสนใจ วิธีนี้ generate และส่งคืน delegator class สำหรับคลาสเฉพาะ ซึ่งเราสามารถ inherit จากมันได้:

class MyClass < DelegateClass(ClassToDelegateTo)
  def initialize
    super(obj_of_ClassToDelegateTo)
  end
end

แม้จะดู confusing ตอนแรก — โดยเฉพาะข้อเท็จจริงที่ว่า right-hand side ของ inheritance สามารถมี arbitrary Ruby code — แต่มันจริงๆ แล้ว follow patterns ที่เราสำรวจก่อนหน้านี้ คือ คล้ายกับการ inherit จาก SimpleDelegator

Standard library ของ Ruby ใช้ feature นี้เพื่อ define คลาส Tempfile ซึ่ง delegate ส่วนใหญ่ของงานไปยังคลาส File ในขณะที่ตั้งค่า special rules เกี่ยวกับ storage location และ file deletion เราสามารถใช้ mechanism เดียวกันเพื่อ setup คลาส Logfile แบบ custom ได้เช่นนี้:

class Logfile < DelegateClass(File)
  MODE = File::WRONLY | File::CREAT | File::APPEND

  def initialize(basename, logdir = '/var/log')
    # Create logfile in location specified by logdir
    path = File.join(logdir, basename)
    logfile = File.open(path, MODE, 0644)
    # นี้จะเรียก initialize method ของ Delegator ดังนั้นจากจุดนี้เป็นต้นไป
    # เราสามารถเรียกเมธอดใดๆ จาก File บน instances ของ Logfile ได้
    super(logfile)
  end
end

Forwardable

น่าสนใจพอสมควรที่ standard library ของ Ruby 提供 library อีกอันสำหรับ delegation ในรูปของโมดูล Forwardable และเมธอด def_delegator และ def_delegators ของมัน

มา rewrite ตัวอย่าง UserDecorator เดิมของเราด้วย Forwardable

require 'forwardable'
User = Struct.new(:first_name, :last_name)

class UserDecorator
  extend Forwardable
  def_delegators :@user, :first_name, :last_name

  def initialize(user)
    @user = user
  end

  def full_name
    "#{first_name} #{last_name}"
  end
end

decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name #=> "John Doe"

ความแตกต่างที่โดดเด่นที่สุดคือ delegation ไม่ได้ถูก提供อัตโนมัติผ่าน method_missing แต่ต้อง declare explicitly สำหรับแต่ละเมธอดที่เราต้องการ forward ซึ่งทำให้เรา "ซ่อน" เมธอดของออบเจ็กต์ที่ wrapped ที่เราไม่ต้องการ expose ให้ clients ได้ ซึ่งให้ control มากขึ้น over public interface และเป็นเหตุผลหลักที่ผมชอบ Forwardable มากกว่า SimpleDelegator

Feature ดีอีกอย่างของ Forwardable คือความสามารถในการ rename delegated methods ผ่าน def_delegator ซึ่งรับ optional third argument ที่ระบุ alias ที่ต้องการ:

class UserDecorator
  extend Forwardable
  def_delegator :@user, :first_name, :personal_name
  def_delegator :@user, :last_name, :family_name

  def initialize(user)
    @user = user
  end

  def full_name
    "#{personal_name} #{family_name}"
  end
end

UserDecorator ข้างต้น expose แค่ personal_name และ family_name ที่ aliased แล้ว ในขณะที่ยัง forward ไปยัง first_name และ last_name ของ User object ที่ wrapped:

decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.first_name #=> NoMethodError: undefined method `first_name' for #<UserDecorator:0x000000010f995cb8>
decorated_user.personal_name #=> "John"

Feature นี้มาใน handy บางครั้ง ผมเคยใช้มันสำหรับการ migrate code ระหว่าง libraries ที่มี interfaces คล้ายกันแตกต่างใน expectations เกี่ยวกับ method names

Outside the Standard Library

แม้จะมี delegation solutions ใน standard library แล้ว Ruby community ได้พัฒนา alternatives หลายตัวผ่านปีๆ มา เราจะสำรวจสองตัวต่อไป

delegate

ด้วยความนิยมของ Rails เมธอด delegate ของมันอาจจะเป็น delegation form ที่ใช้บ่อยที่สุดโดย Ruby developers นี่คือวิธีที่เราสามารถใช้เพื่อ rewrite UserDecorator เก่าๆ ของเรา:

# ใน Rails app จริง นี่น่าจะเป็น subclass ของ ApplicationRecord
User = Struct.new(:first_name, :last_name)

class UserDecorator
  attr_reader :user
  delegate :first_name, :last_name, to: :user

  def initialize(user)
    @user = user
  end

  def full_name
    "#{first_name} #{last_name}"
  end
end

decorated_user = UserDecorator.new(User.new("John", "Doe"))
decorated_user.full_name #=> "John Doe"

สิ่งนี้คล้าย Forwardable พอสมควร แต่เราไม่ต้องใช้ extend เนื่องจาก delegate ถูก define โดยตรงบน Module และดังนั้นพร้อมใช้งานในทุก class หรือ module body (ดีหรือแย่ คุณตัดสินใจ) อย่างไรก็ตาม delegate มี tricks น่าสนใจอยู่บ้าง ก่อนอื่นคือ option :prefix ซึ่งจะ prefix delegated method names ด้วยชื่อของออบเจ็กต์ที่เรา delegate ไป ดังนั้น

delegate :first_name, :last_name, to: :user, prefix: true

จะ generate เมธอด user_first_name และ user_last_name หรือเราสามารถ提供 custom prefix ได้:

delegate :first_name, :last_name, to: :user, prefix: :account

ตอนนี้เราสามารถเข้าถึงส่วนต่างๆ ของชื่อ user ผ่าน account_first_name และ account_last_name

Option น่าสนใจอีกอย่างของ delegate คือ option :allow_nil ถ้าออบเจ็กต์ที่เรา delegate ไปเป็น nil อยู่ — ตัวอย่างเช่นเพราะ unset ActiveRecord relation — เราปกติจะได้ NoMethodError:

decorated_user = UserDecorator.new(nil)
decorated_user.first_name #=> Module::DelegationError: UserDecorator#first_name delegated to @user.first_name, but @user is nil

อย่างไรก็ตาม ด้วย option :allow_nil การเรียกนี้จะ succeed และส่งคืน nil แทน:

class UserDecorator
  delegate :first_name, :last_name, to: :user, allow_nil: true
  ...
end

decorated_user = UserDecorator.new(nil)
decorated_user.first_name #=> nil

Casting

Delegation option สุดท้ายที่เราจะดูคือ gem Casting ของ Jim Gay ซึ่งอนุญาตให้ developers "delegate methods in Ruby and preserve self" นี่อาจเป็น closest to strict definition of delegation เพราะมันใช้ dynamic nature ของ Ruby เพื่อ temporarily rebind receiver ของ method call แทนจะเป็น:

UserDecorator.instance_method(:full_name).bind(user).call #=> "John Doe"

Aspect ที่น่าสนใจที่สุดคือ developers สามารถเพิ่ม behavior ให้ออบเจ็กต์ได้โดยไม่เปลี่ยน superclass hierarchies

require 'casting'
User = Struct.new(:first_name, :last_name)

module UserDecorator
  def full_name
    "#{first_name} #{last_name}"
  end
end

user = User.new("John", "Doe")
user.extend(Casting::Client)
user.delegate(:full_name, UserDecorator)

ที่นี่เรา extend user ด้วย Casting::Client ซึ่งให้เราเข้าถึงเมธอด delegate หรือเราสามารถใช้ include Casting::Client ภายในคลาส User เพื่อให้ความสามารถนี้กับทุก instances

เพิ่มเติม Casting 提供 options สำหรับเพิ่ม behaviors ชั่วคราวสำหรับ lifetime ของบล็อก หรือจนกว่าจะ remove เองอีกครั้ง เพื่อให้สิ่งนี้ทำงาน เราต้อง enable delegation ของ missing methods ก่อน:

user.delegate_missing_methods

เพื่อเพิ่ม behavior สำหรับระยะเวลาของบล็อกเดียว เราสามารถใช้ class method delegating ของ Casting:

Casting.delegating(user => UserDecorator) do
  user.full_name #=> "John Doe"
end
user.full_name # NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">

หรือเราสามารถเพิ่ม behavior จนกว่าจะเรียก uncast เอง:

user.cast_as(UserDecorator)
user.full_name #=> "John Doe"
user.uncast
# NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">

แม้จะซับซ้อนกว่า solutions อื่นๆ ที่นำเสนอ Casting 提供 control มากมาย และ Jim สาธิตการใช้งานต่างๆ และเพิ่มเติมในหนังสือ Clean Ruby ของเขา

Summary

Delegation และ method forwarding เป็น patterns ที่มีประโยชน์สำหรับการแบ่ง responsibilities ระหว่างออบเจ็กต์ที่เกี่ยวข้อง ใน plain Ruby projects ทั้ง Delegator และ Forwardable สามารถใช้ได้ ในขณะที่ Rails code มัก傾向ไปที่เมธอด delegate ของมัน สำหรับ maximum control บนสิ่งที่ delegated gem Casting เป็น excellent choice แม้จะซับซ้อนกว่า solutions อื่นๆ เล็กน้อย

Guest Author Michael Kohl มีความรักกับ Ruby มาตั้งแต่ปี 2003 เขายังชอบเขียนและพูดเกี่ยวกับภาษานี้ และ co-organizes Bangkok.rb และ RubyConf Thailand