ניהול טעויות ואירועים – Events & Error Handling כחלק מ־Clean Architecture

בפוסטים הקודמים דיברנו לעומק על ארכיטקטורה נקייה, סקרנו איך מיישמים Dependency Injection, והסברנו כיצד לעצב נכון את השכבה העסקית (Domain Layer).
היום נדבר על חלק נוסף וחשוב לא פחות: ניהול טעויות ואירועים (Events & Error Handling).

למה ניהול שגיאות חשוב כל כך?

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

  • קוד מסורבל ומלא בלוקים של try-catch.
  • לוגים מבלבלים שקשה להבין מהם מה קרה.
  • זליגת לוגיקה טכנית לתוך השכבה העסקית.

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

עקרונות לניהול שגיאות נקי (Clean Error Handling)

1. הפרדה בין שגיאות עסקיות לטכניות

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

הנה דוגמה קצרה ב־TypeScript להבחנה ברורה בין סוגי שגיאות:

// Business Error
export class BusinessError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "BusinessError";
  }
}

// Technical Error
export class TechnicalError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "TechnicalError";
  }
}

2. טיפול בשגיאות בשכבת Use Cases בלבד

בשכבת ה-Use Case אנחנו זורקים שגיאות עסקיות במפורש:

class RegisterUserUseCase {
  constructor(private userRepo: IUserRepository) {}

  async execute(user: User) {
    const existing = await this.userRepo.findByEmail(user.email);
    if (existing) {
      throw new BusinessError("User already exists");
    }
    await this.userRepo.save(user);
  }
}

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

3. טיפול בשגיאות בשכבת ה־Controllers (Interface Adapters)

השכבה החיצונית של האפליקציה תופסת את השגיאות ומחזירה תגובות ברורות ללקוח:

app.post("/users", async (req, res) => {
  try {
    await registerUserUseCase.execute(req.body);
    res.status(201).send("User created");
  } catch (err) {
    if (err instanceof BusinessError) {
      res.status(400).send(err.message);
    } else {
      res.status(500).send("Internal Server Error");
    }
  }
});

4. שימוש אחיד בלוגים (Logging)

הימנעו מלכתוב console.log בכל מקום. במקום זאת השתמשו בספריית לוגים מסודרת (לדוגמה winston):

import winston from "winston";

const logger = winston.createLogger({
  level: "info",
  transports: [new winston.transports.Console()],
});

// בלוגיקה העסקית אין לוגים טכניים, אלא רק נקודות עסקיות משמעותיות:
logger.info("User registered successfully", { userId: user.id });
logger.error("Database connection failed", { error: err.message });

ניהול אירועים ו־Event-Driven Architecture כחלק מהארכיטקטורה הנקייה

אירועים (Events) הם דרך מצוינת להפוך את האפליקציה שלנו לגמישה, מודולרית, וקלה לתחזוקה.

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

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

דוגמה מעשית ב־Node.js ו־TypeScript:

שלב 1: יצירת תשתית אירועים פשוטה

import { EventEmitter } from "events";

// Event Dispatcher פשוט
export class DomainEventEmitter extends EventEmitter {}

export const domainEvents = new DomainEventEmitter();

שלב 2: אירועים בשכבת הליבה (Domain Layer)

// אירוע עסקי
class UserRegisteredEvent {
  constructor(public readonly userId: string,
  public readonly email: string
) {}
}

// ה־Use Case מפרסם אירוע
class RegisterUserUseCase {
  constructor(private userRepo: IUserRepository) {}

  async execute(user: User) {
    const existing = await this.userRepo.findByEmail(user.email);
    if (existing) {
      throw new BusinessError("User already exists");
    }
    await this.userRepo.save(user);

    domainEvents.emit("UserRegistered", new UserRegisteredEvent(user.id, user.email));
  }
}

שלב 3: האזנה לאירועים בשכבת התשתית

// שליחת אימייל בעת אירוע
domainEvents.on("UserRegistered", async (event: UserRegisteredEvent) => {
  try {
    await emailService.sendWelcomeEmail(event.email);
    logger.info("Welcome email sent", { userId: event.userId });
  } catch (err) {
    logger.error("Failed to send welcome email", { error: err.message });
  }
});

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

טיפים לניהול אירועים נכון בארכיטקטורה נקייה:

אירועים עסקיים בשכבה העסקית בלבד – שכבת ה-UI או DB לא מפרסמת אירועים.

מאזינים (Listeners) בשכבה החיצונית – כך השכבה העסקית נשארת נקייה.

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

טעויות נפוצות שכדאי להימנע מהן:

זליגת אירועים טכניים ללוגיקה העסקית:
אל תייצרו אירועים טכניים ("DB התעדכן", "API נענה") בתוך השכבה העסקית.

ריבוי אירועים מיותרים:
כל אירוע צריך לייצג משהו בעל משמעות עסקית ברורה ("משתמש נרשם", "הזמנה אושרה").

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

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


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

אשמח לשמוע איך אתם מנהלים את השגיאות והאירועים בפרויקטים שלכם. האם יצא לכם להיתקל בקשיים? האם יש פתרונות מעניינים שהייתם רוצים לשתף?

ספרו לי בתגובות – נתראה שם!

ניהול השכבה העסקית (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 חדשים.

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

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

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