ריילס: Active Record (סקירה כללית)

Active Record, או בקיצור ARֿ, היא שכבת ה ORM של ריילס.
סיפקתי הקדמה על AR בפוסט על ריילס (כולל למשל, חוקי הקשר בין שמות מחלקות לשמות טבלאות בבסיס הנתונים) ולא אחזור על מידע זה.

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

אובייקט של מודל AR הוא לא מבנה נתונים פשוט – זוהי מחלקה עם מתודות לכל דבר. המתודות יכולות להיות helpers לעבודה עם נתונים (למשל: calculated fields או find_matching_x), או business logic של ממש – כל עוד הלוגיקה נוגעת לרשומה הבודדת בבסיס הנתונים או מה שהיא מייצגת. לוגיקה הנוגעת בכמה אובייקטים במערכת – תשב במקומות אחרים במערכת (ב Controller או ב Lib).

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

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

בניגוד לחלק מכלי ה ORM (קיצור של Object-Relational Mapping) האחרים, AR לא מנסה ״להחביא לגמרי״ את ה SQL. הגישה היא יותר גישה של ״בואו נהפוך את השימוש ב SQL ליותר DRY\”.
למשל, AR, תחסוך לנו את העבודה המעצבנת ב SQL של לציין שוב ושוב את שמות השדות הרלוונטיים (חשבו למשל על Join מורכב). מצד שני היא כן רואה בשימוש במידה מסוימת של SQL כהכרחי, ולכן מספקת מתודות כגון find_by_sql בהן מזינים שאילתת SQL.

לאחר שנתקלתי שוב ושוב בצורך ״לעקוף את ה ORM״ בעבר – הגישה הזו נראית לי מאוזנת ובריאה. בעוד ששימוש ב SQL הוא לא דבר מומלץ ב Hibernate (כי אז ה caches של Hibernate לא יהיו מעודכנים, למשל), בריילס הנחת העבודה היא ש SQL יהיה בשימוש מפעם לפעם.
מה המחיר ש AR שילמה על זה? כנראה בביצועים, ובמגוון היכולות שיש לכלי ORM ואינם מפותחים כ\”כ ב AR.
מחיר סביר, לטעמי.

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

סכמה

מודל (Model, של MVC) של AR היא מחלקה שיורשת ממחלקת הבסיס ActiveRecord::Base.
AR יצפה ששם המחלקה יופיע בצורת יחיד, ושם טבלה המתאימה – בצורת רבים (ע״פ החוקים שתיארתי בפוסט הקודם).

אין צורך להגדיר במחלקת המודל את העמודות השונות של הטבלה: בזמן ריצה AR יתשאל את בסיס הנתונים, ימצא את העמודות, ותוסיף שדות ו getters/setters לגישה אליהם על מחלקת המודל.
מדוע? בכדי למנוע מצבים של חוסר עקביות בין בסיס-הנתונים לקוד, ולחסוך תחזוקה כפולה של השדות הקיימים על כל טבלה (עקרון ה DRY).

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

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

  • קובץ ה schema.db הכולל תיאור של הסכמה השלמה.
  • תיקיית migrate – בה יש ״סקריפטים״ שמעדכנים את הסכמה בבסיס הנתונים.
  • קובץ ה seeds.rb.
באופן מעט לא-אינטואטיבי, אין לשנות את קובץ ה schema.rb. זהו קובץ שבעצמו הוא generated מתוך בסיס הנתונים, ונועד ליצירת סכמה מהירה ואמינה עבור התקנה חדשה של האפליקציה שלכם בבסיס נתונים חדש. הוא יכול להיות רפרנס טוב בכדי להבין את הסכמה בקלות.

ה״סקריפטים״ (מחלקות רובי) שבתיקיית ה migrate הן מה שמעדכן את הסכמה, והם מסודרים כך שיוכלו להעביר בסיס נתונים מ״מצב 5״ ל״מצב 6״ (שנוצר בעקבות שינויים במודל של האפליקציה), או לאחור מ ״מצב 6״ בחזרה ל״מצב 5״ (ככלי rollback).
את הסקריפטים האלו מריצים בעזרת פקודת rake db:migrate מה command line, בעוד שאת בניית הסכמה מחדש (על בסיס schema.rb) מפעילים בעזרת פקודת rake db:schema:load.

פקודות אלו נוגעות רק לסכמה. עבור הכנסת נתונים ראשוניים לבסיס הנתונים קיים הקובץ seeds.rb, שמופעל בפקודה rake db:seed.

פקודת rake db:setup, שנהוג להפעיל בהתקנת האפליקציה על מערכת חדשה, מפעילה בפועל גם db:create, גם db:schema:load ולבסוף את db:seed. ניתן לבחון את הסריפטים עצמם בגיטהאב.

בכדי לייצר migration חדש בד״כ משתמשים ב generator הבא:

$ bin/rails g migration

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

20150108083136_create_price_records.rb

כאשר ה״קישקוש״ בתחילתו הוא ה timestamp המתאר את זמן יצירת הקובץ.
הקובץ עצמו עשוי להראות כך:

class CreateProducts < ActiveRecord::Migration
def change
create_table
:products do |t|
t.string :name
t.text :description
t.decimal :price

t.timestamps null: false
end
end
end

change היא מתודת העבודה העיקרית. בתוכה נוכל לבצע שינויים בבסיס הנתונים כגון יצירת טבלה (כמו במקרה שלנו), הוספת או הרדת עמודה, או אינדקס, ועוד מספר פעולות. AR יידע להשתמש בתוכן המתודה בכדי לבצע את השינוי, וגם להחזיר אותו בחזרה למצב הקודם במקרה ונרצה לעשות מתישהו rollback (תקלות ב production, מישהו?).
יש מצבים בהם לא ניתן להסיק אוטומטית את הדרך חזרה, למשל: שינוי טיפוס של עמודה, או החלטה שמעתה לא יתקבלו nulls – בגלל שאין תשובה אחת לשאלה \”מה לעשות עם הערכים הנוכחיים?\”. במקרים כאלה יש להשתמש בשתי מתודות אחרות: up (השינוי הרצוי) ו down (פעולת ה rollback) – והטיפול בשאלות הלא-ברורות.

AR מגדיר סט של כמה טיפוסי-ביניים בהן ניתן להשתמש, למשל: string, text, float, decimal, time, וכו׳
על כל עמודה ניתן להגדיר עוד properties, למשל null:false – עבור עמודה שנרצה שבסיס הנתונים יגן שלא תקבל ערך null.
בעזרת Adapter מתאים לכל בסיס נתונים, AR מחליט לאילו טיפוסים של בסיס הנתונים למפות את טיפוסי-הביניים שלו בפועל. למשל, האם text ישמר כ varchar, nvarchar, או CLOB. זהו סוג של שירות שאמור לחסוך למפתח את ההבנה העמוקה בין הטיפוסים השונים של בסיס הנתונים, ועבור רוב התסריטים – הבחירות שלו הן בהחלט טובות.

עבור בסיסי נתונים מסוימים, ה Adapter גם יתמוך בטיפוסים נוספים. למשל, אם אתם עובדים עם PostgreSQL, תוכלו להשתמש בטיפוסי AR בשם json או hstore או properties מסוימים – שיתמפו ליכולות של PostgreSQL.

AR ייצר getters ו setter על המודל עבור על עמודה, דרכם נוכל לגשת לנתונים.

  • אם מדובר בשדה מסוג timestamp ה getter יחזיר אובייקט רובי מסוג Time.
  • אם מדובר במספר עשרוני אז ה getter יחזיר אובייקט רובי מסוג BigDecimal (בכדי לא לאבד נתונים – בסיס הנתונים יכול לשמור דיוק גבוה מהיכולת של רובי). אם מדובר במספר עשרוני ללא ספרות עשרוניות לאחר הנקודה – יוחזר אובייקט רובי מסוג Fixnum (כלומר: \”פרמיטיב\” ה integer).
  • אם מדובר בשדה בולאני, יווצר getter שסימן שאלה בסוף שמו – ע\”פ הקונבנציה המקובלת ברובי.
  • וכו\’….

עמודות עם טיפול מיוחד

אם לא הגדרתם בעצמכם את ה primary key (מה שמומלץ רק במצב של טבלה שכבר קיימת לפני המערכת שלכם), AR ייצר עבורכם עמודת id מסוג auto-increment (המימוש המדויק תלוי בבסיס הנתונים).

גם אם הגדרתם את ה primary key, (לדוגמה: עמודה בשם isbn) הגישה אליו בקוד הרובי תהייה דרך השם \”id\” ולא דרך השם המקורי של העמודה.

אם תפעילו את הפקודה t.timestamps (כמו בדוגמה למעלה), AR ייצר עבורכם 2 עמודות: created_at ו updated_at וינהל אותם עבור כל רשומה, כלומר: בכל עדכון של ערך של רשומה – יתעדכן גם ערך ה updated_at.

המודל

המודלים של AR הן מחלקות היורשות ממחלקת הבסיס ActiveRecord::Base, לדוגמה:

class City < ActiveRecord::Base
belongs_to :country
has_many :streets_to_cities
has_many :streets, :through=> :streets_to_cities

def self.search_for_country(text)
City.search(
# Some complex search logic
)
end

end

כפי שכבר הזכרנו – לא מציינים את השדות של המודל במחלקת המודל עצמה. בזמן עליה AR יתשאל את הסכמה של בסיס הנתונים ואז יוסיף את הפרמטרים ו getters/setters (כלומר: accessors) המתאימים בזמן ריצה. בעת הוספת עמודה חדשה בבסיס הנתונים – אין צורך בשינוי קוד במודל, ועדיין אפשר להשתמש בשדה החדש הזה – מה-Controller או ה View, למשל.

אז מה בכל מוגדר על המודל?

קשרים בין הטבלאות (associations)

ע\”י פקודות הקשר:

  • belongs_to
  • has_one :through ,has_one
  • has_many :through ,has_many
  • has_and_belongs_to_many
השימוש הנכון בפקודות הקשר דורשות מעט אימון. קיים תיעוד מקיף (עם תרשימים), אך הייתי רוצה להסביר כמה נקודות מבלבלות במיוחד:

ניתן לומר שספר has_one \”תוכן עניינים\”, או להיהפך: ש\”תוכן עניינים\” belongs_to ספר. לכאורה זה סימטרי.

בפועל השאלה היא היכן נכון שינוהל ה foreign key בין הטבלאות?
בשימוש ב belongs_to הוא ינוהל על המודל שהגדיר את הקשר (\”המקומי\”), ובשימוש ב has_one הוא יוגדר על המודל האחר (\”הרחוק\”).
פעמים רבות רוצים ניווטיות לשני הכיוונים, ואז מגדירים גם את belongs_to וגם את has_one, בשני המודלים במקביל.

כאשר אנו רוצים להגדיר קשר one-to-many, אזי אנו מגדירים אותו בשני הצדדים: נאמר גם שספר has_many דפים וגם שדף belongs_to ספר. שימו לב ששומרים על צורת היחיד והרבים כך שיתאמו לשפה הטבעית (pages:, אבל book:) :

class Book < ActiveRecord::Base
has_many :pages
end

class
Page < ActiveRecord::Base
belongs_to :book
end

בקשר של many-to_many מגדירים בשני הצדדים \”<has_and_belongs_to_many :<other table\”.

הערה: AR מנהל את ה associations בעצמו. אם אתם רוצים שבסיס הנתונים יאכוף את תקינות ה foreign keys בנוסף ל AR, תוכלו להגדיר foreign keys על ה migration.

ניתן למצוא מידע על עוד אפשרויות בתיעוד של Associations

validation

ע\”י שימוש בשורה של פקודות ואלידציה כמו validate_presence_of או validates_numericality_of


Overriding Accessors
למשל: אתם רוצים לעשות formatting לערך השדה המגיע מבסיס הנתונים (getter) הוא להוסיף validation לא שגרתי או המרה של נתונים (setter).

Transient Model Members

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

Scopes

שאילתות ביניים אותן ניתן להרחיב, רעיון דומה ל Views בבסיס הנתונים. פרטים בהמשך.

מתודות עזר שונות

שעוזרות לגשת לנתונים (כמו search_for_country בדוגמה למעלה) או כאלה שמבצעות business logic הקשור ל scope של המודל (אך לא יותר מכך!)

שמירה

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

יש שתי גרסאות של המתודות הנ\”ל:

  • save – שתחזיר nil אם הייתה בעיה בעדכון הנתונים.
  • !save -שתזרוק Error אם הייתה בעיה בעדכון הנתונים. כנ\”ל לגבי !create.

ניווט וחיפוש

הדרך הפשוטה והבסיסית לשליפת רשומה ב AR היא בעזרת ה id (קרי: primary key):

a_book = Book.find(63114)

דרך פופולרית נוספת היא לבצע דרך פונקציית ה where, שמקבלת תנאי WHERE של SQL:

a_book = Book.where(\”name = \’The White Horse\’ and book_type = \’Fiction\’ \”).first
ע\”י צמצום השאילתה לתנאי ה WHERE בלבד, AR מספקת כוח רב תוך כדי \”דילוג\” על חלקים טכניים בשאילתת SQL (כמו בחירת הטבלה והעמודות שיחזרו, תוך כדי הימנעות מדו-משמעות).
בכדי להתגונן בפני חולשות של SQL Injection וגם מטעויות SQL, קיימת צורת שימוש מוגנת של הפונקציה where:
a_book = Book.where(name: \’The White Horse\’, book_type: \’Fiction\’)

AR ידאג לבצע escaping מתאים על הפרמטרים בכדי להגן בפני התקפת SQL Injection.
בקצה השני יש את מתודת find_by_sql שדורשת כפרמטר שאליתת SQL שלמה – ומחזירה תוצאה דומה. אין לה הגנות בפני SQL Injection, כמובן.

