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

הקדמה

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

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

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

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

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

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

  • 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). זה כל הסוד – בידוד מוחלט של לוגיקה עסקית מטכנולוגיה חיצונית.

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

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

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