שפת רובי – עוד קצת שטויות שכאלו / או: מדריך מזורז למפתחי ג\'אווה מנוסים

בואו נבחר כמה דוגמאות שיחשפו בפנינו צדדים נוספים של שפת רובי:


#1
p nil # nil

#2
puts nil.inspect # nil ; why not NilPointerException !?

#3
puts nil.to_s # empty line

#1

אנו מקלידים את הביטוי \"p nil\" – ביטוי שנראה קצת מוזר, אולי שגיאה.
בפועל, p x הוא קיצור ברובי לכתיבת puts x.inspect.
2 מתודות בסיסיות של המחלקה Object ברובי הן:
  1. to_s (המקבילה של ()toString) – מתודה המחזירה ייצוג של המחלקה כמחרוזת
  2. inspect – עוד מתודה מאוד דומה שמחזירה ייצוג של המחלקה כמחרוזת
מה ההבדל?
to_s נועדה יותר ל output למשתמש, בעוד inspect נועדה יותר לצרכים פנימיים – למשל debug.
ברוב המקרים inspect פשוט תקרא ל to_s, ורק לפעמים היא תציג ערך שונה.
למשל שימו לב להבדלים:

puts 1.to_s        # 1
puts 1.inspect # 1
puts \'1\'.to_s # 1
puts \'1\'.inspect # \"1\"

בעוד to_s מחזירה את אותו הערך, inspect – מספקת לי \"רמז\" על הטיפוס של המשתנה. למשל: העובדה ש \"1\" הוא בעצם מחרוזת  – בעזרת המירכאות שמסביב.