התוצאה של פעולת ה where היא אובייקט בשם ActiveRecord::Relation המכיל את כל הרשומות התואמות.

מלבד פעולות שליפת שורות כגון: first, all או to_a, מאפשר אובייקט ה Relation מאפשר לשרשר עוד תנאים (נקראים finder methods) לשאילתה. למשל:

a_book = Book.where(\’name LIKE The%\’).order(\’publish_date DESC\’).limit(10)

עבור limit יש finder method אחות שעוזרת לעשות paging קל, בשם offset:

a_book = Book.where(\’name LIKE The%\’).order(\’publish_date DESC\’).limit(10).offset(100)

גרסה זו תחזיר את הספרים מספר 100-109 ששמם מתחיל ב \”The\”, ממיונים ע\”פ תאריך הפרסום בסדר יורד.

עוד finder method נפוצה היא joins המאפשרת להכתיב לשאילה לכלול טבלאות נוספות. בד\”כ משתמשים בה כאשר כותבים לבד את ה SELECT:

an_order = Order.select(\”orders.customer_id, count(*) as total\”)
.group(:driver_id)
.joins(:customer)
.where(:\’customer.type\’ => \’premium\’)

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

הנה שימוש בעייתי ב joins:

companies = Company.joins(:persons).where(:persons => { active: true } ).all

# ...

companies.each do |company|
company.person.name
end

הסבר קצר:
נתונה טבלת חברות, עם association לטבלת עובדים. בטבלת העובדים יש שדה שמציין אם העובד עדיין נמצא בחברה (active).

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

מה קרה? השאילתה לא הביאה את שם העובדים (אין הרחבה של ה SELECT לכלול את employee.name).
AR מספק לנו \”מציאות מדומיינת\” של אובייקטים בזיכרון – ולכן אינו מונע מאיתנו לגשת ל person.name. הוא פשוט עושה SELECT נוסף בכל גישה ל name בכדי להביא את הנתון החסר. אבל… כאשר ניגשים ל person.name בלולאה – עשויות להיות הרבה מאוד שאילתות כאלה –> מה שכנראה יגרום לבעיית ביצועים משמעותית.

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

אפשר לעשות זאת בצורה מרכזית ע\”י החלפת הפקודה joins בפקודה includes. פקודת includes פועלת כמו joins, אבל מוסיפה ל SELECT את כל ערכי שדות הטבלה שאילתה עשינו join לתוצאה – כך שיהיו זמינים לגישה ללא קריאה נוספת לבסיס הנתונים.

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

Scopes

תנאים המשורשרים של finder methods נוטים להתארך ולחזור על עצמם. AR מספק מנגנון של שימוש חוזר בהם בשם scopes.

