ניהול טעויות ואירועים – 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 נענה") בתוך השכבה העסקית.

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

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

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


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

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

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

מהי ארכיטקטורה נקייה ומה המטרה שלה?

הקדמה

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

זה בדיוק מה שארכיטקטורה נקייה באה לפתור.

רגע, מה זה בכלל ארכיטקטורה נקייה?

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

איך זה נראה בפועל?

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

  • Entities (יישויות) – אלו הם האובייקטים הבסיסיים ביותר במערכת, כמו משתמשים, הזמנות, מוצרים וכדומה. כאן נמצאים המודלים העסקיים הטהורים, ללא שום קשר לטכנולוגיות או תשתיות ספציפיות.
  • Use Cases (מקרי שימוש) – השכבה הזו מגדירה את הפעולות או התהליכים שהמערכת יודעת לבצע באמצעות אותן ישויות. לדוגמה, "יצירת הזמנה חדשה", "רישום משתמש" או "עדכון פרטי מוצר". כאן נמצאת הלוגיקה העסקית עצמה.
  • Interface Adapters (מתאמי ממשק) – השכבה הזו מתווכת בין הליבה העסקית (Use Cases ו-Entities) לבין העולם החיצוני. למשל, היא מתרגמת מידע שמגיע מהמשתמש לתוך המערכת ומכינה נתונים פנימיים להצגה החוצה.
  • Frameworks & Drivers (מסגרות ותשתיות) – השכבה החיצונית ביותר, שכוללת את כל הכלים והטכנולוגיות החיצוניות, כמו בסיסי נתונים, שרתי HTTP, או ספריות חיצוניות שונות.

הכלל הכי חשוב בארכיטקטורה נקייה

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

ולמה כל זה שווה לנו?

יש כמה יתרונות מאוד גדולים לשימוש בארכיטקטורה נקייה:

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

בואו נראה דוגמה קצרה ב־TypeScript ו־Node.js

הנה דוגמה פשוטה להמחיש את העיקרון של ארכיטקטורה נקייה:

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

// זה ממשק מופשט בשכבת הליבה
interface IUserRepository {
  save(user: User): Promise<void>;
  getById(id: string): Promise<User | null>;
}

// אובייקט עסקי פשוט
class User {
  constructor(public id: string, public name: string) {}
}

// שכבת הלוגיקה העסקית (Use Case)
class CreateUser {
  constructor(private repo: IUserRepository) {}

  async execute(user: User) {
    if (!user.name) throw new Error('Name is required');
    await this.repo.save(user);
  }
}

עכשיו, בשכבת התשתית, נממש את הממשק הזה באמצעות MongoDB, למשל:

// מימוש של הממשק עבור MongoDB (שכבת תשתית חיצונית)
class MongoUserRepository implements IUserRepository {
  async save(user: User): Promise<void> {
    const collection = mongo.db('app').collection('users');
    await collection.insertOne(user);
  }

  async getById(id: string): Promise<User | null> {
    const collection = mongo.db('app').collection('users');
    return await collection.findOne({ id });
  }
}

ולבסוף, בהרכבת המערכת הראשית שלנו:

const userRepository = new MongoUserRepository();
const createUserUseCase = new CreateUser(userRepository);

await createUserUseCase.execute(new User('123', 'Aviv'));

שימו לב:
הקוד של CreateUser לא יודע כלום על MongoDB או על טכנולוגיה אחרת. הוא עובד רק מול ממשק מופשט (IUserRepository). זה כל הסוד – בידוד מוחלט של לוגיקה עסקית מטכנולוגיה חיצונית.

בשורה התחתונה – למי זה מתאים?

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

וכמו תמיד – אשמח לשמוע מכם בתגובות!
יש שאלות? רעיונות? משהו לא ברור? דברו איתי!