#2
טוב… אז p nil שקול בעצם להדפסה של inspect על nil, אבל בעצם – למה לא נזרקת לי Exception?
התשובה היא ש nil ברובי לא ממומש כ pointer ריק (או מקבילה מודרנית) אלא כ Null Object (אובייקט שמדמה ערך null-י – הקישור מוביל לפוסט בנושא). בעצם nil ברובי הוא אובייקט לכל דבר, כמו כמעט כל דבר אחר (למשל הערכים true ו false המיוצגים ע\"י מחלקות גלובליות בשם TrueClass ו FalseClass, בהתאמה).
#3
אז למה ש nil.to_s יחזיר לנו שורה ריקה?
אם זה לא ברור – חזרו שוב על מה שעשינו למעלה….

פוסט זה שייך לסדרה: רובי (Ruby) למפתחי ג\'אווה ותיקים

Special Literals

הנה כמה צורות כתיבה מקוצרות שעשויות להיות מבלבלות (אם אתם לא מכירים), או שימושיות (כאשר אתם כבר מכירים):

# large integer literal
puts 100_000 # 100000
puts 1_0_0_0 # 1000

# character literal
puts ?c # c
puts \'c\' # same. c

# hex literals
puts 0x292 # 658

# binary literals
# rwxrwxrwc
puts 0b100100100 # 292
puts 0b100100100.to_s(8) # 444

large integer literal
בכדי להקל על קריאה של מספרים גדולים – ניתן להכניס קו תחתי \"_\" בין הספרות – וזה עדיין מספר לכל דבר. צורה זו הופכת את המספר \"מאה אלף\" לקל לקריאה, ואפשר להשתמש בה להפוך את אלף להראות כמו… נחש?!

character literal
אין ברובי טיפוס כמו char בג\'אווה, אך ניתן להשתמש בקיצור של ? ואחריו סימן בכדי להכריז שמדובר בכוונה במחרוזת של תו אחד. האם \"??\" יעבוד? – נסו ותראו.

hex literal
ברור

binary literal
תחילית של 0b עם 0 ו 1 אחריה מצהירה שמדובר במספר בינארי. אם לדוגמה אנו רוצים להגדיר הרשאות בלינוקס – פורמט זה נוח יותר לעבודה.
שימו לב שהמספרים שאנו מכירים כקודים להרשאות (444, 777, וכו\') הם מספרים בבסיס 8, אותו ניתן להציג ברובי ע\"י to_s עם הבסיס הרצוי

השוואת ערכים

x = Object.new
if x
puts
\'yeah\'
end

בדומה לג\'אווהסקריפט, ניתן להשתמש במשפט if כדרך מקוצרת לדעת אם למשתנה יש ערך.
רק שימו לב שהערכים הבאים הם evaluated כ true בשפת רובי:

  • מערך ריק
  • המספר 0 (אפס) – בשונה מרוב שפות התכנות
  • מחרוזת ריקה 

בקיצור: ברובי הכל evaluated כ true, מלבד false ו nil. כאשר רוצים להבחין בין false ו nil – עלינו להשתמש במתודה ?nil

השמה קלה

עוד תרגיל תחבירי שדומה לג\'אווהסקירפט היא בדיקה מקוצרת עם ערך הוא nil – והשמת ערך ברירת-מחדל במקום:

data = {}


# long
if data[:currency].nil?
currency
= \'USD\'
else
currency = data[:currency]
end
puts currency # USD


# short
currency = data[:currency] || \'USD\'
puts currency # USD

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

התחביר המקוצר – נחמד בהרבה.

ברובי יש תחביר אפילו יותר מקוצר, שמשתמשים בו לא-מעט, בכדי לקצר מקרה של הגנה והשמה לאותו משתנה עם ערך ברירת-מחדל:

currency = nil

# short
currency = currency || \'USD\'
puts currency # USD

# even-shorter
currency ||= \'USD\'
puts currency # USD

סבבה!

עוד תכונה \"מקצרת\" היא שמשפטי if מחזירים בעצמם ערכים (השורה האחרונה בביטוי):

def born_in_the_USA?
false
end

data[:currency] = if born_in_the_USA?
@@usa_citizens += 1
\'USD\'
else
\'ruble\'
end

puts data[:currency] # \'ruble\'

הערה קטנה: שימו לב שברובי אין operators של ++ או –. משתמשים ב 1 =+.

ומה עם הקיצור ל if-else שאנו מכירים מג\'אווה בצורת \"?\" – אי אפשר להשתמש בו ברובי?

# shorthand
data[:currency] = born_in_the_USA? ? \'USD\' : \'ruble\'

puts data[:currency] # \'ruble\'

אפשר!

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

def name_number(number)
case number
when 0
\'nullus\'
when 1
\'uno\'
when 2..10000
\'other number\'
when /\\d+\\$/
\"that\'s money\"
else
\"don\'t know\"
end
end

puts name_number 0 # nullus
puts name_number 1 # uno
puts name_number 7 # other number
puts name_number \'2$\' # that\'s money
puts name_number \'google\' # don\'t know

כמה הבדלים:

  • משתמשים במילה \"when\" ולא \"switch\" (כל הכבוד!)
  • ניתן להשתמש בטווחים (כמו בפאסקל – אם אני זוכר נכון)
  • ניתן להשתמש ב regex
  • ניתן לערבב בין כולם באותו ה case

שימו לב שהביטוי האחרון בפונקציה הוא ערך ההחזרה שלה, ובמקרה שלנו זהו ה case statement.

ארגומנטים לפונקציה

הנה נושא פשוט… לכאורה.
כיצד ניתן להגדיר ברובי פרמטרים של פונקציה, וכיצד ניתן להעביר ארגומנטים?
מינוח:

  • פרמטרים הם המשתנים בחתימה של הפונקציה שהגדרנו
  • ארגומנט הם המשתנים שהעברנו בהפעלה של הפונקציה

נניח לדוגמה את קיום פונקציה foo הבאה:

def foo(a, b, c)
@@y = a + b * c
end

כאשר a, b, c הם פרמטרים, שכיוון שזו שפה דינאמית – לא מגדירים את הטיפוסים שלהם.

אם נקרא לפונקציה עם ארגומנט יחיד

foo(3)

נקבל שגיאה: \"Argument Error: wrong number of arguments\" עם כל הדינאמיות, רובי מצפה שכל הארגומנטים יישלחו (בניגוד לג\'אווהסקריפט, למשל, שתציב undefined בפרמטרים להם לא נשלחו ארגומנטים).

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

def foo(a, b = 2, c = 5)
@@y = a + b * c
end

foo 3
puts @@y # 15

מצב \"מעצבן\"[א] הוא בו אני רוצה לשלוח ערכים רק ל a ו c, אך \"נאלץ\" לשלוח גם ערך ל b כי הוא קודם בסדר ל c. למשל:

def goo(message, warning = false, log_externally = false)
message
+= \'!!!!!\' if warning
puts message
send_log message
if log_externally
end

goo \'hello\', nil ,true # sending nil = sort of Annoying

(עברתי מ foo ל goo כדי לייצר דוגמה יותר ריאליסטית)

דרך אחת לפתור מצב זה הוא שימוש ב options hash:

def goo(message, options = {})
message
+= \'!!!!!\' if options[:warning]
puts message
send_log message
if options[:log_externally]
end

goo \'hello\', { log_externally: true }

החיסרון בדרך זו היא הצורך בתיעוד – מה בעצם options אומר.

הו לא! זה לא מספיק טוב! לא עבור רובי – חובה למצוא פתרון פשוט יותר!

רובי 2.0 הציגה יכולת שנקראת Keyword Arguments, שבאה פעם אחת ולתמיד לפתור את המצב הלא-נוח שתואר עד כה.

def goo(message, warning: false, log_externally: false)
message
+= \'!!!!!\' if warning
puts message
send_log message
if log_externally
end

goo \'hello\', log_externally: true

אין מחויבות על סדר הארגומנטים שנשלחים עם keyword – ממש כמו hash, אבל יש לנו \"תיעוד\" מובנה בחתימה של הפונקציה.

צילומסך מ Tutorial מפורסם של רובי (Ruby Bits) בו המרצה קופץ על המסך כדי לקרוא סדרה של קריאות התפעלות מיכולת ה keywoard arguments.
הסימן 1UP בא לסמן קוד שהוא \"!Awesome\", ופה בדיוק הופיעו חמישה כאלו ברציפות.

מה בכל זאת מפריע לי ב keyword arguments של רובי?

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

שפת רובי היא דיי מורכבת (יש שיאמרו גמישה) מבחינה תחבירית. יש הרבה \"מבנים\" אפשריים של קוד. כל החלפה של סימן – יכולה ליצור משמעות חדשה.

כדי לפענח את קוד הרובי (parsing), לא כתבו parser בצורה ידנית. במקום זאת, הגדירו את סט המבנים האפשרי בשפה (להלן parser.y) ואז בעזרת generator של parsers בשם Bison (וריאציה של YaCC[ב]) – מקמפלים parser שמפענח את קוד הרובי – ניתן למצוא אותו כקובץ בשם parse.c.
לא תמצאו את הקובץ הזה ב Git Repository של רובי, מכיוון שהוא נבנה בזמן ההתקנה עבור המעבד / מערכת ההפעלה הספציפית.

אצלי (חלונות 7, 64 ביט, רובי 1.9) גודל הקובץ הוא כחצי מגה או 17,000 שורות מורכבות של קוד. לא משהו שמישהו היה כותב בעצמו. שפת PHP, למשל, משתמשת בגישה דומה.

החל מרובי 1.9, מפרשן ברירת-המחדל הפך להיות YARV (קיצור של Yet another Ruby VM[ג]).
בעת הפעלת התוכנה YARV \"מקמפל\" את התוכנה ממש לפני ההרצה לשפה בסיסית יותר – הנקראת YARV instructions. העקרון דומה ל JIT Compiler של ג\'אווה, חוץ מזה שבג\'אווה מקמפלים משפת ByteCode לשפת מכונה, וברובי מ עץ Syntax שפוענח – לשפת \"ByteCode\" (ה YARV instructions) שתעבור אינטרפרטציה מעתה תוך כדי ריצה.

מיותר לציין שהמעבר מ MRI ל YARV מציג שיפור ביצועים משמעותי מאוד לקוד רובי.

הנה דוגמה לפענוח של קוד רובי ל YARV instructions, שמציגה \"על הדרך\", עוד צורה להגדיר פרמטרים ברובי – args* – המקבילה הישירה (והקצת-יותר-גמישה) של ה varags בג\'אווה. למה יותר גמישה? כי רובי לא תחייב את המתכנת להציב את ה varargs דווקא בסוף רשימת הפרמטרים רחמנא ליצלן! אולי יותר נוח / אלגנטי עבורו לשים אותה דווקא באמצע?

לכל scope בשפה, רובי מנהלת בזיכרון Local Table (מזכיר במשהו את ה Activation Frames של שפת ++C) שם מנוהלים המשתנים המקומיים / פרמטרים של הפונקציה או הבלוק.

הטבלה המקומית מגדירה את הערכים, עם הטיפוסים שלהם. בכדי לאפשר args* (טיפוס = Rest) באמצע רשימת הארגומנטים, עלינו להבחין בין ארגומנטים שנשלחו לפני (טיפוס = Arg), לארגומנטים שמגיעים אחרי (טיפוס = Post). המפרשן של רובי יקרא את הארגומנטים שלפני ואחרי, ומה שיישאר – ילך ל args*.

שימו לב ל YARV instructions משמאל – זהו בעצם הקוד המקומפל (סוג של Bytecode) אותו המפרשן מפענח ומריץ בזמן הרצת התוכנית.

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

ועכשיו איך נראית אותה הפונקציה, עם Keyword Argument:

מ-ס-ו-ב-ך.

נכון: מה שהסתבך הוא ה byte code. לא משהו שמפריע לפריון של המפתח. אבל… בתוכנה אין ארוחות חינם[ד].
byte code מסובך יותר משפיע על:

  • ביצועים (קצת)
  • יציבות / אמינות (קצת)
  • קלות בביצוע debug (קצת)
  • קושי לבצע שינויים משמעותיים בפלטפורמה – לדוגמה שיפור התמיכה ב parallelism וב concurrency.
שפת Go, למשל, (שאני מאוד מחבב) – החליטה לוותר על Generics בשפה בכדי לשמור על קומפילציה מהירה.
ברור ששפת Go (שהיא שפה ל System Programming) היא בערך ההיפך הגמור משפת רובי (שפת high level שמתמחה במהירות פיתוח גבוהה) – ולכן הגיוני שההחלטות שלה בתחום יהיו הפוכות לגמרי לאלו של רובי.
קרוב לוודאי שאלו גם קשיי הסתגלות אישיים שלי, לשפה חדשה ולתפיסות העולם השונות שלה.
רוב חיי כתבתי בשפות \"נמוכות\" יותר (C, פאסקל, עד #C ג\'אווה) או כאלו שקידשו פחות את הקריאות ונוחות המשתמש (ג\'אווהסקריפט).
התרגלתי לכך שהקוד שלי לעולם לא יהיה \"מ-ו-ש-ל-ם\", ולמרות חוסר הכיף שבכך, לפעמים עושים בקוד כמה \"צעדים טכניים\" שהם חלק ממגבלות השפה. למשל: לשלוח nil כמה פעמים כארגומנט – כי אין משמעות לפרמטר בשימוש הנוכחי. התרגלתי וזה נראה לי בסדר.

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

למשל, בואו נתבונן על כלי קצת פחות מוכר בשפת רובי: ה Flip Flop Operator (בקיצור FFO):

(1..20).each do |x|
puts x if (x == 5) .. (x == 10)
end

FFO הוא הנקודתיים בין 2 תנאי ה if. משמעותו: קיים את התנאי כל עוד התנאי הראשון מתקיים עד הרגע בו התנאי השני מפסיק להתקיים. את הקוד הנ\"ל אפשר לכתוב בעזרת \"קטן מ…\" ו\"גדול מ…\". האם שיפור הקריאות מצדיק הוספה של אופרטור נוסף לשפה?

הנה דוגמה שקצת יותר מצדיקה שימוש באופרטור שכזה… איתור מקטעים (flip..flop) ברצף:

[3,5,5,19,100,10,1,0,10,2].each do |x|
puts x if (x == 5) .. (x == 10)
end

# result => 5, 5, 19, 100, 10

נחמד – אבל האמת: כמה פעמים נתקלתם בבעיה שכזו בחייכם? האם נכון להוסיף רכיב לשפה בכדי לספק פתרון אלגנטי?

הייתה כבר בקשה רשמית להסיר את האופרטור מהשפה, אך הבקשה לא נכללה בתכולה של גרסה 2.0. מאטצ\' הוסיף שלא יציג שינויים לא תואמים לשפה ברובי 2 – אולי זה יקרה ברובי 3…

סיכום

אני מקווה שהיה דיון מעניין, והצלחתי להעביר בצורה יעילה כמה מהתכונות של שפת רובי במהלך הפוסט.
נשאר עוד הרבה לדבר עליו, עוד לא נגענו בתכונות ה OO בשפה.
ארצה להמשיך בסדרה בצורה שתהיה יותר ממוקדת בהבנת השפה עבור מפתח ג\'אווה שעובר לשפה. למשל: אני 🙂

שיהיה בהצלחה!

—-

קישורים רלוונטיים

Rubular – \"מחשבון\" regex אונליין לרובי

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

[ב] YaCC הוא קיצור של Yet another Compiler Compiler – ונחשב הכלי הנפוץ בתחום (ביחד, אולי עם ANTLR).

[ג] הידוע גם בשם (KRI (Koichi\'s Ruby Interpreter על שם המחבר שלו, בהתאמה ל MRI שנכתב ע\"י מאטצ\'.

[ד] יש מיתוס, בקרב אלו שלא חוו מספיק tradeoffs בתוכנה על בשרם, ש \"אם נשים את האנשים המוכשרים ביותר – הם ימצאו דרך \'למחוק\' את ה tradeoff ולהעלים אותו\". בקשה זו היא כמו לקוחה מהמיתולוגיה היוונית – והתוצאה שלה גם היא לעתים יוונית: טרגדיה.
סיפור מפורסם הוא של ספינת המלחמה Vasa בו זימן אליו מלך שוודיה את אדריכל הספינות המפורסם בעולם (באותה התקופה) כדי שיבנה את ספינת המלחמה הטובה בעולם: ספינה שהיא גם ספינת משא (לחיילים) הדורשת מקום רב ויציבות, וגם ספינת קרב – הדורשת זריזות ויכולת תמרון. גם וגם. זה היה פרויקט אדיר, ומרשים – שלא נראה כמותו באותם הימים. לאחר עבודה שכללה מאמצים אנושיים אדירים ומפוארים – הספינה הושקה למים. היא הצליחה לשלב גמישות קרבית ונשיאת חיילים – למרחק של 1300 מטר בלבד, שם משהו השתבש והיא שקעה למצולות. הטיטאניק בגרסה המקוצרת.

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

חשוב לציין שכן ניתן \"לרמות\" tradeoffs במידה מסוימת, וזה ע\"י החלפה של הבחירה ב tradeoff ברגעים שונים בתוך אותו התהליך. למשל כמו האופן בו Cassandra \"מרמה\" את ה CAP Theorem (שטוען שלא ניתן להשיג גם זמינות וגם עקביות במידע מחולק (partitioned)) – אבל Cassandra מאפשרת לכל query בודד לבחור בין יותר זמינות או יותר עקביות בנתונים, וכך \"כמערכת\" – Cassandra מספקת גם וגם.

כיצד מגדירים ארכיטקטורה? צעד אחר צעד

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

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

התוצאה: מסר יותר חד (\"כך כותבים קוד נקי!\"), בד בבד עם אשליה שאנשים באמת כותבים קוד שכזה \"במכה ראשונה\".

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

ארכיטקטורה, להזכיר, היא סדר וצורה של התוכנה שלנו, וגם קבוצת ההחלטות שקשה לעשות בהן שינוי (ולכן אולי כדאי להקדיש אליהן מחשבה בשלב מוקדם).
תהליך ה\"ארכיטקטורה\" נראה לפעמים כמעט כמיסטיקה – ואני מקווה שפוסט זה יתיר חלק מהערפל (ואולי גם מההילה).
לצורך התרגיל בחרתי בבעיה קלאסית במדעי המחשב (שאני פחות או יותר פותר בעצמי כרגע כחלק מלמידת שפת רובי): \"בעיית שבע המלכות והפרש\". כיצד ניתן להציב על לוח שחמט 7 מלכות [א] ופרש מבלי שיאיימו זה-על-זה.
אני לא אתמקד בתהליך חישובי אופטימלי (מדעי המחשב), כי אם בארגון מוצלח של הקוד למחלקות ופונקציות (\"ארכיטקטורה\" / הנדסת תוכנה).

בואו נצא לדרך.

כיצד מתחילים להגדיר ארכיטקטורה?

הרי אפשר להיתקע ולבהות ב IDE (\"אני בוהה בו, הוא בוהה בי\") זמן-מה מבלי להגיע להחלטה כיצד להתחיל.
כאשר יש כיוון – הקוד רץ מעצמו, \"7 המלכות והפרש\" זה לא באמת \"אתגר\" תכנותי עבורנו. הקושי לרוב יהיה לבחור כיוון ראשוני לארכיטקטורה.
דרך אחת היא פשוט מאוד להתחיל ולכתוב בלי סדר – ואז לעצור, לספוג תובנות מהקוד, ולהיעזר בהן בכדי לקבוע את הכיוון ולבצע Refactoring. לא לכולם זה הולך בקלות.
שיטה קטנה אחרת היא כלל שמגיע מתכנון-מונחה עצמים, ו DDD: \"המלים בהן אנו משתמשים לתאר את הבעיה \'חייבות\' לקבל ייצוג מתאים (מחלקות) במערכת\".
אז הנה, קיצרנו לעצמנו מעט – ויש לנו נקודת התחלה כלשהי.
מהם המלים שחוזרות על עצמן בתיאור הבעיה? \"מלכה\" ו\"לוח\" (לפחות אצלי):
להבהיר: במיוחד במערכת כ\"כ קטנה, במיוחד כשכותב אותה אדם בודד, ובמיוחד בעולם האג\'ייל – אין הגיון לייצר תרשימי UML! יצרתי את התרשים רק עבורכם – קוראי הבלוג.
האופן הטבעי לעשות זאת הוא לכתוב קוד מסגרת ראשוני (כלומר: skeleton):

לגבי שפת רובי (הנה פוסט \"נחיתה\" – למי שלא מכיר), attr_accessor היא דרך מהירה לייצר getter/setters לרשימת ה symbols שהוצמדה לפונקציה. למשל, במקרה של המחלקה Board ייווצר משתנה בשם queens@ שערכו nil, מתודה בשם queens שמחזירה את ערך המשתנה, ומתודה בשם =queens שמאפשרת לבצע השמה לערך בעזרת סימן \"=\" (להלן הסבר יותר מפורט)

כבר ביצירת השלד הזה – בעצם לקחתי כבר כמה החלטות \"תכנוניות\" (להזכיר: קוד < תכנון < ארכיטקטורה – אך יש ביניהם דמיון רב).

עם הניסיון אנו יודעים להחליט מהר יותר מה אנו מעדיפים, ואנו יודעים מהר יותר להסביר מדוע. כשהייתי מהנדס תוכנה צעיר – הייתי יכול להשקיע זמן רב בכדי להחליט כיצד אני רוצה לייצג את הדברים הבסיסיים ביותר. 
אלו החלטות כבר \"לקחתי\"?
דילמה: כיצד לייצג את הלוח והכלים עליו
  • אופציה א\': לוח כמטריצה 8×8 של מערך המחזיק אובייקטים.
    • יתרונות: מודל פשוט וקל להבנה
  • אופציה ב\': ניהול רשימות של הכלים השונים – כאשר כל כלי מחזיק את המיקום שלו על הלוח.
    • יתרון: יותר יעיל (טיול על x כלים – שזה מספר קטן מ 64 תאים – בכדי למצוא כלי) – לא משמעותי כאן.
    • יתרון: מישהו יצטרך לחשב \"איום\" של כלי על כלי אחר. זה גורר דילמה אחרת (מייד) – שאת התוצאה שלה, אופציה זו \"תקבל\" בצורה טבעית יותר.
לכן בחרתי – באופציה ב\'. עד כמה ההחלטה קריטית? – לא ממש. אם המפתחים איתם אעבוד יעדיפו את אופציה א\' – אזרום איתם ללא נקיפות מצפון.
דילמה (forward thinking): כיצד מחשבים איומים של כלי אחד על כלים אחרים
קשה להתקדם ביישום פתרון לבעיה – מבלי שהנקודות המפתח ברורות, והנה אחת כזו.
  • אופציה א\': הלוח הוא זה שמחשב איומים בין הכלים
    • יתרונות: אני מעריך שאופציה זו תדרוש פחות שורות קוד.
  • אופציה ב\': כל כלי הוא זה שמחשב את האיומים שלו
    • יתרונות: יותר OO, משום ש\"הצלחנו למצוא\" מומחיות למחלקות. מלכה מומחית בלהכיר את האיומים שהיא מפיקה, ופרש מומחה בלהכיר את האיומים שהוא מפיק.

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

אם היינו מתמודדים עם בעיה דמיונות בשם \"8 המלכות\" [ב] בה יש רק מלכות – אולי היה היתרון של אופציה ב\' היה פחות ברור. אני חושב שאני אישית עדיין הייתי מעדיף לבחור באופציה ב\' – כי אני לא מרגיש עקרונית נוח עם \"לוח יודע כל\".
בקיצור: אני בוחר באופציה ב\' – וזו בחירה משמעותית, שתשפיע על כל עיצוב המערכת. אני לא אומר שאם מפתחים שעובדים איתי היו רוצים את אופציה א\' הייתי טוען שאופציה א\' היא לא אופציה סבירה. אבל לבחירה הזו תהיה השפעה ישירה ועקיפה על הרבה בחירות אחרות במערכת שלנו – בחירות שלא יהיה ניתן להפוך בקלות (כלומר: ללא שינוי של הרבה שורות של קוד). זוהי דוגמה לבחירה בעלת משמעות ארוכת-טווח.
הנה עדכנתי את התוכנית בהשלכות של הבחירה באופציה ב\':

למי שלא מכיר UML – חץ משמעו קשר, שמתבטא בפועל ב member של המחלקה. כלומר: חץ דו-כיווני אומר שבמחלקה Queen יש member בשם board@, ובמחלקה Board יש מערך של members בשם queens@.
יש לנו כאן קשר דו-כיווני, מה שאמור להדליק לנו נורה אדומה. ככל האפשר (עקרונות ה OO) – מומלץ להפחית קשרים דו-כיווניים (וקשרים בכלל) מהמערכת.
בואו נזכר מדוע אנו רוצים את הקשרים האלו:
  • מלכה מכירה את הלוח – כי היא צריכה לבדוק מה יש בתאים מסביבה – (\"()board.content_at\").
  • לוח מכיר את המלכות – כי הוא מציב את המלכות.
הממ… האם אפשר \"לפטור\" את עצמנו מאחד מהקשרים? נראה שכן. אפשר שהלוח לא יהיה אחראי להצבה (הנה הגדרת אחריות). נגדיר מחלקה אחרת, BoardManager (שם מקומם משהו) שתיקח ממחלקת ה Board את האחריות ואת התלויות שבאות איתה.
בואו נכסה עוד יכולת נדרשת לתכנון: היכולת להדפיס את הפתרון (כלומר: מצב הלוח בו 7 מלכות ופרש לא מאיימים זה על זה) על המסך. לצורך המערכת הזו – מדובר בהדפסת ASCII ב console.

דילמה: כיצד לבצע את ההדפסה:

  • אופציה א\': ה Board אחראי להדפסה
    • ייתרון: יש למחלקה את כל המידע הדרוש להדפסה
    • חיסרון: עכשיו יהיו למחלקה 2 אחריויות: א. לנהל את ה state של פתרון, ב. לנהל את ההדפסה של ה state הזה. 2 אחריויות היא חריגה מה SRP (קיצור של: Single Responsibility Principle).
  • אופציה ב\': ה Board מדפיס את הלוח, ועושה to_s (כלומר: toString ברובי) למלכה – כדי לתת לה את האחריות כיצד להציג את עצמה (למשל: האות \"Q\" או סמיילי)
    • ייתרון: פיזור אחריות בין אובייקטים שאחראים למשימה (scalability פיתוחי)
    • חיסרון: פיזור אחריויות – אין SRP
  • אופציה ג\': ליצור מחלקה נוספת שאחראית על ההדפסה
    • ייתרון: הפרדת אחריויות ברורה בקוד
    • חיסרון: מה השתגענו – עוד מחלקה? כלומר: יותר קוד לכתוב
הדבר הראשון שיש לשים לב אליו במקרה זה הוא שיש לנו 3 אופציות: אופציה א\' ו ב\' הן יחסית דומות בעוד אופציה ג\' שונה. במקרה שכזה כדאי שתידלק לנו נורה אדומה (קטנה) בשל זיהוי דפוס קבלת החלטות שנקרא \"A, A-, B\".

מה זה אומר? באופן טבעי כבני אדם, אנו נוטים להעדיף השוואות נוחות.
קל יותר להשאוות בין אופציה A לאופציה -A שדומה לה, אך פחות טובה ממנה ולבחור את A, מאשר להחליט בין A ל B.
לכן, אנו נוטים \"לזנוח\" את B ולהתמקד בהשוואה בין 2 האופציות הנוחות בלבד.

כדי לקבל החלטה טובה יותר, כדאי לזהות את המצב ולבצע את ההחלטה בשני שלבים:

  1. שלב א\': A מול A-, ולהעלות את אופציה A \"לגמר\".
  2. שלב ב\': לבחון, מחדש, את אופציה A מול אופציה B.
אני חייב להודות שבמקרה שלנו זה סוג של אופציה A1 ו A2 – כי קשה לי לומר בהכרח מי עדיפה מבין אופציה א\' וב\'.
אבל, אם אתם בשלב תכנון ועולות 2 אופציות דומות שאחת נראית בבירור פחות טובה – הסירו אותה. היא נותנת הרגשת ביטחון באופציה הדומה לה – על חשבון אופציות אמיתיות אחרות
אם אתם מעוניינים להעביר החלטה \"בלי שקולגות יפריעו לכם\" (לא יפה!!) – צרו אופציה A- מגוחכת, בכדי להעלות את סיכוייה של אופציה A להיבחר. עוד פרטים על כך אפשר לקרוא בפרק הראשון בספרו של דן אריאלי \"לא רציונלי, ולא במקרה\".
חזרה אלינו:
אנו רוצים לקבל באמת את ההחלטה הטובה ביותר, ולכן הסרתי את אופציה ב\' שנראית לי פחות מאופציה א\'. ולכן:
  • אופציה א\': ה Board אחראי להדפסה
    • ייתרון: יש את כל המידע הדרוש להדפסה
    • חיסרון: עכשיו יהיו לו 2 אחריויות: א. לנהל את ה state של פתרון, ב. לנהל את ההדפסה של ה state הזה. 2 אחריויות היא חריגה מה SRP (קיצור של: Single Responsibility Principle).
  • אופציה ב\' (החדשה): ליצור מחלקה נוספת שאחראית על ההדפסה
    • ייתרון: הפרדת אחריויות ברורה בקוד
    • חיסרון: מה השתגענו – עוד מחלקה? כלומר: יותר קוד לכתוב

בדילמה ביניהן – אני בוחר באופציה ב\'. 
אופציה א\' היא מהירה יותר למימוש, אבל אני אישית מרגיש יותר נוח לתקתק קצת יותר – ולבנות מבנה שהוא יותר scalabale לעתיד.

מצד אחד, זוהי אולי \"שריטה של ארכיטקט\". המערכת שלנו הולכת להיות קטנה למדי (אין לה \"עתיד\") – והקוד יהיה ניתן לניהול גם ללא קנאות ל SRP. זהו Waste.
מצד שני, זהו הרגל טוב למערכות גדולות – והרגלים לא מחליפים בקלות כמו גרביים. אני מעדיף לחיות עם אינטואיציה שהיא פחות יעילה למערכות קטנות, אך יותר יעילה למערכות בינוניות / גדולות (שם אני מבלה את רוב זמני).
אני בוחר באופציה ב\' והמבנה שלנו כעת נראה כך:
ארכיטקטורה א\'
זהו, האם הגעתי לארכיטקטורה המושלמת לבעיה הנתונה?
אז זהו… בוודאי שלא!
  1. אנו לא בעסקי המושלמות (לפחות לא אני). המטרה היא לייצר ארכיטקטורה \"טובה\" (ככה \"top 5\") ולא \"הטובה ביותר\". למה? כי זה פשוט בלתי-אפשרי לוגית. כל דרישה חדשה למוצר הופכת את הסדר בין כמה הארכיטקטורות הטובות ומציבה ארכיטקטורה אחרת בראש הטבלה כ\"ארכיטקטורה הטובה ביותר\". מכיוון שאי אפשר לחזות את העתיד – אז אי אפשר גם להעריך איזו ארכיטקטורה, מבין הארכיטקטורות \"הטובות\" היא הטובה ביותר. פשוט אי אפשר.
  2. ארכיטקטורה מתאימים להקשר מסוים: מי הצוות שיעבוד איתה, מי הלקוח, ומה הסיטואציה (לחץ, פרויקט אסטרטגי, וכו\'). לא יהיה נכון להשוות ארכיטקורות שונות כאשר יש להן הקשרים שונים.
    למשל: בארכיטקטורה הזו אני עומד להיות המפתח. מה שטוב עבורי, לא בהכרח מספיק טוב לכל אדם אחר.
  3. את הארכיטקטורה ניתן באמת להעריך רק לאחר זמן ממושך. דברו איתי עוד חודשיים – ואספר לכם כמה טובה היא הייתה.
    כלומר: מדובר בהימור מחושב.
ברגע שהתגבש מבנה בסיסי, זוהי נקודת זמן טובה להשוות אותו למבנה אחר.
מה קרה? לקחנו סט של בחירות במהלך הדרך – והשוונו בחירה מול בחירה, אך לא תמונה מול תמונה.
ייתכן שלאור הבחירה הראשונה שביצענו – אכן בחרנו בחירות טובות. אבל, אם הבחירה הראשונה הייתה אחרת – היינו מגיעים לסט בחירות שסה\"כ הוא מוצלח יותר.

בואו נבנה מבנה הגיוני אחר (או שניים) – ונבחן אותם. נראה אם אנו יכולים להגיע לפתרון שהוא סה\"כ טוב יותר.
הנה אלטרנטיבה אחרת (להלן ארכיטקטורה ב\'):

ארכיטקטורה ב\'

היתרונות של ארכיטקטורה א\' הם:

  • הפרדת אחריויות מקיפה (כל מחלקה אחראית על משהו אחד)
  • מוכנות לגדילה* (development scalability). 
* מדוע מוכנות לגדילה היא ייתרון? תמיד הרי נוכל לעשות refactoring למבנה הרצוי, ברגע שנזדקק לו בפועל.
המוכנות לגדילה ייתרון במידה ויש עוד מפתחים שעובדים על הפתרון ולא בקיאים בשיקולים כמו המתכנן. ע\"י בניית \"תשתית רחבה יותר\" המתכנן יכול להכווין את המפתחים הנוספים לכיוון הרצוי.
עבורי אישית זה עניין של הרגל / שלווה פנימית.
היתרונות של ארכיטקטורה ב\' הם:
  • מעט מחלקות – מעט קוד -> יותר מהר לפתח. בד\"כ עוד מחלקות דורשות כתיבה של עוד \"Gluing Code\".
  • הפרדה בין אחריויות – אם כי פחות מקיפה.
אין פה עניין של ארכיטקטורה \"נכונה\". שתי הארכיטקטורות סבירות, ומה שנותר לנו הוא שקלול תמורות (trade-off) – איזו ארכיטקטורה נראית לנו מתאימה יותר בהקשר הנתון. בארכיטקטורה, בוחנים trade-offs כל הזמן.
אני – בוחר בארכיטקטורה הראשונה. אני מרגיש פחות נוח עם מחלקת Board שאחראית גם על ניהול מצב, גם על הדפסה, וגם על הכרות עם הכלים השונים.

סיכום התהליך

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

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

  1. הידע על הארכיטקטורה איננו קיים – הוא מתגלה. אנו מגלים תוך כדי תכנון ומעט קידוד (\"spike solutions\" ו/או POCs) תובנות חדשות, ומשלבים אותן בארכיטקטורה.
    נכון, שאם בנינו מערכות דומות בעבר – אנו יכולים להשתמש בידע שנצבר שם בכדי לקצר תהליכים.
  2. אפשרויות בחירה הן קריטיות לתכנון איכותי. ההרגל נוטה למשוך אותנו ל\"פתרון הראשון שיכול לעבוד\" ולהישאר שם, אבל חשוב לעצור לרגע וליצור עוד אלטרנטיבות ישימות – ואז לבחור מביניהן את הטובה יותר.

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

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

    למשל: בארכיטקטורה א\' הצבנו את כל אחריות ההדפסה על הלוח. בגלל ריבוי הכלים (מלכה, פרש) הוא יאלץ לבצע if (או case) ולהגדיר התנהגויות לכל כלי (המקביל לקריאה ל ()to_s בארכיטקטורה ב\'). מצב כזה נחשב כ bad smell בו מומלץ לעשות refactoring לכיוון State Pattern. מצד שני – המצב שייווצר יפגע ב SRP. פה יש עניין של הבחנה עיקר וטפל – שמבוסס במידה רבה על ניסיון מכיוון שאין דרך מתמטית \"להוכיח\" איזה מצב הוא עדיף. ניסיון הוא בסהכ ה\"מושכל\" ב\"הימור מושכל\", כמובן.

זהו, בחמש דקות – עיקרו של תהליך הגדרת הארכיטקטורה.

סיכום

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

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

שיהיה בהצלחה!

—-
[א] מלכה ברבים, מלכות, יש לקרוא כמְלָכוֹת (כ\' רפה) – כמו \"MELAHOT\"
[ב] \"8 המלכות\" זו כמובן הבעיה המקרית. אני רציתי לגוון ולנסות משהו טיפה אחר.