לדלג לתוכן

תבנית Repository מוסברת

כמעט כל מערכת שמדברת עם מסד נתונים נתקלת באותה בעיה - קוד הלוגיקה העסקית מתמלא בקריאות SQL או ORM ישירות, וכשמגיע הרגע לבדוק את הלוגיקה הזאת ביחידה (unit test), אין ברירה אלא להריץ מסד נתונים אמיתי, גם לבדיקה של תנאי if פשוט. תבנית Repository פותרת בדיוק את הבעיה הזאת.

הבעיה - לוגיקה עסקית שמתערבבת עם גישה לנתונים

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

def is_eligible_for_discount(user_id):
    conn = get_db_connection()
    cursor = conn.execute(
        "SELECT total_orders, signup_date FROM users WHERE id = ?", (user_id,)
    )
    row = cursor.fetchone()
    return row["total_orders"] > 10 and row["signup_date"] < one_year_ago()

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

הפתרון - שכבת Repository

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

class UserRepository:
    def __init__(self, db_connection):
        self.db = db_connection

    def get_by_id(self, user_id) -> User:
        row = self.db.execute(
            "SELECT * FROM users WHERE id = ?", (user_id,)
        ).fetchone()
        return User.from_row(row)


def is_eligible_for_discount(user_repo: UserRepository, user_id):
    user = user_repo.get_by_id(user_id)
    return user.total_orders > 10 and user.signup_date < one_year_ago()

עכשיו is_eligible_for_discount לא יודעת כלום על SQL, טבלאות, או חיבורי מסד נתונים - היא מקבלת אובייקט User מוכן, ובודקת עליו תנאי עסקי בלבד. כדי לבדוק אותה ביחידה, מספיק להעביר UserRepository מדומה שמחזיר אובייקט User קבוע מראש, בלי מסד נתונים אמיתי בכלל.

class FakeUserRepository:
    def get_by_id(self, user_id):
        return User(total_orders=15, signup_date=two_years_ago())

def test_eligible_for_discount():
    assert is_eligible_for_discount(FakeUserRepository(), user_id=1) is True

למה זה שווה יותר מבדיקות מהירות

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

Repository מול ORM

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

מתי זה תקורה מיותרת

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

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

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

לסיכום

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