בפוסט הקודם סקרנו מהי ארכיטקטורה נקייה ולמה היא טובה לנו. דיברנו על איך חלוקת המערכת לשכבות מבודדות יכולה למנוע "קוד ספגטי" ולשמור על הקוד קריא ופשוט יותר לתחזוקה. אבל תאוריה לחוד ומציאות לחוד – איך לוקחים את העקרונות היפים האלה ולמעשה מיישמים אותם בפרויקט אמיתי?
בואו נדמיין מצב: יש לנו פרויקט קיים שמתחיל לקרטע בתחזוקה, או שאולי אנחנו עומדים להתחיל פרויקט חדש ורוצים "להתחיל ברגל ימין" מבחינה ארכיטקטונית. מה עושים עכשיו? ננסה לענות על זה בגובה העיניים.
איך מתחילים ליישם ארכיטקטורה נקייה בפועל?
- בפרויקט חדש: נרצה להניח את התשתית הנקייה מהיום הראשון. זה לא אומר לכתוב טונות של קוד לפני שיש אפליקציה עובדת, אבל זה כן בהחלט אומר לתכנן את גבולות הגזרה מראש. בשלב אפיון הפרויקט, חשבו:
מהן היישויות המרכזיות (Entities) והפעולות העסקיות (Use Cases) שהמערכת צריכה לבצע?
התחילו מלפרק את הדרישות למקרי שימוש ברורים.
בשלב הקוד, אפשר להתחיל בקטן: לבחור מקרה שימוש אחד ולממש אותו בצורה "נקייה" מקצה לקצה – כך תיצרו דפוס שאפשר לשכפל בהמשך. הקפידו להפריד כבר מהתחלה בין חלקי הקוד השונים: לוגיקה עסקית במקום אחד, קוד של גישה למסד נתונים/Filesystem במקום אחר, וקוד של HTTP או ממשק משתמש במקום נוסף. אם תתחילו ככה, גם אם לא הכל מושלם, תהיו בכיוון הנכון. - בפרויקט קיים: כאן האתגר גדול משמעותית, כי יש מערכת עובדת (ואולי מבולגנת) שצריך בהדרגה "לנקות". החדשות הטובות הן שלא חייבים (ולא כדאי) לבצע כתיבה מחדש מאפס. במקום זאת, אפשר לאמץ את עקרונות הארכיטקטורה הנקייה צעד אחר צעד: בחרו רכיב או איזור בעייתי בקוד והתחילו לבודד אותו. למשל, אם יש חלק בקוד שכולל גם לוגיקה וגם קריאות ישירות למסד נתונים, נסו להפריד ביניהם: צרו מחלקת שירות עסקי (Use Case) שמכילה את הלוגיקה, והזיזו את הקריאות למסד נתונים למחלקת "מאגר" (Repository) נפרדת. הגדירו ממשק (interface) בין השניים, כדי שהלוגיקה לא תהיה תלויה במימוש הטכני. באופן הדרגתי, כל פונקציונליות חדשה שתוסיפו – כבר תכתבו לפי העקרונות הנקיים, וחלקים ישנים תוכלו לשפר בהדרגה כאשר נוגעים בהם. כן, ייתכן שבתקופת הביניים תחיו עם שילוב של סגנונות, אבל זה בסדר כל עוד אתם במגמת שיפור מתמדת.
בין אם הפרויקט חדש או ישן, חשוב לגשת בגישה פרקטית: לאמץ את מה שנותן ערך מהר, ולא להפוך את הארכיטקטורה הנקייה לפרויקט גרנדיוזי שמונע התקדמות. בהמשך נדבר בדיוק על ההחלטות שצריך לקחת בזמן התכנון והיישום.
תכנון מקרי השימוש והליבה העסקית
אחד השלבים הראשונים בארכיטקטורה נקייה הוא הגדרת מקרי השימוש (Use Cases) של המערכת. מקרה שימוש הוא בעצם תרחיש עסקי או פעולה שהמערכת מבצעת – משהו בעל ערך למשתמש או לעסק. למשל, במערכת הזמנות זה יכול להיות "יצירת הזמנה חדשה", "ביטול הזמנה", "הוספת פריט לסל הקניות". בכל פרויקט, גדול או קטן, כדאי לרשום את הפעולות המרכזיות ולהבין מה כל אחת מקבלת, עושה, ומחזירה. כל מקרה שימוש כזה יהפוך כנראה למחלקה או מודול בלוגיקה העסקית שלנו.
איך לתכנן Use Case? חשבו על כל מקרה שימוש כעל יחידה עצמאית של לוגיקה עסקית. הוא צריך לדעת לעשות דבר אחד (עיקרון האחריות היחידה, אם תרצו) ולעשות אותו טוב. בתוך ה-Use Case נגדיר את הזרימה העסקית: למשל, עבור "רישום משתמש חדש" נגדיר מה קורה – בדיקת תקינות נתונים, יצירת אובייקט משתמש, שמירתו, ושליחת אימייל ברכה אולי. בשכבה הזו לא נרצה לראות שום קוד שקשור לפרטים טכניים כמו איך בדיוק שומרים במסד נתונים או איך נשלח האימייל – את הפרטים האלה נשאיר לשכבות אחרות. ה-Use Case פועל על ממשקים אבסטרקטיים ומפעיל אותם, כך שהוא מגדיר מה עושים, לא איך בפועל עושים את זה.
במקביל, נגדיר את היישויות (Entities) – המודלים הבסיסיים של המידע במערכת. אלה לרוב יהיו מחלקות פשוטות או מבני נתונים שמייצגים דברים כמו משתמש, הזמנה, מוצר וכו'. היישויות יכולות להכיל לוגיקה עסקית בסיסית שקשורה אליהן (למשל, מתודת calculateTotal()
בתוך אובייקט הזמנה), אבל הן יהיו מנותקות מפרטים טכניים. היישויות ומקרי השימוש יחד מהווים את "ליבת המערכת" – החלק החשוב ביותר, שממוקד במה המערכת עושה ללא תלות באיך היא עושה זאת.
בקיצור, בשלב התכנון ענו לעצמכם: מה המערכת שלי עושה? התשובות הן מקרי השימוש שלכם. אילו נתונים ועצמים מרכזיים מעורבים? אלה היישויות שלכם. את אלה נשים במרכז, וכל השאר (מסדי נתונים, שירותי צד ג', ממשק משתמש) ייבנה סביבם כ"תומך" בפעולות הללו.
ארגון הקבצים והתיקיות בפרויקט נקי

אחרי שמבינים מהם חלקי המערכת, צריך לשקף את ההפרדה הזאת גם במבנה הפרויקט. ארגון נכון של תיקיות וקבצים יעזור לצוות לזהות במהירות מה שייך לאן, ויאכוף בצורה עקיפה את עקרון ההפרדה. אין "מתכון יחיד" שמוסכם על כולם לארגון תיקיות בארכיטקטורה נקייה, אבל הנה גישה נפוצה אחת:
src/
├── domain/ # שכבת הדומיין (ליבה עסקית)
│ ├── entities/ # ישויות ומודלים עסקיים
│ └── interfaces/ # ממשקים אבסטרקטיים (למשל, Repository)
├── use-cases/ # שכבת היישום (מקרי שימוש)
├── infrastructure/ # שכבת תשתית (מימושים טכניים)
│ ├── db/ # קוד גישה למסד נתונים
│ ├── services/ # שירותים חיצוניים (לדוגמה, SMTP, API חיצוני)
│ └── frameworks/ # קוד מסגרת (HTTP server, Express וכו')
└── interface-adapters/ # מתאמי ממשק (למשל, Controllers/Routes, View models)
זו רק דוגמה למבנה אפשרי. העיקרון המנחה: הפרידו לפי שכבות אחריות. אפשר לארגן לפי סוגי שכבות (כמו בדוגמה למעלה), או לחלופין לפי תתי-מערכת/מודולים ואז בכל מודול לשים תיקיות של דומיין, תשתית וכד'. מה שחשוב הוא שבסופו של דבר, ניתן יהיה לראות בבירור מהו קוד לוגי עסקי ומהו קוד שקשור לצד הטכני. לדוגמה, קובצי Repository (גישה לנתונים) ימוקמו בתשתית, בעוד שממשקי ה-Repository והלוגיקה העסקית שצורכת אותם יהיו בדומיין/UseCases.
עוד טיפ בארגון הקבצים: שמות ברורים. השתמשו בשמות שמרמזים על התפקיד בשכבה. למשל, UserRepository
עבור ממשק, MongoUserRepository
עבור המימוש שלו, או UserController
עבור קובץ שמטפל בבקשות HTTP של משתמש. ככה, גם בלי לפתוח את הקובץ, מפתח יבין מה התפקיד שלו.
זכרו שאין צורך להגזים במספר השכבות אם הן לא מוסיפות ערך ברור. לפרויקט קטן ייתכן שתעבדו רק עם 3 תיקיות עיקריות: domain
, application
(use cases) ו-infrastructure
. לפרויקט גדול אולי תוסיפו גם שכבת interface-adapters
לתיווך. התאימו את המבנה לגודל וצרכי הפרויקט, אבל שמרו על העיקרון שכל שכבה תלויה רק בשכבות ה"פנימיות" ממנה (ולא ההיפך).
היפוך תלות: מתי ליצור ממשקים והזרקת תלויות
עיקרון מפתח בארכיטקטורה נקייה הוא היפוך התלות (Dependency Inversion Principle) – במקום שהלוגיקה שלנו תהיה תלויה במימושים טכניים, נעדיף שהמימושים יתלו דווקא בהגדרות אבסטרקטיות של הלוגיקה. מה זה אומר בפועל? שימוש בממשקים (interfaces) בכל פעם שהלוגיקה העסקית שלנו צריכה משהו שנמצא מחוץ לה.
מתי נכון ליצור ממשק? בכל נקודת מגע בין הליבה העסקית לבין שירות חיצוני, מסד נתונים או מערכת אחרת. דוגמה קלאסית: גישה לנתונים. אם ה-Use Case שלנו צריך לשמור אובייקט במסד נתונים, הוא לא יקרא ישירות לפונקציה של MongoDB או PostgreSQL. במקום זה נגדיר ממשק כמו IUserRepository
עם מתודות כמו save
ו-getById
. הממשק הזה יוגדר בשכבת הדומיין או היישומים (פנימית), וה-Use Case יקבל משהו שמממש את הממשק הזה. המשהו הזה – המימוש – יחיה בשכבת התשתית (למשל מחלקת MongoUserRepository
). כך ה-Use Case "מדבר" בשפה שלו (אבסטרקטית), ולא אכפת לו אם מתחת אנחנו עובדים עם Mongo, קובץ, או כל דבר אחר.
מלבד מאגרים (Repositories), יצירת ממשקים נכונה גם עבור שירותים חיצוניים: למשל, ממשק IMailService
לשליחת אימייל, ממשק IPaymentGateway
לתשלומים, וכו'. הקריטריון הוא פשוט: האם הדבר הזה עלול להשתנות או להתחלף מבלי לשנות את הלוגיקה העסקית? אם כן – הפכו אותו לממשק. אם לא (למשל, קוד לוגי פנימי לחלוטין), אין צורך בממשק בכוח. לא כל מחלקה קטנה זקוקה לממשק; התמקדו בגבולות שבין הלוגיקה לטכנולוגיה.
אחרי שיצרנו ממשקים, נשתמש בהזרקת תלויות (Dependency Injection) כדי לספק ל-Use Case את המימושים המתאימים בזמן ריצה. הזרקת תלויות היא פשוט דרך מתוחכמת להגיד "נעביר מבחוץ את האובייקט שה-Use Case צריך, במקום שה-Use Case עצמו ייצור או יאתר אותו". אגב, בשביל DI לא חייבים ספרייה מיוחדת: אפשר פשוט להעביר את התלות דרך הקונסטרקטור (Constructor Injection) או כפרמטר לפונקציה. בסביבת Node.js הרבה פעמים מנהלים את ההרכבה הזו באופן ידני בקובץ התחלתי (composition root), וזה לגמרי בסדר גמור. רק בפרויקטים מאוד גדולים אולי ניעזר בכלי DI אוטומטי, אבל לרוב אין בכך צורך בהתחלה.
המטרה של שימוש בממשקים והזרקת תלויות היא שכבת הלוגיקה שלנו תישאר מבודדת: אפשר יהיה להריץ אותה בבדיקות יחידה עם מוקים, ואפשר יהיה לשנות מימושים טכניים בלי להשפיע על הקוד העסקי.
דוגמה מעשית ב-Node.js ו-TypeScript
נניח שאנחנו רוצים לממש Use Case של "יצירת משתמש חדש" במערכת שלנו, כולל שמירת המשתמש למסד נתונים ושליחת אימייל ברכה לאחר ההרשמה. נראה איך זה נראה בקוד עם ארכיטקטורה נקייה:
ראשית, נגדיר את הממשקים והיישות בשכבת הליבה העסקית:
// ממשקים בשכבת הליבה (דומיין)
interface IUserRepository {
save(user: User): Promise<void>;
getById(id: string): Promise<User | null>;
}
interface IEmailService {
sendWelcomeEmail(user: User): Promise<void>;
}
// יישות (Entity) – ייצוג של משתמש
class User {
constructor(public id: string, public name: string, public email: string) {}
}
// Use Case – לוגיקה עסקית לרישום משתמש חדש
class CreateUser {
constructor(
private userRepo: IUserRepository,
private emailService: IEmailService
) {}
async execute(user: User): Promise<void> {
// בדיקה עסקית פשוטה
if (!user.name || !user.email) {
throw new Error("Name and email are required");
}
// שמירת המשתמש דרך ממשק המאגר
await this.userRepo.save(user);
// שליחת אימייל ברכה דרך ממשק האימייל
await this.emailService.sendWelcomeEmail(user);
}
}
מחלקת ה-CreateUser
מגדירה את מה שהיא צריכה דרך שני הממשקים, והיא לא יודעת שום דבר על איך הדברים מתבצעים מתחת. עכשיו נעבור לשכבת התשתית וניתן מימושים קונקרטיים לממשקים הללו. נניח שאנחנו משתמשים במסד נתונים MongoDB, ויש לנו שירות אימייל פשוט (נאמר, דרך SMTP או שירות צד שלישי):
// מימוש ה-Repository עבור 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 collection.findOne({ id });
}
}
// מימוש שירות האימייל (שכבת תשתית)
class SimpleEmailService implements IEmailService {
async sendWelcomeEmail(user: User): Promise<void> {
// כאן היינו משתמשים בספרייה לשליחת אימייל, לדוגמה nodemailer.
console.log(`Sending welcome email to ${user.email}...`);
// לדוגמה: await emailLibrary.send({ to: user.email, ... });
}
}
ובסוף, נחבר הכל יחד באמצעות הזרקת התלויות שלנו:
// הרכבת התלויות והרצת מקרה השימוש
const userRepository = new MongoUserRepository();
const emailService = new SimpleEmailService();
const createUserUseCase = new CreateUser(userRepository, emailService);
// דוגמה להפעלת מקרה השימוש
await createUserUseCase.execute(new User("123", "דני", "[email protected]"));
אפשר לדמיין שקוד ההרצה הזה נמצא למשל בקובץ שמאתחל את השרת (נניח בקובץ הראשי של האפליקציה), או בתוך Route ספציפי. הרעיון הוא שבנקודה הזו אנחנו יוצרים את המימושים ומזריקים אותם. אם היינו משתמשים במסגרת כמו Express, היינו מפעילים את createUserUseCase.execute()
מתוך ה-Controller של הנתיב הרלוונטי, ומעבירים לו את הנתונים מהבקשה.
שימו לב כיצד ה-Use Case שלנו (CreateUser
) לא יודע שום דבר על MongoDB, על console.log, או על איפה האפליקציה רצה. הוא פשוט מבצע את הלוגיקה העסקית: בודק את הנתונים, שומר, ושולח אימייל דרך הממשקים שניתנו לו. עבור בדיקות אוטומטיות – אפשר ליצור אובייקט CreateUser
ולהזריק לו אובייקטי דמה שמממשים את הממשקים (למשל, FakeUserRepository
ששומר למערך בזיכרון, ו-FakeEmailService
שלא באמת שולח כלום), וכך לבדוק את הלוגיקה בלי תלות במסד אמיתי או בסביבת אימייל. עבור פיתוח – זה אומר שאם מחר נחליט לעבור מ-MongoDB ל-PostgreSQL, נכתוב מחלקת Repository חדשה ונתחוף אותה במקום MongoUserRepository
, בלי לגעת בשורה של CreateUser
. בדיוק בשביל הגמישות הזו התכנסנו.
בשורה התחתונה – טיפים להטמעה מוצלחת של הגישה
לפני סיום, הנה כמה המלצות פרקטיות לצוותים שמתחילים לאמץ ארכיטקטורה נקייה, שיעזרו לכם להתמקד בעיקר:
- הפרידו לוגיקה מתשתית מהיום הראשון: גם אם לא בונים את כל השכבות בבת אחת, כדאי לפחות ליצור הבחנה ברורה בין קוד עסקי לקוד של גישה למסד, תקשורת, וכו'. אפילו אם זה אומר לשים אותם בקבצים נפרדים או מודולים שונים – זה צעד ראשון חשוב שישתלם מהר מאוד.
- אל תנסו לתכנן הכול מראש עד הפרט האחרון: ארכיטקטורה נקייה היא מדריך, לא חוק קשיח. ייתכן שבהתחלה לא תהיו בטוחים מהם כל מקרי השימוש או כל הממשקים שתצטרכו. זה בסדר! התחילו ממה שברור, ובנו את הארכיטקטורה בהדרגה תוך כדי למידה. עדיף ליישם 80% מהעקרונות באופן עקבי מאשר לשאוף ל-100% ולתקוע את הפרויקט.
- ממשקים – השתמשו במידה הנכונה: כפי שאמרנו, לא לכל מחלקה צריך ממשק. התמקדו ביצירת ממשקים בגבולות שבין הלוגיקה למימוש חיצוני. אם יש לכם שירות פנימי שמשמש רק את הלוגיקה ואין סיבה להחליף אותו – אפשר לוותר על יצירת ממשק עבורו בשלבים הראשונים. המנעו מאינסוף ממשקי-סרק. במקום זאת, צרו אותם איפה שזה מעניק לכם גמישות או יכולת בדיקה.
- Layered, but pragmatic: המטרה אינה לייצר יותר שכבות מקובצת "כי הארכיטקט אמר". המטרה היא להקל על החיים שלנו כמפתחים. אם אתם מרגישים שחלק מסוים בארכיטקטורה הנקייה מסרבל את הפיתוח בלי תועלת ברורה – בחנו את זה. ייתכן שצריך לכוונן את הפרקטיקה (אולי לאחד שתי שכבות, אולי לדחות הכנסת ספריית DI מתוחכמת לשלב מאוחר יותר). שמרו על הרוח הכללית של ההפרדה, אבל התאימו אותה לצרכים שלכם וליכולות שלכם כצוות.
- למדו מהתנסות ושפרו בהתמדה: אחרי שמיישמים Use Case אחד או שניים, עשו רטרוספקטיבה: האם המבנה שבחרנו עוזר לנו, או שיש תוספות/שינויים שכדאי לעשות? ארכיטקטורה נקייה יכולה לחייב משמעת, אבל ברגע שרואים את היתרונות (קוד קריא, בדיקות שעוברות חלק, יכולת שינוי מהירה), המוטיבציה של הצוות תעלה. שתפו אחד את השני בדוגמאות, כתבו הנחיות פנימיות אם צריך, ותמיד היו פתוחים לשפר את הסדר בפרויקט.
ולסיום, זכרו שארכיטקטורה נקייה היא אמצעי ולא מטרה בפני עצמה. המטרה היא ליצור תוכנה שעובדת היטב, קל לשנות אותה, וכיף לעבוד בה. אם תשמרו על עיקרון הבידוד של הלוגיקה העסקית ותהיו פרגמטיים בשאר – אתם בדרך הנכונה.
וכמו תמיד – אשמח לשמוע מכם בתגובות! 🚀 האם התחלתם ליישם ארכיטקטורה נקייה בפרויקטים שלכם? נתקלתם בהתלבטויות או תובנות מעניינות? אל תהססו לשתף או לשאול.