• בלוג
  • שלושה סוגים של Delegation

שלושה סוגים של Delegation

24/02/2019

טכניקה מאוד קלה לשיתוף קוד בין מחלקות נקראת Delegation (האצלה בעברית - ותודה לכל מי ששלח). הרעיון הוא שאם יש לנו קוד שצריך להיות מופעל משתי מחלקות - אז כדאי לכתוב את הקוד הזה במחלקה שלישית ולקרוא לה משתי המחלקות שלנו. בואו נראה שלוש דרכים מרכזיות לשימוש במנגנון זה.

כל דוגמאות הקוד בפוסט הן בשפת Ruby, אבל העקרונות תקפים לכל שפה שתבחרו.

1. הוצאת קוד למחלקה שלישית רק בשביל שיהיה מסודר

נניח שאנחנו כותבים משחק לוח ובמשחק הזה יש מחלקה לשחקן אנושי ומחלקה אחרת לשחקן מחשב. חלק מהלוגיקה של שתי המחלקות משותף וחלק אחר נפרד. נניח שיש לנו בשתי המחלקות פונקציה שמדפיסה הודעת פתיחה נחמדה למשחק.

במחלקה HumanPlayer הקוד עשוי להיראות כך:

class HumanPlayer
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def start_game
    puts "My name is #{self.name} and I'm gonna win"
  end
end

ובמחלקה AIPlayer הקוד עשוי להיראות כך:

class AIPlayer
  def start_game
    puts "My name is Bot and I'm gonna win"
  end
end

הפונקציה start_game נראית מאוד דומה בין שתי המחלקות. ההבדל היחיד בין המימושים הוא שם השחקן. במקרה כזה הדבר הכי קל לעשות הוא להוציא את הפונקציה הזאת מחוץ להקשר של המחלקה למקום אחר. אפשר לרשום אותה מחוץ לכל מחלקה כפונקציה גלובלית, או בשביל הסדר הטוב לרשום אותה במחלקה אחרת. בואו נראה איך נראית תוכנית מלאה שמגדירה מחלקה אחת לצורך הדפסת הטקסטים, ושתי מחלקות לשחקנים:

class I18n
  def start_game(name)
    "My name is #{name} and I'm gonna win"
  end
end

class HumanPlayer
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def start_game
    messages = I18n.new()
    puts messages.start_game(self.name)
  end
end

class AIPlayer
  def start_game
    messages = I18n.new()
    puts messages.start_game("Bot")
  end
end

שווה להוסיף שבמצב כזה הרבה פעמים אפשר לוותר על מחלקה ולהסתפק במודול או פונקציות סטטיות. אותו הקוד עם מודול יראה כך:

module I18n
  def self.start_game(name)
    "My name is #{name} and I'm gonna win"
  end
end

class HumanPlayer
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def start_game
    puts I18n.start_game(self.name)
  end
end

class AIPlayer
  def start_game
    puts I18n.start_game("Bot")
  end
end

p1 = HumanPlayer.new("John")
p2 = AIPlayer.new()

p1.start_game
p2.start_game

בגלל שאין מידע שצריך להישמר בין קריאות לפונקציה I18n.start_game אפשר להגדיר את המחלקה I18n בתור מחלקה רגילה וליצור ממנה אוביקט כל פעם שצריך לגשת לפונקציות שלה, או ללכת על מבנה של מודול ואז אפשר להפעיל את הפונקציה "ישירות" דרך המודול בלי שצריך ליצור אוביקט.

2. הוצאת קוד ומידע למחלקה שלישית

מצב קצת יותר מעניין קורה כשאנחנו כן רוצים לשמור מידע בין הפעלות של פונקציה ממחלקה שלישית. בואו נוסיף לשני השחקנים מנגנון שמירת נקודות, כך שכל פעם ששחקן מנצח הוא מקבל 10 נקודות. בשביל לשתף את מנגנון שמירת הנקודות נעדיף לכתוב מחלקה שלישית, למשל נקרא לה Score שתהיה אחראית על ניהול הנקודות. מימוש לדוגמא יכול להיראות כך:

class Score
  def initialize
    @score = 0
  end

  def game_over(win)
    if win
      @score += 10
    end
  end

  def to_s
    @score.to_s
  end
end

מחלקת הנקודות צריכה לשמור מידע בין קריאות שונות לפונקציה game_over. כל פעם שפונקציה זו נקראת הנקודות צריכות לעלות ב-10, ולכן אי אפשר להשתמש כאן בפונקציה סטטית. את Score נרצה ליצור בבנאי של כל אחת מהמחלקות שמייצגות את השחקנים וכך המחלקה תמשיך לחיות לאורך כל חיי השחקן:

class HumanPlayer
  attr_reader :name, :score

  def initialize(name)
    @score = Score.new()
    @name = name
  end

  def game_over(win)
    @score.game_over(win)
  end
end

p1 = HumanPlayer.new("John")
p1.game_over(true)
p1.game_over(false)
puts p1.score

