ניהול השכבה העסקית (Domain Layer) – עקרונות ודוגמאות מעשיות

בפוסטים הקודמים עסקנו בעקרונות של ארכיטקטורה נקייה (Clean Architecture) וביישום מעשי של Dependency Injection. עכשיו הגיע הזמן לדבר על אחד הרכיבים הכי חשובים (והרבה פעמים הכי מוזנחים) בארכיטקטורה שלנו – השכבה העסקית (Domain Layer).

תזכורת מהירה: מהי השכבה העסקית (Domain Layer)?

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

השכבה הזו חייבת להיות נקייה לחלוטין מתלות בטכנולוגיות, בתשתיות או בממשק משתמש. המשמעות היא:

  • אסור שתהיה תלויה ישירות ב-DB.
  • אסור שתכיל קוד ספציפי ל-UI.
  • אסור שתהיה תלויה ישירות בשום שירות חיצוני.

אז איך עושים את זה נכון?

Entities ו-Value Objects – מה ההבדל ואיך לתכנן אותם?

Entities (ישויות)

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

דוגמה לישות:

class User {
  constructor(public readonly id: string, public name: string, public email: string) {}

  updateEmail(newEmail: string) {
    if (!newEmail.includes('@')) {
      throw new Error('Invalid email format');
    }
    this.email = newEmail;
  }
}

הישות מוגדרת על ידי הזהות שלה (id). לא משנה אם יש שני משתמשים בעלי שם זהה – כל עוד ה־ID שונה, אלו שתי ישויות נפרדות.

Value Objects

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

דוגמה טובה היא "כתובת":

class Address {
  constructor(
    public readonly city: string,
    public readonly street: string,
    public readonly houseNumber: number,
  ) {}

  equals(other: Address): boolean {
    return (
      this.city === other.city &&
      this.street === other.street &&
      this.houseNumber === other.houseNumber
    );
  }
}

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

איך למנוע זליגת לוגיקת UI או Database לשכבה העסקית?

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

לדוגמה, זה לא תקין:

// דוגמה לא נכונה של Entity עם זליגת DB
class User {
  constructor(public id: string,
  public name: string,
  private db: Database
) {}

  async save() {
    await this.db.users.update(this.id, { name: this.name });
  }
}

כאן, הישויות כבר תלויות ישירות בשכבת ה-DB, מה שמפר לחלוטין את הכלל המרכזי של השכבה העסקית.

אז איך עושים את זה נכון?

הדרך הנכונה היא להשתמש ברפוזיטורים (Repositories) שהגדרנו בשכבת ה-Use Cases. הישויות עצמן לא אחראיות על שמירתן:

דוגמה נכונה:

// Entity נקי לחלוטין מ-DB
class User {
  constructor(public readonly id: string, public name: string) {}

  changeName(newName: string) {
    this.name = newName;
  }
}

// Repository Interface
interface IUserRepository {
  save(user: User): Promise<void>;
}

// Repository ממומש בשכבת התשתית (בחוץ!)

בצורה כזו השכבה העסקית נשארת נקייה לגמרי מלוגיקת DB.

שימוש מעשי ב-Domain Models – דוגמה עם TypeScript

הנה דוגמה מלאה לאופן שבו ה-Domain Model עובד בפועל:

// Entity
class Order {
  constructor(
    public readonly id: string,
    public readonly items: OrderItem[],
    public readonly customerId: string,
    public status: OrderStatus = OrderStatus.Created
  ) {}

  calculateTotal(): number {
    return this.items.reduce((total, item) => total + item.price * item.quantity, 0);
  }

  approveOrder() {
    if (this.status !== OrderStatus.Created) {
      throw new Error('Only new orders can be approved');
    }
    this.status = OrderStatus.Approved;
  }
}

// Value Object
class OrderItem {
  constructor(public productId: string, public price: number, public quantity: number) {}
}

// Enum פשוט לסטטוסים של הזמנה
enum OrderStatus {
  Created,
  Approved,
  Shipped,
  Delivered,
}

ה-Order הוא Entity מרכזי. יש לו פעולות עסקיות כמו חישוב הסכום ואישור הזמנה, וכל פרט טכני אחר (שמירה, טעינה) מנוהל מבחוץ, דרך Repository בשכבת Use Case/Infrastructure.

טיפים למניעת זליגה לשכבה העסקית (Best Practices)

הנה כמה המלצות מעשיות כדי לשמור על השכבה העסקית שלכם נקייה:

  • לעולם אל תעבירו לשכבה העסקית אובייקטים טכניים כמו Request או Response של Express.
  • אין לוגיקת DB בתוך Entities ו-Value Objects. השתמשו בממשקים מופשטים.
  • הקפידו שה-Value Objects יהיו בלתי-ניתנים-לשינוי (Immutable).
  • בדיקות יחידה צריכות לבדוק רק את הלוגיקה העסקית, ללא Mock של DB או HTTP. אם זה קשה לכם – משהו לא בסדר בעיצוב שלכם.
  • אם קוד ה-Entity שלכם מתחיל להסתבך עם הרבה לוגיקה – בדקו אם אפשר להעביר חלק ללוגיקת Use Case או להפריד ל-Value Objects חדשים.

בשורה התחתונה

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

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

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *