צריכה להיות דרך ברורה אחת (ועדיף שתהיה היחידה) לעשות את זה
המשפט בכותרת הוא תרגום של המשפט הבא מתוך ה Zen של פייתון. זה המקור:
There should be one-- and preferably only one --obvious way to do it.
והוא נכון לכל השפות. נזכרתי בו במקרה כשדפדפתי בקוד הבא, דווקא בשפת רובי:
class ShoppingCart < ActiveRecord::Base
has_many :products, :class_name => 'CartProduct', :dependent => :delete_all
def <<(product)
line = CartProduct.find_or_initialize_by(:product => product, :cart => self)
# comment out to allow quantity
# line.increment(:qty) unless line.new_record?
line.save!
@finalized = false
self
rescue ActiveRecord::RecordNotUnique
retry
end
def add_with_options(product, options)
line = CartProduct.find_or_initialize_by(:product => product, :cart => self)
line.price = options[:price]
line.options = options
line.save!
@finalized = false
self
rescue ActiveRecord::RecordNotUnique
retry
end
end
ההגיון ברור: הפונקציה הראשונה היתה הדרך המקורית בה הוסיפו פריטים לעגלת הקניות, ואז מישהו רצה לאפשר הוספת פריטים יחד עם כל מיני מתגים אז במקום לשנות את הממשק (מה שישבור קוד קיים ויצריך Refactoring) הוא פשוט הוסיף עוד פונקציה.
הבעיה? עכשיו יש לנו שתי דרכים להוסיף מוצר לעגלת הקניות - וקל מאוד לטעות ולשכוח את זה למשל כשכותבים בדיקה:
test 'cart price is the sum of all product prices' do
@cart = create(:cart)
@cart.products << create(:item, price: 20)
@cart.products << create(:item, price: 50)
assert_equal(70, @cart.price)
end
הבדיקה נראית יפה אבל לא שווה כלום אם רוב הקוד במערכת משתמש בשיטה השניה כדי להוסיף מוצרים.
צריכה להיות דרך ברורה אחת (ועדיף שתהיה היחידה) להוסיף מוצרים לעגלת קניות. רק כך אנחנו מוודאים שמי שקורא את הקוד יודע לחבר בין הבדיקה לבין הדבר שהיא באמת בודקת, שכל הקוד במערכת מתוחזק ושיש לנו אחידות בממשק.
תיאורטית פיתרון קל מאוד במקרה כזה יהיה פשוט שהפונקציה הראשונה תקרא לפונקציה השניה, כלומר:
def <<(product)
add_with_options(product, {})
end
וזה באמת מתקן את כפילות הקוד במימוש מחלקת עגלת הקניות אבל עדיין לא פותר לנו את הבעיה. הממשק עדיין מסורבל וכולל שתי אפשרויות בלי הבדל ברור ביניהן. יותר חשוב להבין איפה במערכת מפעילים את add_with_options
ולבחור לפונקציה שם שיותר מתאים ל Use Case הספציפי שלה, ובאותה נשימה הייתי משנה גם את השם של פונקציית הוספת מוצר הישנה למשהו שיתאר טוב יותר את ה Use Case הספציפי שלה.
במקרה שלנו אפשר לחשוב על:
class ShoppingCart < ActiveRecord::Base
def add_product_sold_by_partner(product, partner_price, partner_options)
end
def add_product_from_website(product)
end
end
בצורה כזאת השם add_product_from_website
כבר רומז לנו שיש דרכים נוספות להוסיף מוצרים לעגלה ומעודד אותנו ללכת למחלקה לבדוק מהן.