במצב כזה נגיד שהמחלקה Score מוכלת במחלקה HumanPlayer ומהווה חלק מהותי ממנה. כל פעם שיוצרים אוביקט HumanPlayer יוצרים גם Score שמתאים לו, ושילווה אותו לאורך כל חיי האוביקט. אותו Score פנימי מקיים יחס של אחד לאחד עם אוביקט HumanPlayer ולא יקרה מצב ששני אוביקטים מהמחלקה HumanPlayer יחלקו את אותו אוביקט Score.

3. הוצאת קוד ומידע למחלקה שלישית משותפת

במחשבה שניה לגבי שמירת הנקודות יהיה נחמד אם יהיה לנו מקום אחד מסודר בו נשמרות כל הנקודות של כל השחקנים. כל שחקן יוכל לדווח שניצח בצירוף השם שלו, ובסוף נוכל לבדוק בלוח התוצאות למי יש הכי הרבה נצחונות. נעדכן את המחלקה Score ואת מחלקות השחקנים כדי לקבל את ההתנהגות החדשה.

תחילה המחלקה Score צריכה עכשיו לקבל עם כל דיווח על סיום משחק גם את שם השחקן שמדווח. זה יראה כך:


class Score
  def initialize
    @score = {}
    @score.default = 0
  end

  def game_over(name, win)
    if win
      @score[name] += 10
    end
  end

  def to_s
    @score.to_s
  end
end

השינוי השני קשור לקשר בין Score לבין HumanPlayer (או AIPlayer). במקום שמחלקת השחקן תיצור את Score עבור כל אוביקט, אנחנו משתמשים באוביקט Score אחד שמשותף לכל השחקנים השונים, בין אם הגיעו מ HumanPlayer או מ AIPlayer. זה אומר שב HumanPlayer אנחנו כבר לא יכולים ליצור את האוביקט בעצמנו - ונצטרך לקבל אותו מבחוץ. גם הקריאה ל game_over צריכה להשתנות ולכלול עכשיו גם את שם השחקן:


class HumanPlayer
  attr_reader :name, :score

  def initialize(name, score)
    @score = score
    @name = name
  end

  def game_over(win)
    @score.game_over(self.name, win)
  end

  def start_game
    puts I18n.start_game(self.name)
  end
end

שינוי דומה במחלקה AIPlayer יגרום לה להיראות כך:

class AIPlayer
  def initialize(score)
    @score = score
    @name = "Bot"
  end

  def game_over(win)
    @score.game_over(@name, win)
  end

  def start_game
    puts I18n.start_game(@name)
  end
end

והקוד הראשי של התוכנית יכול להיראות כך:

score = Score.new
p1 = HumanPlayer.new("John", score)
p2 = AIPlayer.new(score)

p1.start_game
p2.start_game

p1.game_over(true)
p1.game_over(true)
p2.game_over(true)
puts p1.score

הפעם אנחנו יוצרים את המחלקה המשותפת מבחוץ ומעבירים אותה לבנאי של כל אחד מהאוביקטים. במצב כזה כל אוביקט שחקן מחזיק Reference לאוביקט לוח התוצאות. לוח התוצאות משותף לכל השחקנים אבל הוא לא גלובאלי לתוכנית - ובהחלט אפשר לדמיין מצב שנחלק את השחקנים למשל לחדרים שונים, ובכל חדר יהיה לוח תוצאות אחר. בארכיטקטורה כזאת חשוב להקפיד לא "למחוק" את האוביקט המשותף לפני שמוודאים שכל השחקנים שהשתמשו בו כבר סיימו לשחק. בדוגמת הקוד שלנו אין מנגנון איפוס ללוח הנקודות, אבל קל לדמיין פונקציה שמאפסת את הנקודות בלוח, ואם פונקציה כזאת קיימת אז שימוש בה משחקן אחד יאפס את הנקודות של כל השחקנים.

כשאנחנו מסתכלים על קוד כזה טוב לחשוב על לוח התוצאות כאוביקט נפרד עם קיום עצמאי, שהשחקנים יכולים לבקש ממנו שירותים (למשל לבקש שירשום להם ניצחון או הפסד, או שיאפס את הלוח), אבל כל שחקן צריך לזכור שהוא לא היחיד בחדר וכל פעולה משפיעה גם על האחרים.

הבחירה בין שלושת הסוגים של Delegation תלויה בהקשר ובמשימה שלכם:

  1. אם אתם צריכים לשתף קוד בלבד בלי לזכור מידע - לכו על מחלקה סטטית או מודול.

  2. אם אתם צריכים לזכור מידע בין הפעלות, אבל לא צריכים לחבר בין מידע שהגיע מאוביקטים שונים - לכו על הכלה ישירה בין המחלקות ביחס של אחד לאחד.

  3. ואם אתם צריכים לזכור מידע בין פעולות, ומידע זה עשוי להיות מושפע מקלטים שהגיעו מאוביקטים שונים - צרו אוביקט Delegation חיצוני והעבירו את הקישור אליו כפרמטר לכל אחד מהאוביקטים שצריכים אותו.