• בלוג
  • עמוד 160
  • צריכה להיות דרך ברורה אחת (ועדיף שתהיה היחידה) לעשות את זה

צריכה להיות דרך ברורה אחת (ועדיף שתהיה היחידה) לעשות את זה

23/07/2020

המשפט בכותרת הוא תרגום של המשפט הבא מתוך ה 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 כבר רומז לנו שיש דרכים נוספות להוסיף מוצרים לעגלה ומעודד אותנו ללכת למחלקה לבדוק מהן.