class Order < ActiveRecord::Base
scope :created_today, -> { where(\'created_at >= ?\', Time.zone.now.beginning_of_day) }
end

todays_biggest_invoices = Order.created_today.order(\'amount DESC\').limit(25)
invoices_to_handle
= Order.created_today.where(status: open)
invoices_to_handle
= Order.where(status: open).created_today # also valid

מחזור-החיים של מודל 

AR מספק שורה של events לאורך מחזור החיים בהם אנו יכולים להתערב: לבצע validations מורכבים, למנוע ברגע האחרון עדכון נתונים לבסיס הנתונים, או לבצע שינויים במודל on-the-fly:

בנוסף יש עוד 2 callback:

  • after_initialize – מיד לאחר יצירה של כל מופע של המודל
  • after_find – מיד לאחר כל פעולת find

ל callback אפשר להרשם בצורה דקלרטיבית, או בעזרת בלוק:

class CreditCard < ActiveRecord::Base
belongs_to :person
after_validation :do_after_validation

before_validation(on:
:create) do
self.number = number.gsub(/[^0-9]/, \'\') if attribute_present?(\'number\')
end

protected
def do_after_validation
log.write(
\'something\')
end
end

הפרמטר on: :create מאפשר לי לבצע את הקוד רק לפני validation של רשומה חדשה (פעולות create ולא update).
כמובן שצורת הבלוק מאפשרת לנו להוציא לוגיקה למקום משותף ולקרוא לה מכמה מודלים / מכמה שלבים שונים ב lifecycle.

כדי לעצור את ה Lifecycle ניתן לזרוק מתוך ה callback שגיאה מסוג ActiveRecord::Rollback. כל שגיאה אחרת תחלחל לשאר המערכת.
באירועי before_xxx, ניתן להחזיר את הערך false – בכדי להשיג תוצאה זהה.

Best practices

הגבלת הניווט בין מודלים ע\”פ חוק דמטר (Law of Demeter)

החוק של דמטר הוא החוק של \”the least knowledge\”: התוכנה תהיה ניתנת יותר לתחזוקה אם כל מחלקה תדע כמה שפחות על מחלקות אחרות. הכלל הפרקטי ב AR הוא כלל \”הנקודה האחת\”: אל תנווט בין מודלים מעבר לנקודה אחת.

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

foo(driver.fleet.contract.cancellation_fee, ….)

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

ע\”פ חוק הנקודה האחת, הקוד שלנו היה אמור להראות כך:

foo(driver.fleet_cancellation_fee, ….)

כאשר למודל fleet ולמודל driver יש \”חסמי תלות\” – מן getters שמחזירים לנו תשובה ללא הצורך להמשיך לנווט ולהיות תלויים בתלויות הפנימיות שלהם עצמם.

כתיבת ה getters הללו היא כמובן לא \”DRY\”, ולשם כך ריילס מספקת פקודה בשם delegate שתקצר לנו את העבודה:

class Driver < ActiveRecord::Base
belongs_to :fleet
delegate :overtime_rate,
:cancellation_fee,
:to => :Fleet,
:prefix => true
end

class
Fleet < ActiveRecord::Base
has_many :drivers
has_one :contract
delegate :overtime_rate,
:cancellation_fee,
:to => :Contract
end

class
Contract < ActiveRecord::Base
belongs_to :fleet
end

משמעות השימוש ב delegate בדומה לעיל היא כזו:

  • אם מבקשים מ driver את דמי הביטול – העבר את הבקשה ל Fleet.
  • אם מבקשים מה Fleet את דמי הביטול – העבר את הבקשה ל Contract.
כאשר ברמת ה Driver אנו מוסיפים את ה prefix עם שם ה delegate בגישה לשדות – עבור הקריאות / בגלל הקונבנציה.

בעתיד, כאשר נרצה לעשות שינוי בקוד ולהביא את דמי הביטול לא מתוך חוזה, אלא מתוך חישוב אחד – נוכל לעשות שינוי רק במחלקה Fleet ע\”י כתיבת getter בשם מתאים – והסרת ה delegete.
אם דמי הביטול יהיו דינאמיים לכל נהג – נוכל פשוט להוסיף getter בשם fleet_cancellation_fee על מודל הנהג – ולהסיר את ה delegation. שינוי במקום אחד וללא פחדים.

העיקרון הוא אותו עיקרון מאחורי השימוש ב getter ו setter – אבל הוא אפילו יותר שימושי: שינויים בקשרים במודל הם יותר נפוצים (ויותר כואבים לשינוי?) מאשר התסריט של הפיכת member במחלקה לערך מחושב – התסריט עליו getters/setter מגנים.

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

כאשר נאלצים לעשות שינויים \”עקומים\” בקוד אנו מאשימים את אנשי ה product – שלא מסכימים \”לשאת במחיר השינוי הנכון\”: שבוע עבודה וסכנה מסוימת לרגרסיות. \”למה הם לא מוכנים להשקיע באיכות???\” – אנחנו שואלים.

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

מקוצר זמן, אתאר את שני ה best-practices הבאים בקצרה בלבד:

ביצוע כל פעולות ה find מתוך המודל (ולא חס-ושלום מתוך ה View)

אפשר, ואולי נוח לבצע פעולות find על המודל מתוך ה View, כמו ב PHP או ASP של שנות ה-90!   😉
יש בכך 2 בעיות:

  • סבירות גבוהה לשכפול קוד – כי כמה Views זקוקים לאותו חיפוש.
  • קשה להרכיב תמונה ברורה של הגישות לבסיס הנתונים. כאשר רוצים לעשות שינויים במודל – לא ברור אילו השפעות יהיו לו, כי הקוד הרלוונטי \”מפוזר\” ברחבי המערכת וקשה למצוא את כולו. 
התוצאה – נטיה להעדיף שינויים פחות מסוכנים, אך גם פחות טובים. \”אולי נכתוב trigger בבסיס הנתונים – וגמרנו?\”
נכון, אפשר להעביר את פעולות ה find ל controller ולקבל 90% מהתמורה. לוגיקת הגישה לבסיס הנתונים עכשיו מפוזרת על 2 מחלקות בלבד. אבל למה לא לחסוך את הצעד הקטן הנוסף?? פשוט שימו את כל לוגיקת הגישה לבסיס הנתונים במודל – וחסכו בלבולים וחוסרי הבנות.

שחרור לוגיקה שאיננה חלק מהמודל למחלקות משנה

ככל שמחלקת המודל תהיה גדולה יותר – יהיה קשה יותר לעקוב אחרי מה שהיא עושה. לא נדיר למצוא פעולות parsing בתוך המודל, למשל: to_json  ו parse_json => סוג של מומחיות משנה של המודל.

למען הסדר הטוב, הוציאו קוד שאינו קשור ישירות למודל למחלקות-משנה (או module-משנה) של המודל. למשל:

  • לוגיקת parsing / serialization. יתרון נוסף – תוכלו לבדוק (בדיקות-יחידה) את הקוד הזה בקלות.
  • finders – אם יש הרבה.
הכל במידה, כמובן. אם יש מתודת parsing אחת באורך 4 שורות – זה יהיה כנראה מיותר להוציא אותה החוצה.

סיכום

טוב, סקרנו את ActiveRecord – בצורה שתאפשר לקבל תמונה טובה במה מדובר.
בפוסט הצלחתי לכסות רק חלקים מסך היכולות של AR: לא דיברנו על transactions, לא נכנסו לנבכי אפשרויות ה validation, ודילגנו על הרבה אפשרויות ותכונות הקיימות ב AR. AR הוא ותיק, ועשיר באפשרויות.

הערות יתקבלו בברכה.

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

—–

לינקים מעניינים:

http://blog.rubybestpractices.com/posts/gregory/055-issue-23-solid-design.html

Bypassing ActiveRecord for better performance

דרכים לעדכון מודל של AR

רובי: RVM

RVM הוא כלי בסיסי ונפוץ מאוד ברובי המאפשר לנהל כמה גרסאות רובי שונות על אותה המערכת.

אם תנסו לפתח רובי על מערכת ההפעלה \”חלונות\” – RVM עשוי להיות האכזבה הראשונה שלכם. כ\”כ הרבה מדריכים מניחים שאתם משתמשים ב RVM – אבל RVM לא רץ על חלונות (רק יוניקס / לינוקס / מק). החלופות הקיימות לחלונות (למשל pik) – הן לא ממש טובות.

RVM הוא קיצור של \”Ruby enVironment Manager\” (כפי שמוסבר באתר), אם כי באותו האתר, בכותרת בגדול, מצוין דווקא הקיצור הישן (\”Ruby Version Manager\”). צחוקים.

RVM מאפשר:

  • להתקין גרסאות רובי שונות (נקראות Rubies) על אותה מערכת ההפעלה, ללא התנגשות (למשל 1.9.3, 2.1.0, ו 2.1.5).
  • להחליף בכל רגע את גרסת הרובי \”הנוכחית\” בפקודת shell פשוטה.
  • להצמיד גרסת רובי מסוימת לתיקיה מסוימת במערכת הקבצים (כלומר: לפרויקט רובי שנמצא בתיקיה הזו).
  • לנהל רשימה נפרדת של Gems (נקרא gemset) לכל גרסת רובי או לכל פרויקט.
מה מיוחד ברובי, שכלי כמו RVM הופך ל\”הכרחי\”? מדוע בג\’אווה, למשל, אין כלי דומה?

  • RVM מפשט את ההתקנה של רובי. בג\’אווה זה היה פשוט, ואין קושי להתקין כמה גרסאות זו לצד זו.
  • אי-היכולת לנהל רשימה נפרדת של Gems לכל פרויקט היא סוג של \”חור\” במנגנון ה Gem המקורי. RVM מציג פתרון לבעיה זו אם כי גם Bundler, כלי נפוץ מאוד אחר – גם הוא מציע פתרון לאותה הבעיה.
  • התאימות לאחור של המפרשנים של רובי (יהיו אלו Rubinius, YARV, או JRuby) היא פחות טובה מהמקובל בג\’אווה. אם פיתחתם אפליקציה על רובי 2.0.3, כנראה שלא תרצו להריץ אותה על מפרשן של גרסה 2.0.5, ובטח לא על מפרשן של 2.2.0.
    יש פה גם עניין תרבותי, אני חושב: הסבירות שמפתחי ג\’אווה יבחרו לעבוד על JDK 1.7 עם patch level מסוים – כי פרויקט אחר שלהם על אותה הגרסה (למרות שיש כבר ג\’אווה 8), היא גבוהה יותר מהסיכוי שמפתחי רובי יעשו כן. אנשי רובי נמשכים חזק יותר ל\”חדש\”. לעבוד עם רובי 2.1.3, כשיש רובי 2.2.0 (בטא) כבר בחוץ? חחחחחחח!

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

הדוד בוב (Robert C. Martin), בתחפושת \”נער הרובי\” שלו…

אמונות מקובלות לגבי RVM – נכון, או לא נכון?

\”RVM מתאים בעיקר לפיתוח, ב Production הוא עושה בעיות וכדאי להימנע ממנו\”.

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


RVM עובד רק עם Bash

ובכן, RVM מבצע שינויים בקובץ ה bashrc. בזמן ההתקנה.
הוא עצמו כתוב ב shell script, והוא מניח שמספר כלי shell זמינים לו ב path (למשל awk, sed, tar, git וכו\’), וכמו כן הוא מניח על זמינות של כמה פקודות shell (מערכים וכו\’).

אני עובד עם zsh – והוא עובד לי מצוין. אם אתם עובדים עם shell אחר, כנראה שיהיה עליכם לעשות כמה צעדים ידניים (קונפיגורציות + וידוא זמינות הכלים הנחוצים ל RVM). חפשו בגוגל.

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

אסור להפעיל RVM עם Sudo (וללא רשות מרגע ודודלי!)

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

מקור המגבלה הוא השפה בה RVM כתוב, שהיא shell script. כשעובדים ב shell scripts, כמה משתני סביבה ו paths הם שונים בתור משתמש root – מה שמשבש את ההנהגות של RVM, וכנראה ללא דרך מוצא פשוטה.

\”את RVM לא ניתן להסיר! פרמטו את המחשב – בכדי להסיר אותו!!\”

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

האמונה הזו כנראה נובעת מאנשים שמצאו תשובות שגויות / חלקיות בגוגל, מה שסיבך להם אח\”כ את תהליך ההסרה. או אולי, חס וחלילה, מישהו שהתקין את RVM עם sudo, ואז בלי sudo – ואז ניסה להסיר….

RVM הוא העבר, יחי rbenv! … או chruby! … ?!

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

אם תבחנו את ההבדלים בין RVM ל rbenv, למשל – תראו שהכלים דיי דומים. RVM מכסה קצת יותר קרקע, והיא ממומשת בצורה אחרת: עטיפה פקודת ה cd של ה shell (פקודה מאוד נפוצה) ובמחיר של כמה עשרות ms נוספים לכל הפעלה של הפקודה, בעוד rbenv עוטפת בעזרת \”shims\” את gem את רובי ועוד כמה קבצים אחרים – מה שמסוכן לא פחות.

To paraphrase Jonathan Jackson, RVM is to Rails as rbenv is to Sinatra. Sinatra is a lightweight framework whereas Rails is much more robust. Sometimes Sinatra just fits, and other times you\’d be a fool to not go with Rails.

לא יודע… עבורי הרעיון של ללכת עם האופציה הבסיסית והפשוטה (יהיה זה Sinatra או Lotus) – דווקא קוסם.

אז איך נראית העבודה ב RVM (בקצרה)?

אחרי שהתקנתם את RVM, הנה כמה פעולות שימושיות:

$ rvm install 2.0.0

יתקין את רובי גרסה 2.0.1, וייצור לה gemset בשם default. ניתן לציין גם patch level מדויק.

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

$ rvm use 2.0.0

האם אנו רוצים לשנות ידנית את גרסת הרובי בכל מעבר בין פרויקטים? ברור שלא!

ניתן ליצור קובץ rvmrc. שיעשה זאת עבורנו. נכנס לתיקיית הפרויקט ונקליד:

$ echo \’rvm use 2.0.0\’ > .rvmrc

כשנצא מהתיקייה ונחזור אליה – RVM ישאל אותנו אם אנו יצרנו את הקובץ, ואם כן, הוא \”יאשר\” אותו ויחליף את גרסת הרובי הנוכחית אוטומטית בכל כניסה לתיקיה. את קובץ ה rvmrc. כדאי להפקיד ב git repository שלנו.

האמת, שיש תקן, עליו הסכימו מנהלי הגרסאות השונים של רובי לקבצי קונפיגורציה. בואו נשתמש בו:

$ rm .rvmrc
$ echo \’2.0.0\’ > .ruby-version

יש יתרון בסטנדרטיות לא רק במחשבה על מעבר מהיר (הפעלת שורה אחת פחות?!) לכלי ניהול גרסאות אחר – אלא בעיקר כי מספר כלי צד-שלישי מכבדים את הפורמט המשותף, אך לא מכבדים את קובץ ה rvmrc.
היתרון של קובץ rvmrc. הוא שמדובר בקובץ סקריפט לכל דבר שמופעל בעת כניסה לתיקיה. השורה שהכנסנו למעלה rvm use היא פקודת shell לכל דבר, וניתן להוסיף פקודות נוספות.

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

$ echo \’ruby-2.0.0\’ > .ruby-version

יופי!

עכשיו בואו נרחיב את השימוש ל gemsets. ניצור gemset חדש (בשם \”project_name\”) תחת גרסת הרובי הנוכחית (2.0.1), ונהפוך גם את ה gemset שיצרנו להיות הנוכחי הנוכחי:

$ rvm gemset create project_name
$ rvm use 2.0.0@project_name

הקונבנציה המקובלת היא לקרוא ל gemset כשם ה root folder של הפרויקט. RVM משתמש בפורמט של ruby_version@gemset_name בכדי לתאר צמד. נזכיר ש gemset משוייך לגרסת רובי מסוימת.

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

$ echo \’project_name\’ > .ruby-gemset

ייתכן ותתקלו בהמלצה לוותר על קובץ ה ruby-gemset. ולכתוב בקובץ ה ruby-version. את הפורמט של RVM, קרי \”ruby-2.0.0@project_name\”. זה יעבוד לכם עם RVM, אך עלול לא לעבוד עם כלים אחרים (זה לא חלק מהסטנדרט).

אם נקרא ל gem list – נראה רק את ה default gems שמגיעים עם רובי גרסה 2.0.0 (יש כ 14 כאלה).
נוודא, ליתר בטחון, שאנו משתמשים בגרסת הרובי וה gemset שאליו התכוונו, ע״י פקודת

$ rvm info | grep HOME


(הדרך הכי קצרה שאני מכיר)
נתקין gem לדוגמה, ונראה שהוא נוסף ל gemset:

אתם יכולים לקרוא ל rvm use 2.0.0 ואז ל gem list בכדי לראות שה gem שהתקנו לא נמצא ב default gemset.

את ה gem עצמו תוכלו למצוא בתיקיית ה gemset המנוהלת תחת התיקייה rvm/gems./~.
זה, פחות או יותר, הבסיס.

RVM מספק עוד כל מיני יכולות. הנה פקודה נחמדה:

$ rvm all do ruby test.rb
המריצה את קובץ הרובי (יש עוד אפשרויות) בכל הגרסאות המותקנות של רובי במערכת, ומסייעת לבדוק הבדלי תאימות / ביצועים / וואט-אבר, בין גרסאות הרובי השונות.

אלטרנטיבה לשימוש ב gemsets

יש כאלו שמעדיפים להימנע משימוש ב gemset, ולהשאיר את ניהול ה gems ל bundler.
למה? אולי זה עושה יותר סדר בראש, אולי בגלל יכולות של bundler כמו פקודת bundle update שתעדכן את כל ה gems לגרסאות עדכניות – כולל התלויות שלהן. לכו תעשו את זה ידנית בעזרת gem update[א]
אם אתם משתמשים ב bundler, אז:
  • קובץ ה Gemfile מנהל עבורכם את הגרסאות השונות של ה gems.
  • את התקנת ה gems אתם עושים, כנראה, ע\”י עדכון של ה Gemfile והרצת הפקודה bundle install.

RVM נוצר בכדי לפתור \”חורים\” ב RubyGems, אבל הנה bundler עושה את אותו הדבר בעצמו.

    אם אתם בוחרים בגישה זו חשוב לשים לב ש RubyGems מריץ תמיד את הגרסה האחרונה המתוקנת במערכת של ה gem שציינתם.
    מצוין לכם ב Gemfile את rake גרסה 10.0 בעוד פרויקט אחר דרש את גרסה 10.1? – שתי הגרסאות מותקנות ובהפעלת הפקודה rake תרוץ הגרסה המאוחרת (10.1) על אף שהגרסה המצוינת ב Gemfile היא 10.0. יותר גרוע: התלויות של rake גם הן תבחרנה בגרסה המאוחרת ביותר שצוינה כתואמת. \”סמוך על סחבק\” – מה שנקרא.
    מה עושים?
    פשוט זוכרים להפעיל את rake בצורה:
    $ bundle exec rake

    שזו בעצם הדרך לעבוד עם bundler, כהגדרה. (אנו פשוט נוטים לשכוח)
    לא אוהבים להקליד bundle exec כל הזמן? הפקודה:
    $ bundle install –binstubs
    תיצור עבורכם תיקיית bin שבה יהיו wrappers לכל הפקודות הרלוונטיות (נקראים binstubs, ריילס משתמשת בהם כברירת מחדל), שרצים ב context של bundle exec.

    סיכום

    האם זה הגיוני שניהול gems שונים לכל פרויקט דורש צד-שלישי שאינו חלק מהפלטפורמה של השפה?
    האם ייתכן שכלי כ\”כ נפוץ כתוב בעצם ב shell script (כלומר: 20 אלף שורות של shell script), עם כל המגרעות והבעיות שבדבר? שכל כלי בתחום מציב כמה ״חוסרי נוחות״ שיש להתמודד איתם?

    ברוכים הבאים לעולם הרובי! אני בעצמי עוד מתרגל 🙂

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

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

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

    [א] נראה ש RVM מקנאה ביכולת הזו, והיום היא מציעה יכולת דומה בעזרת הפקודה do:

    $ rvm all do gem update -V
    אני לא יודע לומר אם פקודה זו גם מעדכנת את ה gems שהם תלויות, בצורה רקורסיבית.

    מקורות / לינקים רלוונטיים:

    ריילס, רובי on ריילס

    ריילס, או בשמה המלא Ruby On Rails (קיצור מקובל: ROR) היא מסגרת פיתוח (Framework) פופולרית לבניית אפליקציות ווב.

    ספציפית: אפליקציות עם ממשק וובי מצד אחד, ובסיס נתונים (בד\”כ רלציוני, אבל לא רק) – בצד השני. 
    מדוע ריילס?
    ריילס הגיעה כתגובה ל JEE (בימיו הפחות יפים – תחילת דרכו) וכאלטרנטיבה פשוטה ומהירה לכתיבת אפליקציות ווב. באותם הימים, מרכז הכובד של JEE היה ה Enterprise JavaBeans (בקיצור EJB) שהיו כבדים ומסובכים. למשל, ה Guideline היה לכתוב לכל רכיב שמייצג נתונים 4 Interfaces נפרדים (רגיל, Home, Remote, ו LocalHome – אם אני זוכר נכון). ל EJB היו ה-מ-ו-ן קובצי קונפיגורציה XML לא-ידידותיים, ולמרות הכל – זה היה טרנד חם ורבים עברו לעבוד בו.
    JEE הביא יתרונות רבים (סביבת multi-platform סטנדרטית, המתאימה ל Enterprise), אך במחיר גבוה. הוא כנראה היה טוב יותר מ Microsoft DNA או CORBA (כנראה…), אבל היו אנשים שחשבו שאפשר להשיג את הערך בעלות נמוכה יותר.
    אחד מהם הוא Rod Johnson שיצר את Spring Framework, ה Framework שהפך הכל להרבה יותר פשוט (בעיקר הציג אלטרנטיבה פשוטה ל EJBs) – ומה שאח\”כ הפך להיות הסטנדרט של JEE, בצורת EJB גרסה 3.0 (שהעתיקה הכל, כמעט, מ Spring + Hibernate).
    חלוץ אחר הוא David Heinemeier Hansson (ידוע גם בקיצור DHH), בחור דני צעיר יחסית (יליד 79) שעבד בחברה בשם 37singals (היום נקראת basecamp) שפיתחה לעצמה Framework יעיל מאוד לפיתוח מערכות ווב בשפת רובי. ה Framework הזה יצא כ Open Source ונקרא Ruby on Rails. נראה שריילס, אגב, השפיעה באזורים מסוימים גם על Spring בגרסאותיה המאוחרות (אני לא מדבר רק על Spring Boot) – ומשם היא השפיעה גם על JEE. בכלל, ריילס הוא Framework רק השפעה, שניתן למצוא השפעות שלו במקומות רבים בעולם התוכנה.

    מקור השם Rails הוא כזה: \”במקום להתקדם באיטיות, כל אחד במסלול שלו, הצטרפו אלי: אני קובע את הדרך – אבל תוכלו לנוע במהירות\”. ריילס הוא יישום של אוסף של best practices (ש DHH האמין שהן מוצלחות, לא אוסף גנרי) – שמתאימים זה לזה, ומסביבם נכתב Framework שהופך את השימוש בהם למאוד קל ומאוד מהיר. ריילס הוא לא סיפור על פלורליזם – זהו סיפור על דעה מוצקה כיצד נכון לפתח אפליקציות ווב. אם אתם רוצים להיות יעילים בריילס, עליכם לקבל עליכם את \”The Rails Way\”, או לפחות את רובה. אם תקבלו – תגלו את אחד ה Framework הפרודקטייבים הקיימים לפיתוח אפליקציות ווב. ריילס פרחה ביוחד בסביבות של סאטראט-אפים, שהדבר שהכי חשוב להם הוא לספק Value מהיר לשוק שאותו הם עוד לומדים.

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

    התכונות העיקריות של ריילס

    ההחלטה התכנונית המרכזית של ריילס, אם כן, היא \”פריון (productivity) על פני גמישות\”.

    שני עקרונות מרכזיים שנוהגים להזכיר שוב ושוב, בהקשר של ריילס, הם:
    • Convention over Configuration – במקום לומר היכן נמצא משהו (קובץ, משתנה, וכו\’) ומה שמו – בואו נגדיר כלל מוסכם (convention) היכן הוא אמור להימצא / איך הוא ייקרא – ונחסוך לנו את כל התקשורת הזו. הרעיון של CoC הוא לא חדש, ניתן למצוא אותו במייבן, למשל, שקצת קדמה לריילס. בכל זאת, ריילס היא זו שעשתה PR משמעותי מאוד לעקרון התכנוני הזה (ובמיוחד כ contra ל JEE) – והשפיעה בכך רבות על התעשייה.
    • Don\’t Repeat Yourself (בקיצור DRY) – גם זה רעיון שכבר מקובל עשרות שנים, אבל ריילס חרטה אותו על דגלה. לא לכתוב בקוד שום דבר פעמיים.
    בעזרת עקרונות אלו (ועוד כמה אחרים) הצליחה ריילס ליצור סביבה סופר-פרודקטיבית לכתיבת אפליקציות ווב. כלל אצבע אחד אומר שבריילס יהיה עליכם לכתוב חמישית משורות הקוד בג\’אווה בכדי להשיג את אותה התוצאה.
    ריילס צמחה על גבי שפת רובי, קצת רחוק מהקהילה הטבעית של ג\’אווה (ה Enterprises), ולכן ההשפעה שלה הייתה קצת יותר רחוקה. אל דאגה! JEE ננגחה שוב ושוב על היתירות של הקוד שהיא יצרה (בתרגיל הנדסי כ\”כ מרשים): גם ע\”י NET. (מי שזוכר את קרבות ה PetShop) וגם ע\”י Spring – בניצוחו של רוד ג\’ונסון (Rod Johnson), שגילה מעט מאוד עדינות כלפי החבר\’ה של JEE. היום מצבה של JEE טוב בהרבה מאשר היה באותם הימים, אבל עדיין – פיתוח בריילס נחשב מהיר בהרבה.
    עוד אלמנטים מרכזיים בריילס שכדאי לציין הם:
    הרחבה לשפת רובי:
    רובי, כתכונה של השפה מאפשרת (ומעודדת) שימוש ב metaprogramming בכדי להרחיב את השפה עצמה וליצור DSLs חדשים. יכולות אלו, הן אולי הסיבה המרכזית שריילס הצליחה להיות יותר אפקטיבית [א] מספריות ווב אחריות (ASP.NET MVC, Lavarel או !Play) – שהיו משוחררות גם הן מ\”כבלי ארכיטקטורת ה Enterprise של JEE\”. פקודות שימושיות בכתיבה של אפליקציות ריילס (למשל הגדרות שונות על המודל) הן בעצם הרחבות של ריילס לשפת רובי. מתכנת ריילס ללא רקע משמעותי ברובי עשוי לא להיות מסוגל להבחין אלו תכונות בהן הוא משתמש שייכות לשפת רובי – ואלו שייכות לריילס (ואולי גם לא ממש אכפת לו).

    אינטגרציה:

    ריילס כוללת stack של טכנולוגיות שמחוברות טוב מאוד זו לזו, ומספקות חווית פיתוח אחידה end-to-end. החל מבסיס הנתונים (Active Records – כלי ה ORM של ריילס), ועד צד הלקוח (SASS, Haml, ו CoffeeScript [ה]) – הכל מרגיש מתאים, ודומה למדי. החוויה דומה, אולי, לזו של פיתוח בפלטפורמת NET. של מייקרוסופט – לה שליטה רחבה על כל ה Stack הטכנולוגי.

    עד גרסה 2, ריילס הייתה gem [ב] אחד, שלא גמיש להחלפות. החל מגרסה 2 חלק מהרכיבים (Active Records, למשל) הופיעו כרכיבים הניתנים להחלפה, אם כי ההחלפה בפועל הייתה מאתגרת למדי. ובמשך הזמן ממש התפתחו plug-in APIs המאפשרים לעשות החלפות אלו בצורה פשוטה יותר ויותר.
    שלא תבינו לא נכון: אם אתם הולכים לכתוב אפליקציה ראשונה בריילס, ורוצים כבר בשלב זה להחליף רכיבים כראות עיניכם – זה לא ממש כדאי. החלפת רכיבים היא עדיין פרקטיקה מתקדמת שמומלצת רק למי שמבין היטב את דרכי העבודה של ריילס. באפליקציות הראשונות – כדאי להיצמד לסטדרטי ולמקובל.

    הגישה שרואה בריילס \”אוסף של רכיבים\” (מה שטכנית באמת נכון) – מהם ניתן להרכיב תצורה מותאמת-אישית, נקראת (גם) Hexagonal Rails (ע\”ש Hexagonal Architecture, שדוגלת בהפרדת ה domain model משאר הרכיבים, ורואה בהם סוג של plugins שמתחברים מסביב אליו) – והיא פחות נפוצה, ואולי אף שנויה במחלוקת: האם ההשקעה, הלא פשוטה, \”לאלף את ריילס\” מתירה בסופו של דבר תוצר שמצדיק את ההשקעה הזו?

    בדיקתיות:

    תכונה אחרונה של ריילס שארצה לציין היא \”בדיקתיות\”. יוצרי ריילס (ממש כמו יוצרי AngularJS, למשל) היו מודעים היטב לצורך בבדיקות – והם בנו תשתית שמאפשרת לבדוק את הקוד שרץ עליה בקלות יחסית. בדיקות יחידה ואינטגרציה הן חלק מובנה מריילס, ו flows הבדיקה מאופשרים היטב בכל הרמות , אם כי קצת יותר לכיוון ה integration tests וקצת פחות לכיוון ה isolated unit tests. כמות ספריות הבדיקה הזמינה לשפת רובי היא מרשימה (RSpec, MiniTest, Test::Unit, Cucumber, Factory_girl), ולכמה מהספריות הללו יש הרחבות מיוחדות לריילס (למשל Rspec_rails או Factory_girl_rails). בדיקות הוא לא רעיון זר בקהילת הרובי או ה RoR…

    מעניין לציין שדווקא DHH הוא זה בשנה האחרונה העלה כמה דברי ביקורת על כתיבת בדיקות. בגלל ש DHH קידם מאוד את תרבות כתיבת הבדיקות (וגם בגלל שהוא הגורו הבלתי-מערוער בעולם של ריילס?) – דבריו זכו לתהודה רבה. הוא כתיב פוסט בשם Test-induced design damage, ואח\”כ העביר סשן בשם \”?Is TDD Dead\”. בסוף הפוסט תוכלו למצוא לינק לסדרת הסשנים המעניינת בה הפגישו אותו עם יוצר ה TDD (קנט בק) ומרטין פאוולר (ששימש כמגשר?). סדרה של 3 שעות – אך בהחלט מעניינת.

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

    ריילס בחזית הטכנולוגיה

    בעולם של Single Page Applications, בעולם בו חווית משתמש שהפכה מקובלת, שדורשת שקוד משמעותי ירוץ בדפדפן, ובעולם של noSQL – ריילס כבר זוהרת פחות. אפשר לעשות את כל הדברים האלו עם ריילס, אבל היא כבר לא יעילה כמו שהייתה בעבר, ומפתחי ריילס צריכים לפעמים להיות יבשושים ולכתוב קוד משעמם / קצת פחות יעיל.
    אם אתם מחפשים את החלופה \”העכשווית\” לריילס שתוכננה במקור לקוד צד-לקוח משמעותי, ו MongoDB – אתם כנראה מחפשים את MeteorJS (עם כל היתרונות, וגם החסרונות שלה).

    אכן נראה שחלק ממפתחי הריילס (כלומר: רובי + ריילס) עוזבים את ריילס ומחפשים Frameworks חדשים לעבוד איתם. הטיעון ששמעתי כבר כמה פעמים הוא \”אני לא רואה הגיון לעשות MVC גם בשרת וגם בצד הלקוח\” (ברור שלא! מוזר לשמוע בכלל ניסו כזה דבר). באופן טבעי, חלקם מחפשים את המוכר, ולא מפתיע למוצא frameworks שמנסים לספק את הצורך הזה. קשה לי שלא לציין את Sails – של node.js ו grails של שפת גרובי (groovy). האחרון הוא לא חדש – אבל קשה להתעלם מהצליל המוכר שהוא בחר לעצמו 🙂

    הרכיבים העיקריים בריילס

    ריילס בבסיסה היא ספריית MVC שמושפעת ישירות מה MVC (המקורי) של שפת smalltalk:

    המודל – שומר את ה state של המערכת (בעזרת בסיס הנתונים, אם כי לפעמים רק בזכרון), אוכף \”חוקים עסקיים\” על נכונות ה data (למשל: סטודנט חייב להיות רשום לתוכנית לימודים כלשהי). הוא לא בהכרח המאמת היחידי של הנתונים, אלא ה gatekeeper לפני שהמידע נשמר / משותף עם חלקים אחרים במערכת. בניגוד ל MVC של Smalltalk הוא גם כולל חלק מהלוגיקה – ראו בהמשך את ההסבר על Active Records.
    המודל, בגדול – הוא זמין לכלל האפליקציה, אין בו partitioning (או bounded context).

    ה View – הוא הקוד שמייצר דפי HTML למשתמש, על בסיס מידע במודל. ריילס היא תשתית לכתיבת MVC בצד-השרת (כלומר: השרת מייצר HTML, ומוסיף JavaScript לאינטרקטיביות). בדומה למודל ה MVC הקלאסי ה View לא מקבל input מהמשתמש, ולא מנהל איתו שום סוג של אינטרקציה (לפחות לא בצד-השרת).

    ה controller – אחראי לקבלת קלט מהמשתמש, טיפול בו, ותפעול (ניתן לומר: orchestration) של המערכת עד שהמשתמש מקבל בחזרה את התשובה שלו. דרך העבודה שלו היא לעדכן את המודל, ואז להזניק את ה View המתאים (הוא לא מעביר מידע ל view ישירות).

    בריילס יש גם routers – שהם ממפים URLs, ומצבים – ל Views. לדוגמה: אם משתמש שהוא admin ניגש לדף מסוים – הוא יקבל View אחר ממשתמש רגיל. ה router עושה מה שבמערכות אחרות עושה סוג של \”super controller\”. הוא דקלרטיבי, בעזרת סט של ״פקודות״ מיוחדות לעניין (מה שהופך אותו לפשוט יותר לכתיבה ותחזוקה) – וסה\”כ הוא מהווה הרחבה מבורכת על מודל ה MVC ה\”קלאסי\”.

    רכיבים עיקריים של ריילס הם:

    Active Records (בקיצור: AR)

    כלי ה lightweight ORM של רובי, המנהל בפועל את הגישה לבסיס הנתונים של המודל. רובי מספקת גם כלים להגדרת טבלאות ברובי דקלרטיבי (ולא SQL, על הווריאנטים תלויי בסיס הנתונים הספציפי שלו) וכלים מובנים לביצוע migrations בין גרסאות שונות לסכמת בסיס הנתונים (מה שמקובל בג׳אווה לעשות עם flyway).

    השם \”Active Records\” הוא בעצם של שם של דפוס-עיצוב (מופיע בספר PoEAA) אותו המודול מממש.

    דפוס העיצוב \”Active Record\” מגדיר מחלקה בשפה (במקרה שלנו: רובי) שמתארת נתונים של שורה (להלן Record) בבסיס הנתונים, אבל מוסיפה עליהם גם את התנהגות ה domain הרלוונטית לאותם נתונים (להלן Active). למשל: validation, חישובים, derived fields, וכו\’. הבהרה: יש מחלקה אחת לכל טבלה, ומייצרים מופעים שלה ע\”פ הרשומות להם נזקקים באותו הרגע.

    דפוס עיצוב זה מרשה לערבב בין מיפוי הנתונים בין בסיס הנתונים לזיכרון, עם לוגיקה עסקית – כל עוד הלוגיקה העסקית היא רק ברמה של הרשומה הבודדת (ולא \”שייכת\” ל scope רחב יותר). ערבוב זה, נמצא בוויכוח תמידי על הלגיטימיות שלו, מכיוון שהוא סותר כלל מקובל בארכיטקטורה הנפוצה ביותר למערכות מבוססות-נתונים (הרי היא Layered Architecture) – הפרדה בין Persistence ל Business Logic, או בצורת דפוס העיצוב \”Data Mapper\” אותה מממשים רוב כלי ה ORM המוכרים – הפרדה מוחלטת בין לוגיקה עסקית למיפוי הנתונים בין בסיס הנתונים לזיכרון [ו].

    בריילס, יידרש מאמץ מיוחד בכדי לא-לעבוד עם Active Records, אז כדאי שתתנו לדפוס זה הזדמנות – גם אם כל חייכם \”גדלתם\” על Data Mappers ;-).

    Active Records מאפשר לנו לכתוב אובייקט \”פשוט\” ברובי, לרשת מ ActiveRecord::Base ולקבל את פעולות ה CRUD הבסיסיות ע\”י סיפוק הגדרות בסיסיות של מה שאנו רוצים (declarative programming).
    לא! זה לא \”בחינם\”, אין ארוחות-חינם בהנדסת תוכנה. אנו מוותרים על גמישות וקצת על ביצועים – בכדי לקבל את הנוחות הזו.

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

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

    • Action Pack – זהו הרכיב שמתפעל את ה MVC בריילס. כל פעולת משתמש בריילס נקראת \”Action\”. הוא מורכב מ -3 תתי-מודולים:
      • Action Dispatch – אחראי ל routing.
      • Action Controller – מספק את מחלקת ה ActionController::Base ממנה יורשים כל ה controllers במערכת. 
      • Action View – אחראי לרינדור ה Views, בעזרת מנגנון ה ERB (קיצור של Embedded Ruby) – מנגנון שדומה מאוד ל JSP בג\’אווה או ל PHP: סוג של template (בד\”כ HTML, יכול להיות אחר) שבו מושתל קוד רובי המתאר את ההתנהגות הדינאמית.
    • Active Model – המשמש כ Facade של AR מול ה Action Pack.
    • Active Resource – המספק יכולות צריכה של שירותי REST חיצוניים. סוג של שירות אינטגרציה בין מערכות.
    • Active support – סט של utilities כלליים (שימושיים) שמגיע עם ריילס. המקבילה של Apache Commons – של מג\’אווה.
    • Action Mailer – אשר תופס בתיעוד, משום מה, נתח מכובד. הוא בסה״כ שולח מיילים.
    • Active Job – לניהול Queues של jobs. הוא בעצם שכבת הפשטה למימושים קיימים (כגון Resque) ולא מימוש בפני עצמו. הוא חדש בריילס 4.2 .
    • Railties (\”קשרי מסילה\”) – זהו ה gluing logic של כל רכיבי ה Active / Action למיניהם. ה Strategy של תפעול אפליקציות הריילס.
    • Rails – רכיב קטן למדי, שאחראי על אתחול המערכת – והכנסת המודולים האחרים לפעולה.

    כל עבודת ה low-level ברמת ה HTTP הוא לא חלק מריילס. חלק זה יטופל ברמת ה Application Server – עליו נדבר בהמשך.

    מבנה אפליקציית ריילס

    כחלק מה Convention over Configuration, לאפליקציית ריילס יש מבנה תיקיות מוסכם-מראש. בכדי ליצור מבנה זה יש להקליד בשורת הפקודה:

    $ rails new [optional: -d ]

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

    מבנה אפליקציית ריילס חדשה

    התיקיות המעניינות ביותר באפליקציית הריילס הן תיקיות ה app, config, ו lib. אם יש בדיקות טובות – הוסיפו לכך (במקום גבוה) את תיקיית ה tests (לפעמים תראו אותה בשם spec).

    תיקיית ה bin כוללת את הסקריפטים לפעולות הבסיסיות של האפליקציה. אלו בעצם wrapper scripts (נקראים ברובי גם binstubs) שרק מכינים את סביבת הריצה ואז קוראים ל executable הנכון. לפני ריילס גרסה 4 קראו לתיקיה זו בשם \”scripts\”.

    קובץ ה Gemfile (מתחיל באות גדולה) הוא קובץ של כלי רובי בשם bundler, כלי שעוזר להגדיר, ואז להתקין – תלויות (gems [ב]) על סביבות חדשות בהן תרוץ האפליקציה.
    ברגע שמריצים את הפקודה \”bundle install\”, אז bundler יקרא את קובץ ה Gemfile, יבדוק מהן הספריות הנדרשות וגרסאותיהן, ויתקין את מה שלא נמצא – בגרסאות המתאימות. אם אתם מכירים את קובץ ה package.json של node.js – זה ממש אותו הדבר.

    קובץ ה Gemfile שנוצר בעקבות הפקודה \”rails new\” כולל כבר רשימה של ספריות שימושיות: דרייבר לבסיס הנתונים שהגדרתם, כמה ספריות צד-לקוח, ספרייה ל generation של תיעוד, ספריה לעבודה עם JSON, וכו\’.

    Gemfile.lock הוא קובץ בו bundler מנהל לעצמו את הגרסאות שמותקנות בפועל. זהו קובץ שתרצו להכניס ל git repository שלכם – אך לא לשנות ידנית (אין קשר לקבצי lock של MS Office, שמטפלים ב concurrency).


    קובץ ה rakefile הוא של כלי ה build של רובי שנקרא rake. ה build ברובי כולל בעיקר בדיקות, פענוח הסכמה, Linting, וטיפול בקצבי HTML ו CSS (למשל: minification).

    קובץ ה rakefile של ריילס יהיה לרוב מינימלי, והדבר המרכזי שהוא יעשה הוא לטעון ולהריץ רשימה של rake tasks (שאתם תכתבו). קבצי ה task יושבים בתיקיה lib/tasks.

    בריילס, עובדים לא מעט עם command line עבור scaffolding [ג]: ניהול בסיס הנתונים, ביצוע buid, הרצת האפליקציה, ועוד.

    לפעמים הפקודות הללו לא עובדות כראוי, ו\”טריק\” מקובל הוא לקרוא ל  <bundle exec rake <rake params – מה שהופך את כל ה gems שמצוינים ב Gemfile לזמינים, אפילו אם הם לא ב path של מערכת ההפעלה.

    התיקייה lib, ע\”פ הגדרה אחת, כוללת קוד שהוא לא מודל, controller, router, או View.
    התיקייה lib, ע\”פ הגדרה נוספת, כוללת קוד שניתן לעשות שימוש חוזר בו בין הרכיבים השונים (view, model, controller).
    מלבד כמה תתי ספריות נדרשות (למשל tasks) – יש לכם חופש כיצד לארגן אותה.

    בגרסאות ישנות של ריילס, כל הקבצים שישבו ב lib נטענו כזמינים לכל הרכיבים במערכת. כלל זה שונה והיום עליכם להדיר במפורש באיזה lib רכיב ה app שלכם רצה להשתמש – בכדי להשתמש בו. ( או פשוט להוסיף ל config/application.rb את הפקודה config.autoload_paths += %W(#{Rails.root}/lib שתעשה מה שריילס עשתה בגרסאות ישנות).

    נוהגים לומר שהקהילה של ריילס \”התבגרה\” בשנים האחרונות. ריילס \”ניסתה לדלג\” על כל מיני כללים מקובלים של הנדסת תוכנה בכדי להאיץ את קצב הפיתוח. חלק מהדילוגים הללו הוכחו כבעייתיים – ואז ריילס חזרה בה (נראה לי שהמקרה הנ״ל הוא אחד כזה). דילוגים אחרים – אולי הוכחו כלא בעייתיים, בהקשרים בהם ריילס עובדת. אני זוכר ששמעתי דיונים על כך בקהילת הג\’אווה לפני מספר שנים: \”הנה ריילס מתבגרת, והחברה שם עושים היום יותר ויותר הנדסת תוכנה כמו ג\’אווה\”. המסקנה שהוסקה (שגם לי נראתה הגיונית, בזמנו) הייתה ש\”ג\’אווה צדקה\” (שהחילה על עצמה כל כלל אפשרי של הנדסת תוכנה) – וריילס \”בדרך להיות סוג של ג\’אווה\”.
    נו טוב, סתם רכילות cross-קהילתית.

    שני קבצים חשובים שנטענים בעליה של ריילס הם config/environement.rb ו config/application.rb.
    בנוסף תמצאו בתיקיה config/environemnts קובץ קונפיגורציה לכל סוג סביבה: development, production ו test – זה הבסיס למערכת ה staging של ריילס.
    כשמפעילים את אפליקציית הריילס, ניתן לציין את הסביבה כפרמטר, למשל:

    $ rails server – e development

    בכדי לטעון קונפיגורציה לסביבת ה staging המתאימה.

    זהו.

    ייתכן ותתקלו בקבצים ריקים בשם ״keep.״. מטרתם היא למנוע מכלים מסוימים להתעלם מהתיקיות שנוצרו ע\”י \”rails new\” – וניתן (ומומלץ) למחוק אותם כאשר התיקיה מתמלאה בתוכן. חבל לראות פרויקט ריילס בן כמה שנים – שעדיין שמר את הקבצים הללו.

    החידושים שהגיעו עם ריילס 4, ששוחררה באמצע 2013. גרסה מג\’ורית של ריילס משוחררת אחת לשלוש שנים, בערך. מקור.

    ה Application Server

    הפעלת האפליקציה נעשית ע\”י פקודת \”rails server\”, או בקיצור \”rails s\”.
    כל אפליקציית ריילס רצה על \”שרת ווב משלה\”, בתוך תהליך של מערכת ההפעלה. ניהול של כמה אפליקציות על אותו שרת פיסי, מתרחשת ע\”י הפעלה של כמה \”תהליכי שרת\”, אולי מסוגים שונים ו/או גרסאות שונות.

    ריילס מגיע כברירת מחדל עם שרת \”רזה\” בשם WEBrick. זהו שרת פשוט למדי – שמשמש בעיקר לצורכי פיתוח (ולא ל production). ב Production מקובל להשתמש ב Puma, Raptor או ב Unicorn.

    Rack הוא שמה של הספציפיקציה של שרת ווב ברובי, וגם השם של ה Reference Implementation שלה.
    Rack דומה בתפקידו ל Servlet API של ג\’אווה, שבמקרה יש לו גם מימוש באותו השם (דמיינו ש Tomcat נקרא \”Servlet\”). החלק החשוב יותר של Rack הוא הספסיפיקציה. כנראה שהחבר\’ה של רובי ראו מה קרה בעולם של פייטון, שבו לא היה תקן שכזה, וכל שרת ווב עושה את אותו הדבר – אבל מגדיר זאת אחרת.

    סה\”כ ה API ש Rack מציע הוא מאוד בסיסי: הוא עובד ברמת הפשטה מאוד נמוכה – ה HTTP Request-Response Lifecycle (בדומה ל CGI – אם אתם ותיקים מספיק בכדי לדעת מה זה).
    מצד מימושי השרת, המימושים הנפוצים ביותר הם Rack (כמובן), Unicorn, או Mongrel.
    ספסיפקציית ה Rack משתמש לא רק את RoR, אלא גם Frameworks אחרים כגון Sinatra, Ramaze ו Merb (שמוזג לתוך RoR בגרסה 3.0).

    כמה מלים על Rack כ reference implementation:
    מקור השם Rack הוא הכוננית בחדר השרתים שעליה מתקינים את השרתים / ציוד הרשת. יוצריו בחרו בשם זה בגלל מנגנון ה plugins שלו, שמאפשר למודולים צד שלישי (הנקראים middleware, דומים ל Servlet Filters בג\’אווה) להרחיב אותו בקלות. הנה רשימה של מודולים זמינים.
    מעניין להזכיר את Rack::MockRequest שמייצר mocks לאובייקטי request ו response (נחמד שזה מגיע מ\”הספק\”), ו Rack::Lint שעושה Linting על קוד המשתמש ב API של Rack ומוצא כמה common pitfalls.

    קובץ הקונפיגורציה של Rack, הנקרא config.ru מכיל את ההגדרה של ה middlewares שאנו משתמשים בהם, ו metals.
    Rails Metal Apps (בקיצור: metal, מלשון \”bare-metal\” – אני מניח) הן חתיכות קוד באפליקציית הריילס שלנו, שירוצו ישירות מעל ה API של Rack, ללא תוספות.

    מדוע לעשות זאת? בגלל ביצועים, למשל: כדי לספק REST API שקוראים לו בקצב מהיר.

    ה stack של ריילס שמריץ את ה controllers (ה Action Controller Stack) מציב תקורה משמעותית לכל request שמטופל. אם אנו רוצים לספק תשובה פשוטה, ולעשות זאת מהר – כדאי לעקוף אותו.

    הערה: ברור שאם אנו זקוקים לביצועים ממש גבוהים – גם metal עדיין מציב מגבלה על מה שאפשר להשיג מהחומרה. שפת רובי היא לא concurrent (השתפרה, אבל עדיין לא ממש) ואין לה parallelism. כדי להגיע ל throughput או tps גבוה במיוחד – כדאי לשקול עבודה נקודתית עם node.js (עבור קוד \”רעב\” ל I/O) או עבודה ב Go (עבור קוד \”רעב\” ל CPU) [ד].

    אחרונה חביבה, היא ספרייה בשם spring שעובדת עם rake (שהיא כבר חלק מובנה מריילס 4.1 ומעלה) שעושה preloading לשינויים באפליקציית הריילס מבלי לעשות restart לשרת (אלא על בסיס של fork של ה process – לא עובד ב Windows או ב JRuby). יכולת דומה קיימת באופן מובנה בשרת ה Unicorn.

    ה Conventions של ריילס

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

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

    שמות של טבלאות בבסיס הנתונים יהיו ב snake_case – כמו משתנים. טבלאות תמיד יקראו ע\”פ צורת הרבים של האובייקט (plural): למשל People או Invoices ולא Person או Invoice.
    למה? כדי שהקוד יהיה קרוב יותר לשפה הטבעית: \”Select a Product from products\”. אני מניח שהבדל זה יכול גם להקל להבחין מהר בין מחלקה לטבלה בבסיס הנתונים.

    שמות של קבצים (model, view, controller, וכו\’) יהיו גם הם ב snake_case – כפי שמקובל ברובי גם מחוץ לריילס.

    נניח שאנו רוצים ליצור מודל בשם LineItem (שם מחלקה ברובי הוא CamelCase). כדי שהכל יעבוד, ללא קונפיגורציה נוספת, על שם הקובץ המכיל את המחלקה להיות line_item.rb (והיא תהיה בתיקייה app/models), ועל שם הטבלה בבסיס הנתונים להיות line_items.

    \”אני מבין איך עושים זאת עבור line_items, אבל איך *לעזאזל* ריילס תקשר עבורי בין people ל person?\”. שאלה טובה. הציצו ב utility של ActiveSupport שעושה זאת: inflections.rb. הוא מכיר יוצאי-דופן מוכרים כמו person-people או octopus-octupi, ואתם גם יכולים ללמד אותו כללים חדשים, למשל: selfie-selfieez.

    ל controllers יש כללים נוספים:

    אם יש לנו Controller ששם המחלקה שלו הוא InvoiceController, אז עליו להיות בקובץ בשם invoice_controller.rb שנמצא בתיקייה app/controllers. עד כאן – זה כמו המודלים.

    בנוסף, ריילס תצפה שיהיה קובץ נוסף בשם invoice_helper.rb בתיקייה app/helpers שיכיל מחלקה בשם InvoiceHelper.

    ריילס גם תצפה שה view templates של ה controller הזה יהיו בתיקיה בשם apps/views/invoice. כהתנהגות ברירת מחדל, ריילס תשתמש ב output של ה templates הללו ותכיל אותם בתוך layout template (לא דיברנו עדיין ממש על הפרטים של ה view…) בשם invoice.html.erb (או invoice.xml.erb – אם מישהו משתמש עדיין ב XML/XHTML) שנמצא בתיקייה app/views/layout.

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

    אם יש לנו כמה controllers הקשורים זה לזה (למשל Admin screens), אנו יכולים להגדיר אותם בהיררכיה בתוך התיקיה app/controllers. אם הדפדפן ביקש URL בשל admin/user אז ריילס תחפש את ה controller בקובץ בשם user_controller.rb בתיקיה app/controllers/admin (כלומר: תת-התיקיה admin מקבצת את כל ה controllers בקבוצה).

    בכדי למנוע התנגשות בין שני controllers בעלי אותו השם (נאמר user_conroller.rb בתיקיה app/controller/report), שם המחלקה יהיה namespaced ע\”י מודול בשם של תת-התיקיה. במקרה שלנו: Admin::UserController.

    רוצים לסדר את מבנה התיקיות בצורה שנוחה לכם? אולי ברור לכם עכשיו כמה קונפיגורציות תאלצו לנהל!



    קישורים נוספים:

    The Rails Doctrine – הרעיונות התכנוניים / פילוסופיים מאחורי ריילס.

    מה ההבדל בין RDoc ל Markdown

    ?Is TDD Dead

    A Conversation with Badri Janakiraman about Hexagonal Rails

    פוסט על הארכיטקטורה של ריילס



    [א] זו לא אמת חד-משמעית, אך כך מקובל להאמין.

    [ב] Gems, הן ספריות קוד של רובי אותן ניתן להתקין בקלות (<gem install <gem name $) מ repository משותף-לכל. דומה ל npm packages של node.s, ל eggs של פייטון, או Pears (אגסים) של PHP.

    [ג] פירוש המילה: פיגומים. הוספת templates קטנים (controller חדש, מודל חדש) תוך כדי עבודה ע\”י הפעלה ב command line של \”… rails generate\”, או בקיצור: \”… rails g\”. זו המקבילה הרזה ל Create New Wizard ב Eclipse, למשל. פקודת rails generate דומה מאוד ל Yo – למי שמכיר, ובעצם הייתה עבורה מקור ההשראה.
    [ד] יש כמובן את Scala, Elixier, Rust, ועוד. הצגתי את הבחירות הנפוצות של אנשי רובי.

    [ה] טכנולוגיות אלו הן לא \”של ריילס\”, אבל ריילס עושה בהן שימוש מקיף, ואלי גם השפיעה עליהן במידה:

    • SASS – שפת מטא ל CSS
    • Haml – תחביר מקוצר ל HTML 
    • CoffeeScript – שפת מטא לכתיבת ג\’אווהסקריפט, בתחביר שדומה לשפת רובי.

    כתבתי על שפות המטא של ג\’אווהסקיפט ו CCS בעבר.

    [ו] Data Mapper, כמו Hibernate – מבודד את ה Domain מבסיס הנתונים, בכדי להפוך את ה Domain Logic לבלתי תלוי, קל לבדיקה (Unit Testing), ולאפשר שינויים בבסיס הנתונים במנותק מה Domain וליהפך. אמנם במקרים רבים הצוות משתמש ב Data Mapper כי זה \”Best Practice\” למרות שהכוונה הברורה היא שבסיס הנתונים יהיו מיפוי מדויק של המודל – או ליהפך (הכוונה: תמיד אובייקט = טבלה, בלי שום רמת הפשטה). כמובן שרמת ההפשטה האפשרית בפועל מעל בסיס הנתונים היא מסוימת – ולא \”אינסופית\”. כדאי להזכיר ש Data Mappers נולדו בסביבת ה Enterprise, בה לעתים רבות לאפליקציה לא הייתה בלעדיות על ה Database schema.

    סדרה: רובי (Ruby) למפתחי ג\’אווה ותיקים

    לאחרונה אני עובר ל Stack טכנולוגי של: רובי, ריילס, ו AWS (יש גם עניין של מובייל, אבל שם אני יחסית מעודכן).
    כחלק המדרך שאני עובר אני כותב בבלוג על הנושאים הללו. באופן דומה למעבר שעשיתי במעבר לג\’אווהסקריפט – אני מנסה לכתוב את הדברים מנקודת המבט שאני הייתי זקוק לה: מתכנת וותיק, שמכיר (נאמר) ג\’אווה ו Object Oriented היטב – אך \”נוחת\” בעולם לא מוכר של רובי.

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

    אני מרגיש פספוס קטן שרק עכשיו, אחרי כ 12 שנים בתעשיה, אני נחשף לראשונה באמת להבנה מהם רובי וריילס. פתאום אני מזהה נקודות השפעה שונות שלהן שראיתי שהכרתי כבר שנים – אך לא זיהיתי את מקור ההשפעה. נכון: ריילס נמצאת בדעיכה, מתוך כך שהמשקל של ה UI עובר לצד הלקוח, וכדי לכתוב REST APIs ברובי מספיק כנראה לקחת את Sinatra ו Active Records (כך אומרים). לא צריך את כל \”המשקל\” של ריילס. ההחלשות של ריילס משפיעה גם על רובי, ופתאום יותר מגניב מ\”לעשות רובי\” זה \”לעשות node.js\” [א].

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

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

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


    RVM – כלי לניהול סביבות (וגרסאות רובי)
    כאשר אתם מתחילים לכתוב רובי קצת יותר ברצינות – זהו כלי שכדאי להכיר.

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

    ריילס, רובי on ריילס
    שפת רובי איננה שפת רובי ללא ה framework הכ\”כ-מפורסם שלה לפיתוח אפליקציות ווב: Ruby on Rails. בפוסט זה נספק סקירת high level על ריילס.

    —–

    [א] אפילו מאטצ\’ (שם החיבה של Matsumoto Yukihiro – ממציא רובי) עובד (במקביל) על שפה חדשה – Streem.

    —–

    לינקים מעניינים

    Ruby Weekly – דרך טובה להתעדכן על החדש ברובי / ריילס.

    תכנות מונחה-עצמים בשפת רובי

    שפת רובי היא שפת Object-Oriented (בקיצור: OO).

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

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

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

    • היכן האובייקטים, המחלקות, הממשקים, וכלי ה OO שאנו מכירים מג\’אווה – נמצאים ברובי?
    • מהם מודולים, וכיצד משתמשים בהם?
    • מה ההבדל בין מודל ה OO של רובי לבין זה של ג\’אווה? (רמז: יש הבדלים רבים)
    • כיצד יודעים לחזות, ללא הפתעות – איזו מימוש של מתודה יופעל כאשר האובייקט שלנו הוא חלק משרשרת הורשה?
    • להכין אתכם בפני כמה מהפתעות אפשריות בשימוש ב super – כך שלא יהיו לכם הפתעות.

    נפתח בהתמצאות בסיסית:

    המבנים הבסיסיים של ה OO ברובי הם:

    • מחלקה (class)
    • אובייקט (object)
    • מודול (module) – אפשר לחשוב עליו כ\”מודול הרחבה\” שניתן \”לחבר\” אותו למחלקה בכדי להרחיב את היכולות שלה.
    • מחלקה יחידנית (singleton class) – לפעמים נקראת גם metaclass, משמשת להשגת התנהגויות מסויימות שנדבר עליהן בהמשך.
    • מבנה (struct) – סוג של \”תחליף זול\” ליצירת מחלקות פשוטות בקלות.

    ברובי אין interfaces, ואין abstract classes.

    בואו נשים לב לעוד כמה תכונות והבדלים בין המבנים שיכולים לעזור בהתמצאות:

    • ניתן לייצר מופעים (instances) ממחלקות וממבנים בלבד.
    • מודול ומחלקה יחידנית הם דומים מאוד למחלקה – אבל יש עליהם כמה מגבלות (הברורה: אין מתודה new ליצירת מופעים).
    • כל המבנים הנ\”ל הם בעצם גם אובייקטים לכל דבר: הם instances של מחלקה כלשהי, יש להם self, וכו\’ – לא שונה כ\”כ מג\’אווה, למען האמת.

    הערה: מכיוון ש\”כל המבנים הם אובייקטים\”, אשתמש במונח \”מופע\” (instance) בכדי לתאר אובייקט שהוא מופע של מחלקה (class), כלומר: אובייקט \”פשוט\”.

    נראות במחלקה

    מה העיקרון החשוב ביותר ב OO? – הכמסה!
    בואו נראה איך הכמסה עובדת ברובי.

    כברירת מחדל ברובי:

    • כל משתני המחלקה (@@) או משתני המופע (@) – הם private
    • כל המתודות – הן public
    • מקרה מיוחד היא המתודה initialize (הבנאי) שהוא private: המתודה new של המחלקה BasicClass (ממנה כל מחלקות הרובי יורשות) היא public והיא קוראת לבנאי של המחלקה שלנו.
    יש שוני בין ההגדרות של private ו protected בין ג\’אווה (או ++C) לבין רובי.

    נתחיל בהגדרת ה private ברובי (במילה: מחמיר מעט יותר מג\’אווה):

    • ברובי private הוא פרטי של המופע – כלומר מופעים אחרים של המחלקה לא יכולים לגשת אליו (כמו שהם יכולים בג\’אווה – אם אתם לא מכירים. בג\’אווה כל האובייקטים מאותו המחלקה הם חברים, friends)
    • ההגדרה ברובי ל private: לא ניתן לקרוא למשתנה / מתודה – אם צריך לציין מי המקבל של ההודעה.
    • להבהיר: הורים וילדים של המחלקה – יוכלו לגשת למתודה, אולם באופן עקיף – ע\”י הקריאה למתודה כאילו היא שלהם או ע\”י super. בן לא יוכל לקרוא למתודה של מופע אחר מאותה המחלקה, למשל.

    למשל:

    class MyClass

    @other_object = MyClass.new

    def foo
    say_it
    # specified no-one - okay!
    self.say_it # specified \'self\' - no go.
    @other_object.say_it # specified \'other_object\' - no go.
    end

    private

    def say_it
    puts
    \'yay!\'
    end

    end

    x = MyClass.new
    x.say_it
    # NoMethodError
    x.foo # yay!, then NoMethodErrors
    המילה השמורה private בגוף המחלקה מגדירה שכל מתודה שהוגדרה מקטע זה ואילך תהיה private (דומה ל ++C). ניתן בהמשך להגדיר מקטע protected ואז public או private בחזרה, וכו\’

    למתכנת ג\’אווה מנוסה, הקוד הבא אמור להעלות שאלה:
    \”ניתן לראות בקלות ש x.say_it היא קריאה למתודה פרטית. למה ה IDE (נניח RubyMine) צועק על בעיות אחרות – אך לא על זו ?!\”
    התשובה היא בגלל שניתן לשנות את נראות המתודות בזמן ריצה – ולכן לא ניתן להחליט בבבירור שזוהי שגיאה.

    ניתן לקרוא ל \”MyClass.protected :foo\” בזמן ריצה בכדי לשנות את המתודה (מתוך המחלקה) ל protected.
    לא נהוג באמת לשנות את הנראות בזמן ריצה, אבל כן נהוג להשתמש במתודה הנ\”ל בכדי להגדיר שמית מתודות ואת הנראות שלהן – במקום לנהל את הנראות במקטעים.

    מה המשמעות אם כן של protected?

    protected ברובי היא דומה יותר ל private בג\’אווה. הההגדרה: המתודה היא נגישה כל עוד self מתייחס למופע של אותה המחלקה.
    הקריאה יכולה להתבצע מהאובייקט עצמו, מופע אחר של אותה המחלקה, או מופע של מחלקה שיורשת / נורשת מאותה המחלקה. הייעוד של protected ברובי הוא לשתף מידע בין מופעים הקשורים זה-לזה.

    מחלקות ומופעים – כיצד הם מיוצגים?

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

    רובי מנהלת מבני נתונים למופעים ולמחלקות, כאשר:

    • מופעים מכילים state – משתנים
    • מחלקות מכילות מתודות וקבועים
    • מכיוון שמחלקה היא גם מופע – אזי היא גם מכילה משתנים, אם כי ב\”רמה\” אחרת.
    מתודות המופע (instance methods)
    מתודות המשותפות לכל המחלקות המופעים של המחלקה MyClass. אלו בעצם המתודות הרגילות של המחלקה, כמו המתודה foo בדוגמת הקוד למעלה.
    מתודות המחלקה (class methods)
    הן מתודות שזמינות להפעלה מתוך reference למחלקה עצמה. הן דומות למתודות סטטיות (static method) בג\’אווה.
    שימו לב שבתיעוד מקובל לסמן מתודות מופע ב# ומתודות מחלקה ב::
    class MyClass
    def self.my_class_method
    puts
    \'first!\'
    end

    def MyClass.second_class_method
    puts
    \'second!\'
    end

    class << self
    def MyClass.third_class_method
    puts
    \'third!\'
    end
    end
    end

    x = MyClass.new

    MyClass.my_class_method # first!
    MyClass.second_class_method # second!
    MyClass.third_class_method # third!

    x.my_class_method # error !@#$!
    x.class.my_class_method # first!

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

    מתוך המופע x אינני יכול לקרוא למתודות המחלקה של MyClass (למרות שאני מופע של MyClass), אלא רק בעזרת התייחסות (reference) למחלקה עצמה (למשל: x.class).

    משתני מחלקה וקבועים
    את משתני המחלקה אנו מכירים מהפוסט הראשון – הם מתחילים ב @@ והם דומים ל static fields בג\’אווה. ניתן לגשת אליהם מתוך מתודות של המופע.

    נזכיר שגם משתני מופע (@) וגם משתני מחלקה (@@) הם private members. ניתן לגשת אליהם מתוך מתודות של המחלקה / מופע – אך לא מתוך קריאה ל x.@some_variable (מדוע? מכיוון שציינו את מקבל ההודעה, כמובן!)

    class MyClass
    @@y = 4
    Z = @@y

    def foo
    puts
    @@y
    @@y += 1
    end
    end

    MyClass.new.foo # first instance -> 4
    MyClass.new.foo # second instance -> 5
    # MyClass.new.@@y = syntax error

    x = MyClass.new
    puts
    MyClass::Z # 4
    puts x::Z # error!

    אופיים של קבועים (Z – במקרה שלנו) נקבע ע\”י כך ששמם שמתחיל באות גדולה. שימו לב שמחלקות ומודולים – הם (אובייקטים) קבועים בשפת רובי.

    מתודות ישירות / יחידניות (singleton method)
    בשונה מג\’אווה ניתן להגדיר מתודות על מופע בודד – ולא על המחלקה.

    class MyClass
    def foo
    puts
    \'yay!\'
    end
    end

    y = MyClass.new

    def y.goo
    puts
    \'woo\'
    end

    x = MyClass.new

    y.goo
    # woo
    puts y.singleton_class.method_defined? :goo # true

    puts x.singleton_class.method_defined? :goo # false
    x.goo # NoMethodError

    כלומר: המתודה goo קיימת רק על המופע y – ולא על שאר המופעים במחלקה.
    אם אתם זוכרים את הכללים שלמעלה – מופע (אובייקט) מכיל רק state ולא מתודות. מתודות יושבות על מחלקות או מודולים. כיצד זה מסתדר עם מה הקוד שזה עתה ראינו?

    ובכן… ברגע שאנו מגדירים מתודה על \”מופע\”, בעצם מאחורי הקלעים נוצרת מחלקה יחידנית (singleton class), חסרת שם, שתארח את המתודה הזו. המופע יחזיק התייחסות (reference) למחלקה היחידנית – יכולה להיות לו אחת כזו, לכל היותר.

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

    המחלקה היחידנית של MyClass היא לא חלק מהיררכיית ההורשה של המופע x (היררכית הורשה היא עניין אישי ברובי) – ולכן אם מתודת המחלקה נקראת ישירות מתוך המופע (x.some_class_method) – המופע לא ימצא אותה.

    הנה דרך נוספת להגדיר singleton method על המחלקה x

    x = MyClass.new
    y
    = MyClass.new

    class << x
    def poo
    puts
    \'ok\'
    end
    end

    puts x.poo # ok
    puts y.poo # NoMethodError

    הביטוי class << x גורם לנו להכנס ל scope של מהחלקה היחידנית. פשוט.
    בכדי שלא יהיה משמעם, קיים הבדל בין שתי דרכי ההגדרה של מתודה על מחלקה יחידנית – הבדל שקשור לנראות של קבועים על המחלקה היחידנית. בצורת ההגדרה הקודמת  הפונקציה לא \”תראה\” את הקבועים שעל המחלקה היחידנית, ואילו בצורת ההגדרה הנוכחית – הם יהיו זמינים מחלקה. למה? מדוע? – לא חפרתי מספיק בכדי להבין…

    הערה אחרונה בנושא: הגדרת נראות private למתודת מחלקה (class method) נעשית בצורה מעט שונה: לא ע\”י שימוש ב private שאנו משתמשים עבור מתודות מופע, אלא ע\”י שימוש ב private_class_method. שימוש מקובל ב directive הזה הוא להחביא את המתודה new:: בכדי להגדיר דפוס עיצוב של Singleton (של GOF).
    ה directives של הנראות (private, public, protected) הן בעצם פונקציות של המחלקה Module המשפיעות על המטא-מודל של האובייקטים ברובי. מכיוון שמתודות מחלקה לא נמצאות באמת על המחלקה (אלא על המחלקה היחידנית של המחלקה) – זקוקים למנגנון מעט שונה בכדי לשנות את הנראות שלהם מבלי לסבך. מבלי לסבך את המפשן של רובי, התכוונתי.

    זוכרים שהזכרנו בתחילת הקטע את המטאפורה של המחלקה כ\”שק של תכונות\”?
    בואו נראה התנהגות זו בפעולה:

    class MyClass
    def foo
    puts
    \'aleph\'
    end

    def foo
    puts
    \'beth\'
    end
    end

    x = MyClass.new
    x.foo
    # beth

    הגדרנו מתודה בשם foo, ואז הגדרנו אותה שוב.
    התוצאה? דריסה של רישום המתודה הראשון ברישום השנה – וזה מה שנשאר.

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

    הנה דוגמה נוספת:

    class MyClass
    def foo
    puts
    \'aleph\'
    end
    end

    x = MyClass.new

    class MyClass
    def goo
    puts
    \'beth\'
    end
    end

    y = MyClass.new
    x.foo
    # aleph
    x.goo # beth
    y.foo # aleph
    y.goo # beth

    הגדרנו את המחלקה MyClass ואז הגדרנו אותה שוב?
    הפעולה הזו נקראת ברובי \”re-opening a class\”. הגדרה נוספת של מחלקה לא \”דורסת\” את רישום המחלקה הקיים, אלא גורמת לרישום חדש או נוסף של כל ה members לאותו \”שק\” של מתודות שנקרא MyClass.

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

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

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

    האם יש יכולת דומה בשפות אחרות?
    בג\’אווהסקריפט ניתן לעשות אותו הדבר – שינוי של ה prototype של האובייקט (= בערך מחלקה ברובי) מכל מקום במערכת, וזה נחשבת פרקטיקה לא טובה.
    ב #C יש Partial Classes שזה סוג של פיצול מחלקה לכמה קבצים שונים – אבל השימוש הנפוץ הוא שחלק אחד נוצר מ code generation והשני – מתוחזק בצורה ידנית, והם יושבים במבנה הפרוייקט זה לצד זה.

    בקיצור:ע\”פ כל קריטריטיון שאני מכיר, כדאי להמנע מלהשתמש ביכולת ה\”פתיחה מחדש של המחלקה\” – ולשמור על יכולת ההתמצאות (orientation) וההבנה הקלה של המערכת. שימוש אחד שיכול להיות סביר להרחבת מופעים והוספת singleton_methods היא unit-testing ו mocking. עדיף קוד שלא דורש זאת – אך יש כמה מצבים שבהם אני מעריך שהייתי שמח להשתמש ביכולות הללו בבדיקות.

    מודולים

    מודול ברובי הוא מבנה שמקבץ מתודות, שבניגוד למחלקה – אי אפשר לייצר ממנו מופע (instance).
    איך משתמשים במתודות הללו? \”מחברים\” את המודול למחלקה (או כמה מחלקות) – והן יקבלו את הפונקציונליות הנוספת. מודולים הם בעצם, מה שנקרא בשפות אחרות mixin.

    module NicePrinter
    def putz(msg)
    puts
    \'[\' + msg + \']\'
    end
    end

    class
    MyClass
    include NicePrinter

    def fooz
    putz
    \'yay!\'
    end
    end

    MyClass.new.fooz # [yay!]

    ניתן לשלב מודול בכמה מחלקות, ולבצע include לכמה מודולים באותה המחלקה (קירוב של \”הורשה מרובה\”).

    דרך נוספת לשלב מודול במחלקה הוא בעזרת extends

    module NicePrinter
    A = 1
    def putz(msg)
    puts
    \'[\' + msg + \']\'
    end

    module NiceSpacer
    B = 2
    def add_spaces(str)
    str.scan(
    /./).join(\' \')
    end
    end
    end

    class
    MyClass
    include NicePrinter
    extend NicePrinter::NiceSpacer

    def fooz
    putz
    MyClass.add_spaces(\'yay!\')
    end
    end

    x = MyClass.new

    x.fooz
    # [y a y !]
    puts MyClass::A # 1
    puts MyClass.singleton_class::B # 2
    puts MyClass::NiceSpacer::B # 2

    בדוגמה הזו עשינו include כמו בדוגמה הקודמת, וגם עשינו extend ל מודול המקונן NiceSpacer (ניתן לעשות extends לכל מודול – פשוט רציתי להראות קינון של מודולים \”על הדרך\”).

    למה השתמשנו ב\” ::\” ולא פשוט בנקודה? כי :: הוא ה resolution directive – דרכו מגיעים לקבועי-מחלקה. מודול (מכיוון ששמו מתחיל / חייב להתחיל באות גדולה) הוא קבוע,  – ועל כן זו הדרך הנכונה לגשת אליו. שימוש בנקודה היה זורק Exception.

    extend, בניגוד ל include, מוסיף את המתודות שעל המודול להיות class methods. להזכיר: include מוסיף את המתודות שעל המודול להיות instance methods. כנ\”ל לגבי קבועים.

    שימו לב שהקבוע B נוסף לנו פעמיים:

    • בפעם הראשונה (כרונולוגית) – כחלק מה include: כאשר עשינו include נוספף המודול NicePrinter וכל המודולים המקוננים שלו (במקרה שלנו: NiceSpacer).
    • בפעם השניה פעם על המחלקה היחידנית של MyClass – כאשר השתמשנו ב extends

     ברגע שמוסיפים מודול מקונן – אז גם המודול הפנימי נוסף תחת ה namespace המקונן שלו. בדוגמה למעלה בעצם הוספנו את NiceSpacer פעמיים: פעם כמודול מקונן על המחלקה MyClass, ופעם ישירות על המחלקה היחידנית של MyClass. אני מקווה שקריאת ההסבר פעמיים תספיק בכדי לקלוט את העניין… אם לא – פשוט פתחו irb ונסו קצת בעצמכם.

    האם יש למודולים עוד תכונות ויכולות? – כן.

    ניתן להגדיר צורות שונות של תלויות בין מודולים כך שחיבור של אחד למחלקה (ע\”י include או exclude) בעצם יחבר גם מודולים אחרים, או סתם \”יפתח\” (re-open) את המחלקה שמוסיפה את המודול ויבצע בה שינויים.

    יכולות אלו שימושיות מאוד לבניית DSL – אבל כדאי מאוד להיזהר בהן בכתיבת \”תוכנה בסיסית\”. לכו תבינו שמתודה שלכם מתנהגת אחרת כי מודול שנוסף, גרר מודול אחר – שעושה שינוי ב members של המחלקה…
    התחכמות (cleverness) ב Metraprogramming של רובי היא סגולה ללילות לבנים ללא שינה. ראו הוזהרתם!

    מודולים משמשים גם כ namespace (כמו ב ++C או #C), ניתן לאגד בתוכם מחלקות, פונקציות, וקבועים – תחת שם שלא יתנגש עם מחלקות, פונקציות, וקבועים אחרים:

    module MyModule
    class MyOtherClass
    def goo
    puts
    \'wow!\'
    end
    end
    end

    class
    MyClass
    include MyModule
    end

    x = MyModule::MyOtherClass.new
    x.goo
    # wow!

    y = MyClass::MyOtherClass.new
    y.goo
    # wow!

    האם ניתן לעשות include ו/או extend גם למודולים כאלו שמכילים מחלקות? – בוודאי. חשבו על Module ומחלקה כ hash (\”שק תכונות\”) שניתן להרכיב אותם (עם כמה מגבלות) אחד על השני.

    בדוגמת הקוד ניגשתי פעם ל MyOtherClass דרך המודול כ Namespace, ופעם אחרת כקבוע על המחלקה MyClass. שתי הדרכים אפשריות ותקינות.

    מתודות ו\”שליחת הודעות\”

    בשונה משפות כמו ג\’אווה / ++C בהם מתייחסים לx.foo כ \”method invocation\”, ברובי (כמו ב Smalltalk או Objective-C) מתייחסים להפעלת מתודות כשליחת הודעות.

    למשל:

    class MyClass
    def foo
    puts
    \'yay!\'
    end
    end

    x = MyClass.new

    x.foo
    # \'yay!\'
    x.send :foo # \'yay!\'

    קראנו ל foo ב 2 אופנים:

    • x.foo היא רמת ההפשטה הגבוהה, \”כאילו\” foo היא תכונה של המופע x.
    • x.send :foo היא האופן בו הדברים קורים בפועל ברובי – שליחת הודעה בשם foo ל x.

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

    הנה כמה הבדלים עקרוניים:

    • ניתן לשלוח כל הודעה (בעזרת המתודה send) שראינו למעלה. זהו בעצם syntactic sugar למה שמתרחש באמת.
    • ניתן להענות לכל הודעה, גם ל\”הפעלת\” מתודה שלא הוגדרה מראש במחלקה – ע\”י מימוש המתודה method_missing במחלקה.
    • בעזרת method_missing ניתן לעכב הודעות, להתעלם מהודעות, וכו\’.

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

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

      הכלי של method_missing מאפשר כח רב ברובי – להענות למתודות שלא הוגדרו מראש על ידי המחלקה.
      מימוש ברירת המחדל של method_missing הוא לזרוק exception, והוא נמצא בתוך module בשם kernel ש\”מחובר\” למחלקה Object.

      האם שתי דרכי הפעולה זהות?
      לא. אם תקחו את הדוגמה למעלה ותהפכו את foo למתודה private – תראו שדרך ההפעלה הראשונה (בעזרת נקודה) – נכשלת, אבל הדרך השניה (send) מצליחה. מדוע?

      אכיפת ה visibility ברובי נעשית כאשר משתמשים במנגנון הנקודה, ו send פשוט עוקפת את המנגנון הזה. send היא בעצם מתודה public של המחלקה Object (הבת הישירה של BasicObject) – שתפעיל את send_internal של המפרשן של רובי.
      כלומר: ע\”י send ניתן לקרוא, מכל מקום בקוד, למתודות private של מחלקות אחרות. מה עוצר מבעדנו לכתוב קוד שקורא ל private members של מחלקות אחרות ללא מגבלה? – משמעת עצמית בלבד.

      היררכיות ההורשה ברובי

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

      • ברובי יש מנגנון של הורשה (inheritance) בין מחלקות – וזו הורשה יחידה.
      • ברובי, כפי שראינו, יש מנגנון של ה Modules שהוא מנגנון של mixins.
      • ראינו שיש גם מחלקות יחידניות (singleton classes) שהן אלמנט טכני, \”מאחורי הקלעים\”, אבל הידיעה אודותיהן עוזר להבין כמה מההתנהגויות של רובי – שאחרת היה קשה להבין.

      הנה דוגמה פשוטה של הורשה:

      class Person
      def say_my_name
      puts
      \'john smith\'
      end
      end

      class
      Employee < Person

      end

      Employee.new.say_my_name # john smith

      אין פה שום דבר מפתיע.

      בואו נסבך מעט. האם אתם יכולים להסביר את התוצאה הבאה?

      module SongsWeLike
      def say_my_name
      puts
      \"destiny\'s child\"
      end
      end

      class
      Person
      include SongsWeLike
      end

      class
      Employee < Person

      end

      Employee.new.say_my_name # destiny\'s child

      puts Employee # Employee
      puts Employee.superclass # Person
      puts Employee.superclass.superclass # Object

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

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

      בדוגמה למעלה, כאשר Person חיבר את המודול SongsWeLike, המפרשן יצר מחלקה יחידנית ורשם אותה כ superclass של המחלקה Person.

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

      הנה האופן שבו נראית ההיררכיה באמת:

      אם נפלט לכם עכשיו במקרה \”?WTF\” – זה בסדר. זו באמת היררכיה גדולה, הכוללת כמה אלמנטים טכניים. נסביר את הכללים:

      • במידה ויש למחלקה שלנו מחלקה יחידנית משלה (נניח שהגדרנו מתודה ישירות על המופע) – היא תכנס להיררכיה מעל המופע.
      • כפי שאמרנו כבר: כל מודול אותו מחברים למחלקה – מוסיף מחלקה יחידנית מעל המחלקה שהוסיפה אותו. המחלקה היחידנית מחוברת למודול וחושפת את המתודות / קבועים שלו.
        • משמעות ראשונה של המבנה הזה הוא שמודול לעולם לא יוכל \”להסתיר\” מתודות של המחלקה עם אותו השם: כיוון החיפוש אחר מתודה הוא מלמטה למעלה – תמיד קודם ניתקל במחלקה ורק אז במודולים שהיא הוסיפה.
        • אם מוסיפים למחלקה כמה מודולים – אזי המחלקות היחידניות של המודולים יתווספו להיררכיה בסדר הפוך לסדר החיבור שלהם למחלקה, קרי LIFO. 
      • המבנה הזה אולי נראה גדול ומורכב – אבל הוא מפשט את ה runtime של רובי: בעת קריאה למתודה, המפרשן פשוט צריך לטייל על ההיררכיה מלמטה למעלה – בלי \”לחשוב הרבה\”. פעולה של חיפוש אחר מתודה מתרחשת תכופות בזמן ריצה – ולכן חשוב מאוד שהיא תתבצע במהירות.

      super

      מה קורה כאשר אנו רוצים להרחיב מחלקת-אב, אך עדיין להשתמש בחלק מהפונקציונליות שלה?
      בדומה לג\’אווה (שיש את this), יש ברובי מילה שמורה בשם super.

      class MySuperClass
      def foo(num)
      puts
      \'super \' + num.to_s
      end
      end

      class
      MyClass < MySuperClass
      def foo(n)
      puts
      \'duper:\'
      super
      super n + 1
      end
      end

      MyClass.new.foo 7 # duper:, super 7, super 8

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

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

      class MySuperClass
      def foo(num)
      puts
      \'super \' + num.to_s
      end
      end

      class
      MyClass < MySuperClass
      def foo(n)
      n
      = 0
      super
      super n + 1
      end
      end

      MyClass.new.foo 7 # super 0, super 1

      אני מניח שהשיקול התכנוני מאחורי התנהגות זו הוא צמצום הגודל של ה local table (המקבילה ברובי ל Activation Frame של ++C) – תחת ההנחה שמדובר במקרה קצה שלא יבלבל הרבה מפתחי רובי.

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

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

      האם סיימנו עם ההפתעות? – חס וחלילה!

      ניתן לחשוב בתמימות ש super תמיד קורא למתודה של ה superclass עם אותו השם. זה הגיוני, אבל אם אתם זוכרים ההיררכיה יכולה להכיל מחלקות יחידניות – מה שעלול להוביל להתנהגות לא צפוייה ו/או לא רצויה.
      במקום זאת, בקריאה ל super רובי תפעיל את אלגוריתם החיפוש אחר מתודה עם השם של המתודה ממנה היא הופעלה – החל מה superclass של המחלקה הנוכחית ומעלה. כלומר: תעלה לאורך שרשרת ההורשה עד שהיא מוצאת מתודה בשם של המתודה ממנה היא נקראה. אם היא הגיעה ל BasicObject ולא מצאה מתודה בשם הצפוי – היא או תתחיל לחפש (שוב – החל מה superclass שלנו) מתודה בשם method_missing.

      מצד אחד – זה הגיוני, מצד שני – עלול להפתיע.

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

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

      ריבוי-צורות (polymorphism) ברובי

      ל OO יש שלושה עקרונות בסיסיים:

      • הכמסה – דיברנו!
      • הורשה – דיברנו!
      • ריבוי צורות…. – נדבר מייד.
      חשוב לציין שריבוי צורות נחשב עיקרון חשוב יותר מהורשה. רעיון ההורשה הוא פשוט \”פוטוגני\” יותר – ולכן מקבל יותר \”זמן מסך\” בספרות ה OO.
      ובכן… כבר ציינו בתחילת הפוסט שבשפת רובי אין מבנים המקבילים ל interface או abstract class בג\’אווה. כיצד אם כן ניתן להשיג ריבוי-צורות בלעדיהם??

      בעצם – האם איי פעם שפה מנעה מאיתנו יישום של רעיונות בכך שלא סיפקה לנו כלים מוכנים ליישם אותם? עיכבה – כן, אבל לא מנעה. חשבו כיצד מתכנתי ג\’אווהסקריפט שמיישמים encapsulation בשפה – למרות שהיא לא קיימת ככלי בשפה (למי שלא מכיר: ע\”י דפוס עיצוב בשם Module / Revealing Module או פשוט ע\”י קונבנציה של קו תחתון בתחילת מתודות שהן \”פרטיות\” – ומשמעת שאין לגשת אליהן מבחוץ).

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

      הנה ה\”דפוס\” המקובל ליישום ריבוי-צורות ברובי:

      class AbstractHerald
      def announce
      raise
      NotImplementedError, \'You must implement the announce() method\'
      end
      end

      class
      OptimisticHerald < AbstractHerald
      def announce
      puts
      \'life is good!\'
      end
      end

      class
      PessimisticHerald < AbstractHerald
      def announce
      puts
      \'... life sucks!! :(\'
      end
      end

      x = OptimisticHerald.new
      puts x.is_a?
      AbstractHerald # true
      puts x.is_a? Hash # false

      האכיפה על כך שנממש את המתודה announce – נמצאת ב runtime (מוזר בג\’אווה, טיפוסי ברובי).
      בעזרת ?is_a – אנו יכולים לבדוק אם מופע שביידנו יורש ממחלקה מסויימת.

      חשוב להזכיר שנטייה טבעית כשמגיעים לשפה חדשה היא לחפש את המבנים המוכרים. חשוב להיזהר ולא להגזים, לא \”לכתוב ג\’אווה ב syntax של רובי\”. שפת רובי היא דינאמית מאוד מטבעה – וחשוב להבין ולהתרגל לסגנון זה.

      מה עם ריבוי ממשקים?
      נדמיין לרגע מחלקה בג\’אווה בשם User שניתן לגשת אליה דרך הממשק UserInfo לפעולו ת של קריאת נתונים, ודרך הממשק UserMaintainance – לפעולות עדכון על פרטי המשתמש (היא מממשת את שני הממשקים). המחלקות השונות במערכת מוגבלות לעשות על User רק את מה שהממשק שבידן מאפשר להן – הגבלה שנאכפת ע\”י השפה.

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

      כיצד עושים זאת? המתודה ?respond_to בודקת האם מחלקה (וההיררכיה שלה, המודולים שמחוברים אליה, וכו\’) יודעים לטפל במתודה (ע\”פ השם שלה – לא ע\”פ מספר הפרמטרים!).
      אם השתמשתם ב method_missing יהיה עליכם לדרוס את המתודה ?respond_to_missing ולהצהיר על אלו מתודות אתם מגיבים – בכדי ש ?respond_to תמשיך לספק תשובה נכונה (מזכיר את equals ו hash בג\’אווה).

      puts x.respond_to? :announce # true
      puts x.respond_to? :quit_job # false

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

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

      בפועל, סביר שתשתמשו ב ?respond_to בנקודות בהן:

      • הקוד שלכם נכשל בהן בעבר, והחלטתם \”לחזק\” (hardening) אותן.
        כלומר – הסיבה לתקלה הייתה מורכבת, ולא טעות הקלדה \”טפשית\”.
      • בנקודות שהסיכון לתקלה שכזו הוא בהחלט סביר, בהנחה שאתם מתכנתים זהירים / קפדנים.

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

      מה עושים כדי למנוע בכל-זאת תקלות? הרבה מאוד unit tests.
      למפתחי ג\’אווה זה עשוי להשמע מחריד: \”שפה שאנו לא סומכים עליה ב 100%?!\”, אולם מניסיוני בג\’אווהסקריפט – המצב הזה עשוי לשרת דווקא לטובה.
      הקומפיילר של ג\’אווה מספק ביטחון מסוים (בעצם: תחושת ביטחון מסוימת) – שמהווה מכשול למפתחי ג\’אווה להשקיע הרבה בבדיקות יחידה / אוטומציה. דווקא בשפות דינאמיות שהמפתחים מודעים יותר לכך שהם \”חשופים לתקלות\” – המוטיבציה להשקעה בבדיקות גבוהה יותר, ולעתים גם קל יותר לכתוב ולתחזק בשפות הללו את הבדיקות – מה שיכול להוביל בסה\”כ למערכת אמינה יותר [ב].

      Structs

      לכאורה Struct הוא מבנה פחות משמעותי בשפה. הוא ממומש בעזרת כלים קיימים, וייתכן שהמפרשן של רובי כמעט או בכלל לא מכיר אותו. בכל זאת – הוא שימושי.

      אתם אולי מכירים structs מ ++C או #C – שם \”הקטע\” שלהם היא רציפות בזכרון / יכולת שמירה על ה stack של ה thread. ברובי אין כאלו דברים – והשיקולים הם שיקולים של מהירות פיתוח ותחזוקה. Struct נמצא על הרצף בין Hash (מבנה הנתונים מטיפוס Dictionary – ללא מתודות או קבועים, או הורשה) למחלקות. הוא קצת יותר מ Hash וקצת פחות ממחלקה.

      Struct הוא בעצם generator של מחלקה פשוטה, שכוללת כמה משתני מופע – ו getters / setter למשתנים הללו. היא חוסכת, מצד אחר, הקלדה של מחלקות משעממות, ומצד שני מספקת מבנה מעט יותר יציב / מוגדר-היטב מ Hash. היתרונות העקריים הם:

      • יש הגדרה ברורה לסכימה של ה struct (לאלו פרמטרים מצפים).
      • ניתן לגשת לפרמטרים הללו בצורה יותר אלגנטית: במקום [data[:currency משתמשים ב data.currency.
      כאשר משתמשים ב Hash (שזו דרך מהירה מאוד, ושימושית מאוד בהמון מקרים) לתאר מבנה נתונים – אין מקום ידוע בו מוגדרת הסכמה: איזו שדות קיימים? כיצד בדיוק הם נקראים? הסכמה, פעמים רבות, היא צירוף כל המפתחות בהם עשו שימוש איפשהו בקוד.

      class MyClass
      MyMessage = Struct.new(:source, :target, :text)

      def foo
      some_condition
      = false
      MyMessage.new(\'a\', \'b\', \'c\') unless some_condition
      end

      def goo
      MyMessage.new \'a\', \'b\' # text will be nil
      end

      end

      msg = MyClass.new.foo
      puts msg.text
      , msg.source # c, a

      קצת חבל שה IDE בו אני משתמש, RubyMine, לא מספק auto-complete ממשי ל Structs.

      אם אתם רוצים להוסיף איזו מתודה או שתיים ל struct – אפשר.
      Struct::new יכולה לקבל גם בלוק (של מתודות קבועים) ותמיד אפשר \”לפתוח את ה Struct להרחבה\”. כל עוד זה נעשה בצמוד ליצירה שלו – אני מאשר. 🙂

      הנה אופן השימוש בבלוק:

      MyMessage = Struct.new(:source, :target, :text) do
      def encryptText
      self.text = self.text.gsub(/./, \'*\')
      end
      end

      msg = MyMessage.new \'me\' , \'you\', \'a secret\'
      msg.encryptText
      puts msg.text
      # ********

      סיכום

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

      סקרנו את תכנות מונחה-העצמים ברובי ואני מקווה שהסקירה, למרות שנדחסה כולה לפוסט אחד – היא עדיין מספיק מקיפה ועמוקה.
      שמעו: רובי היא שפה מורכבת. יותר מפייטון (ברור!), יותר מג\’אווהסקריפט (דאאא), ויותר מג\’אווה. אני משווה את המורכבות שלה רק ל ++C (אני לא משווה עדיין ל Scala…). זה לפחות הרושם שלי – בתור אחד שכתב בכולן (לא ממש בסקאלה).

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

      ——

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

      http://rubymonk.com/learning/books מקור טוב ללמידת רובי לעומק

      ——

      [א] אם אתם רוצים לראות את שרשרת ההיררכיה של מחלקה – פשוט הפעילו עליה את המתודה ancestors.

      [ב] מפתחי ג\’אווה, ענו לעצמכם בכנות: איזה אחוז משגיאות הקוד המשמעותיות הקומפיילר \”תופס\”? ואיזה אחוז הוא לא?