התנעת פרויקט הדגל — Git, PRD, MVP
התנעת פרויקט הדגל
שיעור זה שונה מכל שיעור שלמדת. אין Dry Run. אין MCQ. יש פעולה אחת: לבחור פרויקט, לתכנן אותו כמו מהנדס, ולהתחיל לבנות אותו.
- בחירת פרויקט — קריטריונים, רשימת רעיונות, מה לא לבחור
- PRD — מסמך דרישות: feature list, out of scope, success criteria
- MVP — Minimum Viable Product, "Done is better than perfect"
- Git — init, add, commit, push, branch, merge, .gitignore
- GitHub — Remote repository, README.md, Git history כ-portfolio
- venv — בידוד סביבת Python, requirements.txt
- מבנה פרויקט מקצועי — src/, tests/, main.py, __init__.py
- Agile Milestones — פירוק לאבני דרך ניתנות לבדיקה
- unittest — Tests directory, setUp, assertTrue, assertLess
- Password Analyzer — דוגמת פרויקט מלאה: Singleton, OOP, CLI
בחירת הפרויקט: הקריטריונים שחשובים
הטעות הנפוצה: לבחור "פרויקט מרשים" בלי לחשוב כיצד לדבר עליו. פרויקט פשוט שאתה מבין לעומק ויכול להסביר עדיף על פרויקט מסובך שמשכת מ-GitHub.
| קריטריון | מה בודקים | משקל |
|---|---|---|
| רלוונטיות לסייבר/טכנולוגיה | חיבור לנושא המיון — כלי אבטחה, ניתוח נתונים, אוטומציה | ⭐⭐⭐ |
| שימוש ב-OOP | Classes, Inheritance, Encapsulation — ניראה בסדנה | ⭐⭐⭐ |
| קצר ומושלם > ארוך ושבור | MVP שעובד 100% עדיף על פרויקט ענק שמתרסק | ⭐⭐⭐ |
| Git History נקי | 15+ commits עם הודעות ברורות — מוכיח תהליך עבודה | ⭐⭐ |
| README מפורט | מה הפרויקט, כיצד להריץ, דוגמאות פלט | ⭐⭐ |
רעיונות מומלצים לפרויקט
🔐 Password Strength Analyzer — מנתח חוזק סיסמה עם class PasswordPolicy ו-Singleton.
🌐 Port Scanner בסיסי — סורק פורטים פתוחים ב-IP נתון עם socket.
📊 Log Analyzer — קורא קובץ log, מנתח שגיאות, מדפיס סטטיסטיקה (File I/O).
🔑 Caesar/Vigenere Cipher — הצפנה קלאסית עם ממשק encrypt/decrypt. OOP מלא.
🗂️ Student Grade Manager — מנהל ציונים, ממוצעים, תעודות. Class system עם Inheritance.
! אל תבחר את הפרויקטים האלה ›
❌ "פרויקט AI" שמשתמש ב-API של ChatGPT — זה לא שלך. אי אפשר להסביר את הקוד.
❌ פרויקט שהורדת מ-GitHub ו"שיפרת" — הראיין יבין מיד.
❌ פרויקט Web מלא עם React+Backend — יותר מדי. אתה צריך עומק, לא רוחב.
❌ פרויקט "מרשים" שאתה לא מבין: "בניתי מנוע machine learning" — "ספר לי על הארכיטקטורה" — "..."
Git: Version Control מהאפס
בלי Git = אין ראיות שעבדת. עם Git = רואים 25 commits → רואים תהליך עבודה אמיתי. GitHub Profile = ה-LinkedIn של מהנדס תוכנה.
Workflow יומי: Add → Commit → Push
# הגדרה ראשונה (פעם אחת בלבד)
git config --global user.name "שמך"
git config --global user.email "email@example.com"
# יצירת Repository
mkdir my_project && cd my_project
git init
# Workflow יומי
git status # ראה מה השתנה (אדום=unstaged, ירוק=staged)
git add . # Stage כל הקבצים
git commit -m "Add password length validation" # Snapshot
git push origin main # שלח ל-GitHub! הודעות Commit — הטעות הנפוצה ביותר ›
❌ "fix" — לא מסביר כלום
❌ "update" — update מה?
❌ "asdfgh" — נראה מגוחך לראיין
✅ "Add input validation for password length"
✅ "Fix: handle empty password input correctly"
✅ "Refactor: extract scoring logic to separate method"
כלל: הודעת commit = משפט שמסביר מה ולמה. Past tense, English (convention).
Branches ו-.gitignore
# Branch — עבודה מקבילה
git checkout -b feature/blacklist-check # צור + עבור
git add . && git commit -m "Add common password blacklist"
git checkout main
git merge feature/blacklist-check
# פקודות שימושיות
git log --oneline # history קצר
git diff # מה השתנה (לפני stage)
git stash # שמור שינויים "בכיס"
git stash pop # הוצא מהכיס
# .gitignore — מה לא להעלות
# __pycache__/ *.pyc
# venv/ .venv/
# .env secrets.json ← אל תעלה אף פעם!
# .idea/ .vscode/Virtual Environment: בועה מבודדת לכל פרויקט
venv = סביבת Python מבודדת לכל פרויקט. כל פרויקט יכול להשתמש בגרסאות שונות של ספריות — בלי שיתנגשו.
# יצירה
cd my_project
python3 -m venv venv
# הפעלה
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# ה-prompt: (venv) user@machine:~/my_project$
# התקנת ספריות
pip install requests pytest
# שמירת רשימה
pip freeze > requirements.txt # "תעודת הזהות" של הסביבה
# כשמישהו מוריד הפרויקט
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt # מתקין הכל בבת-אחת
# ביטול
deactivate! venv/ לא מועלה ל-GitHub — תמיד ב-.gitignore! ›
תיקיית venv/ יכולה לשקול 50-200MB ומכילה binaries ספציפיים ל-OS. הכל מה שצריך להעלות: requirements.txt (כמה KB). מי שמוריד: יריץ pip install -r requirements.txt וישחזר הכל.
Password Analyzer: פרויקט מלא לדוגמה
הנה ארכיטקטורת פרויקט שלמה המדגימה את כל מה שלמדת: Singleton, Encapsulation, Properties, CLI, Tests.
import re
from .policy import PasswordPolicy
class PasswordAnalyzer:
"""מנתח חוזק סיסמאות — orchestrates הניתוח."""
def __init__(self):
self._policy = PasswordPolicy.get_instance()
def analyze(self, password: str):
scores, suggestions = {}, []
# בדיקת אורך
length = len(password)
if length >= 16: scores["length"] = 30
elif length >= 12: scores["length"] = 20
elif length >= 8:
scores["length"] = 10
suggestions.append("Increase length to 12+")
else:
scores["length"] = 0
suggestions.append(f"Too short ({length} chars). Use 12+.")
# בדיקת סוגי תווים
char_checks = {
"uppercase": (r"[A-Z]", 15, "Add uppercase letters"),
"lowercase": (r"[a-z]", 15, "Add lowercase letters"),
"digits": (r"\d", 15, "Add numbers"),
"special": (r"[!@#$%^&*]", 25, "Add special characters"),
}
for key, (pattern, pts, msg) in char_checks.items():
if re.search(pattern, password): scores[key] = pts
else: scores[key] = 0; suggestions.append(msg)
# בדיקת סיסמאות נפוצות
if self._policy.is_common(password):
scores["uniqueness"] = -50
suggestions.insert(0, "⚠️ Common password!")
total = max(0, sum(scores.values()))
return PasswordReport(password, total, suggestions)class PasswordPolicy:
"""Singleton: כלל מדיניות הסיסמאות — instance אחד בלבד."""
_instance = None
_common_passwords: set = set()
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls.__new__(cls)
cls._instance._load_common_passwords()
return cls._instance
def _load_common_passwords(self):
try:
with open("data/common_passwords.txt") as f:
PasswordPolicy._common_passwords = {line.strip().lower() for line in f}
except FileNotFoundError:
PasswordPolicy._common_passwords = {"password","123456","qwerty","admin"}
def is_common(self, password: str) -> bool:
return password.lower() in self._common_passwordsאתגר מעשי — פרויקט הדגל — Git וגרסאות
שיא השיעור: לבנות ולתקן מערכות אמיתיות — מפרשים, פרוטוקולים, מכונות. פתרו את החובה ואז את האתגר; ההגשה נבדקת אוטומטית מול מקרי-קצה נסתרים.
סיכום השיעור
- בחירת פרויקט: קריטריונים — רלוונטיות, OOP, MVP עובד, Git history, README
- PRD: feature list, out of scope, success criteria, milestones
- MVP: קטן ועובד 100% עדיף על גדול ושבור — "Done is better than perfect"
- Git: init, add, commit, push, branch, merge, .gitignore — workflow יומי
- GitHub: README.md, Git history כ-portfolio, Activity Graph
- venv: בידוד סביבת Python, requirements.txt, מה ב-.gitignore
- מבנה פרויקט: src/, tests/, main.py, __init__.py, data/
- Agile Milestones: פירוק עבודה לאבני דרך ניתנות לבדיקה
- unittest: setUp, assertLess/assertGreater — tests כתיעוד חי
עד שיעור 11: בחר פרויקט → כתוב PRD → git init → כתוב Class ראשי → git commit + push.
שאלות תרגול ותשובות (12)
שאלה 1.1 · שני מועמדים: אחד עם Password Analyzer ב-200 שורות קוד נקי + OOP + Git עם 20 commits. השני עם Web App עם 500 שורות שעובד בקושי. מי יותר מרשים בראיון?
- א. השני, שכן יותר שורות קוד מעידות על יותר ניסיון: הנחה שגויה, אף שקוד נקי שעובד עדיף על היקף גדול ושבור כאן
- ב. שניהם שווים לחלוטין בעיני המראיין: הנחה שגויה, אף שמוצר נקי שרץ מרשים הרבה יותר מפרויקט ענק ושבור בהרצה
- ג. תלוי בשפת התכנות שבה נכתב כל פרויקט: הנחה שגויה, אף שהקריטריון הוא איכות והבנה עמוקה ולא בחירת שפת התכנות
- ד. הראשון בפער גדול, שכן מוצר מינימלי נקי עם הבנה עמוקה עדיף על פני פרויקט ענק ושבור שבקושי רץ בהרצה כאן
✓ תשובה: ד) — הראיין בגאמא מחפש: הבנה עמוקה, תהליך עבודה מסודר, יכולת הסבר. "Less is more" הוא עיקרון מהנדסי אמיתי. python main.py → crash = רושם גרוע. python main.py → עובד = מרשים.
שאלה 1.2 · מה MVP (Minimum Viable Product)?
- א. גרסה שעדיין בפיתוח ומכילה את כל התכונות המתוכננות: הנחה שגויה, אף ש-MVP הוא מינימלי ועובד במלואו כבר כעת
- ב. גרסה ניסיונית שעלולה להיכשל בזמן ההרצה: הנחה שגויה, אף ש-MVP חייב לעבוד לחלוטין ולא רק להיות בגדר ניסיון
- ג. הגרסה הקטנה ביותר שעדיין מבצעת את הפעולה המרכזית ועובדת לחלוטין מקצה לקצה ללא תקלות כלשהן בהרצה כאן
- ד. גרסה מינימלית לחלוטין וללא כל תיעוד נלווה: הנחה שגויה, אף ש-MVP כולל תיעוד קריא לצד הפונקציונליות שלו כבר
✓ תשובה: ג) — MVP = "Done is better than perfect". עובד end-to-end, ניתן לבדיקה, ניתן להרצה ב-3 פקודות מ-README, קוד קריא ומתועד. Feature שבור = חוב טכני.
שאלה 1.3 · מה Out of Scope ב-PRD?
# הגדרה ראשונה (פעם אחת בלבד) git config --global user.name "שמך" git config --global user.email "email@example.com" # יצירת Repository mkdir my_project && cd my_project git init # Workflow יומי git status # ראה מה השתנה (אדום=unstaged, ירוק=staged) git add . # Stage כל הקבצים git commit -m "Add password length validation" # Snapshot git push origin main # שלח ל-GitHub
- א. תכונות שנשמטו בטעות מן התכנון המקורי: הנחה שגויה, אף שמחוץ-לתחום הוא החלטה מכוונת ולא השמטה בשגגה כלשהי
- ב. באגים ידועים שטרם תוקנו בקוד הקיים: הנחה שגויה, אף שמחוץ-לתחום מגדיר תכולה ואינו רשימת תקלות פתוחות כלשהי
- ג. התיעוד הנלווה של הפרויקט כולו: הנחה שגויה, אף שמחוץ-לתחום מגדיר מה לא יפותח כעת ואינו קשור לתיעוד כלל כאן
- ד. דברים שהוחלט במכוון לא לממש בגרסה הנוכחית, מה שמגדיר את גבולות הפרויקט וממקד את מאמצי הפיתוח כולם כאן
✓ תשובה: ד) — Out of Scope הוא מכוון, לא שגגה. "הוחלט לא לממש כרגע" = scope management. בדוגמת Password Analyzer: GUI, חיבור ל-DB, HIBP API — כולם Out of Scope ב-V1.
שאלה 4.1 · מה ההבדל בין git add לבין git commit?
- א. שתי הפקודות זהות לחלוטין בפעולתן: הנחה שגויה, אף ש-add מכין את השינויים ו-commit שומר אותם בהיסטוריה כאן
- ב. add מעלה ל-GitHub ו-commit שומר מקומית: הנחה שגויה, אף ששתיהן מקומיות ורק push מעלה בפועל לשרת המרוחק כאן
- ג. git add מעביר שינויים לאזור ההיערכות, ואילו git commit מצלם את הקבצים המוכנים ושומר אותם בהיסטוריה כאן
- ד. commit שקול ל-add ואחריו push: הנחה שגויה, אף ש-commit שומר מקומית בלבד ואינו מעלה כלל לשרת המרוחק בעצמו
✓ תשובה: ג) — Workflow: עבוד → git add . (stage) → git commit -m "message" (snapshot) → git push (GitHub). שלושה שלבים נפרדים.
שאלה 4.2 · מה .gitignore? ציין 3 סוגי קבצים שתמיד צריך להוסיף.
- א. קובץ המגדיר את הענף הראשי של המאגר: הנחה שגויה, אף ש-gitignore מגדיר קבצים להתעלמות ואינו מגדיר ענפים כלל
- ב. קובץ המגדיר אילו קבצים Git לא יעקוב אחריהם, ובהם תיקיית venv ותיקיית __pycache__ וקובץ env עם הסיסמאות
- ג. קובץ המכיל את היסטוריית ה-commits: הנחה שגויה, אף ש-gitignore מגדיר התעלמות מקבצים ואינו שומר היסטוריה כלל
- ד. קובץ הגדרות Git הגלובלי של המשתמש: הנחה שגויה, אף ש-gitignore הוא מקומי למאגר ומגדיר קבצים להתעלמות מהם כאן
✓ תשובה: ב) — .gitignore מגדיר מה Git מתעלם ממנו. חיוני: venv/ (גדול ולא portable), __pycache__/ (compiled files), .env (אל תעלה סיסמאות ו-API Keys לGitHub אף פעם!).
שאלה 4.3 · מה if __name__ == "__main__": אומר? למה חשוב?
# יצירה cd my_project python3 -m venv venv # הפעלה source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows # ה-prompt: (venv) user@machine:~/my_project$ # התקנת ספריות pip install requests pytest # שמירת רשימה pip freeze > requirements.txt # "תעודת הזהות" של הסביבה # כשמישהו מוריד הפרויקט python3 -m venv venv source venv/bin/activate pip install -r requirements.txt # מתקין הכל בבת-אחת # ביטול deactivate
- א. נועד לתת שם לקובץ הנוכחי בלבד: הנחה שגויה, אף שהתנאי קובע מתי הקוד ירוץ ואינו קשור כלל לשם הקובץ עצמו כאן
- ב. נועד להגדיר את פונקציית ה-main: הנחה שגויה, אף שהתנאי שולט במועד ההרצה ואינו מגדיר את הפונקציה עצמה כלל כאן
- ג. הקוד ירוץ רק כשהקובץ מורץ ישירות, וכשהוא מיובא כמודול הבלוק אינו רץ ולכן הקוד ניתן לבדיקה נוחה יותר כאן
- ד. נועד לייבא מודולים נוספים אל הקובץ: הנחה שגויה, אף שהתנאי בודק הרצה ישירה ואינו מבצע כל ייבוא של מודולים כאן
✓ תשובה: ג) — __name__ == "__main__" רק בהרצה ישירה (python main.py). כשמיובא: __name__ = שם המודול (לא "__main__") → הבלוק לא רץ, ו-main() לא נקרא. חיוני לבדיקות: אפשר לייבא פונקציות מ-main.py בלי שהקוד הראשי ירוץ.
שאלה 6.1 · מה הפקודה הנכונה ליצירת venv חדש ב-Python 3?
- א. python3 -m venv venv: הדגל -m מריץ מודול, המילה venv היא שם המודול, והמילה השנייה היא שם תיקיית הסביבה
- ב. pip create venv: הנחה שגויה ש-pip יוצר סביבה וירטואלית, אף שהפקודה הנכונה היא python3 עם המודול venv כאן כן
- ג. virtualenv --new: הנחה שגויה שזו הפקודה הסטנדרטית ליצירה, אף שהמודול המובנה נקרא python3 עם venv בתחביר כאן
- ד. python3 venv init: הנחה שגויה שקיימת תת-פקודה בשם init, אף שהתחביר הנכון הוא python3 -m venv venv בלבד כאן
✓ תשובה: א) python3 -m venv venv — הפקודה הסטנדרטית. -m = run as module. venv = שם המודול. venv (השני) = שם תיקיית הסביבה.
שאלה 6.2 · מה pip freeze > requirements.txt עושה? למה חשוב?
- א. מוחק את סביבת ה-venv הנוכחית כולה: הנחה שגויה, אף שהפקודה מייצאת רשימת ספריות לקובץ ואינה מוחקת דבר כלשהו
- ב. מדפיס את כל הספריות המותקנות עם גרסאותיהן המדויקות לקובץ, כך ש-pip install -r משחזר את הסביבה כולה כאן
- ג. מקפיא את גרסת Python המותקנת במערכת: הנחה שגויה, אף שהפקודה שומרת את גרסאות הספריות ולא את גרסת השפה עצמה
- ד. מוחק ספריות שאינן נחוצות עוד בפרויקט: הנחה שגויה, אף שהפקודה רק מייצאת רשימה לקובץ ואינה מסירה ספריות כלשהן
✓ תשובה: ב) — requirements.txt = "תעודת זהות" הסביבה. כל שורה: package_name==version. חשוב לשיתוף: מי שמוריד הפרויקט יכול לשחזר בדיוק את אותה סביבה.
שאלה 6.3 · מה Single Responsibility Principle (SRP)?
import re
from .policy import PasswordPolicy
class PasswordAnalyzer:
"""מנתח חוזק סיסמאות — orchestrates הניתוח."""
def __init__(self):
self._policy = PasswordPolicy.get_instance()
def analyze(self, password: str):
scores, suggestions = {}, []
# בדיקת אורך
length = len(password)
if length >= 16: scores["length"] = 30
elif length >= 12: scores["length"] = 20
elif length >= 8:
scores["length"] = 10
suggestions.append("Increase length to 12+")
else:
scores["length"] = 0
suggestions.append(f"Too short ({length} chars). Use 12+.")
# בדיקת סוגי תווים
char_checks = {
"uppercase": (r"[A-Z]", 15, "Add uppercase letters"),
"lowercase": (r"[a-z]", 15, "Add lowercase letters"),
"digits": (r"\d", 15, "Add numbers"),
"special": (r"[!@#$%^&*]", 25, "Add special characters"),
}
for key, (pattern, pts, msg) in char_checks.items():
if re.search(pattern, password): scores[key] = pts
else: scores[key] = 0; suggestions.append(msg)
# בדיקת סיסמאות נפוצות
if self._policy.is_common(password):
scores["uniqueness"] = -50
suggestions.insert(0, "⚠️ Common password!")
total = max(0, sum(scores.values()))
return PasswordReport(password, total, suggestions)- א. כל מחלקה צריכה להכיל מתודה אחת בלבד: הנחה שגויה, אף שהעיקרון עוסק באחריות אחת בלבד ולא במספר המתודות שבה כאן
- ב. מותרת ירושה יחידה בלבד בין מחלקות: הנחה שגויה, אף שהעיקרון עוסק בהפרדת אחריות בין מחלקות ולא במבנה הירושה כאן
- ג. כל פונקציה צריכה להחזיר ערך אחד בלבד: הנחה שגויה, אף שהעיקרון עוסק באחריות היחידה של המחלקה ולא בערך המוחזר
- ד. כל מחלקה או פונקציה אחראית על דבר אחד בלבד, כך שהמנתח מנתח, מחלקת החוקים מחזיקה חוקים והדוח מציג בנפרד
✓ תשובה: ד) SRP — מ-SOLID Principles. הפרדת אחריויות מפשטת קריאה, בדיקה, תחזוקה. 4 classes קטנים עם 5 methods > class אחד ענק עם 20 methods.
גאמא סגנון א׳ — ראיון · מראיין שואל: "מה ה-Big-O של blacklist check בפרויקט שלך?" — מה התשובה הנכונה ב-Password Analyzer?
- א. O(1), שכן השתמשתי בקבוצה שממומשת כטבלת גיבוב ולכן בדיקת השייכות היא בזמן קבוע ממוצע ולא בזמן לינארי כאן
- ב. O(n) בסריקה לינארית על רשימה: הנחה שגויה, אף שקבוצה מאתרת בזמן קבוע ממוצע ורק רשימה נסרקת לינארית לגמרי כאן
- ג. O(log n) בחיפוש בינארי ממוין: הנחה שגויה, אף שקבוצה משתמשת בגיבוב ובזמן קבוע ולא בחיפוש בינארי לוגריתמי כאן
- ד. O(n²) בהשוואה כפולה של איברים: הנחה שגויה, אף שבדיקת שייכות בקבוצה היא בזמן קבוע ממוצע של O(1) בלבד כאן כן
✓ תשובה: א) O(1) — set ב-Python = hash table. חיפוש (x in s) = O(1) amortized. לעומת list שחיפוש = O(n). זו בדיוק שאלת ראיון קלאסית — "למה בחרת set ולא list?"
גאמא סגנון ב׳ — Git · מה הפקודה ליצירת branch חדש ומעבר אליו בבת-אחת?
- א. git checkout -b feature/x: הדגל -b יוצר ענף חדש ומיד עובר אליו, שקול לצירוף של branch ולאחריו checkout
- ב. git branch feature/x: הנחה שגויה שהפקודה גם עוברת לענף החדש, אף שהיא יוצרת ענף בלבד וללא מעבר אליו כלל כאן
- ג. git new feature/x: הנחה שגויה שקיימת תת-פקודה בשם new, אף שהתחביר הנכון ליצירה ומעבר הוא checkout עם -b כאן
- ד. git create feature/x: הנחה שגויה שקיימת תת-פקודה בשם create, אף שיצירה ומעבר בבת-אחת נעשים ב-checkout עם -b
✓ תשובה: א) git checkout -b feature/x — -b = new branch + checkout = עבור אליו. שקול ל: git branch feature/x && git checkout feature/x. בgit החדש: git switch -c feature/x.
גאמא סגנון ג׳ — ארכיטקטורה · מראיין שואל: "למה 3 Classes (Analyzer/Policy/Report) ולא class אחד?" — מה העיקרון?
def compare_versions(a: str, b: str) -> int:
pa = [int(x) for x in a.split('.')]
pb = [int(x) for x in b.split('.')]
n = max(len(pa), len(pb))
pa += [0] * (n - len(pa)); pb += [0] * (n - len(pb)) # חלקים חסרים = 0
return (pa > pb) - (pa < pb)- א. יותר מחלקות פשוט נראות מרשימות יותר: הנחה שגויה, אף שהסיבה האמיתית היא הפרדת אחריות ולא רושם חיצוני כלשהו כאן
- ב. עקרון האחריות היחידה, שכן כל מחלקה עושה דבר אחד בלבד ולכן קל יותר לבדוק ולהרחיב את הקוד של כל מחלקה בנפרד
- ג. שפת Python מחייבת חלוקה למחלקות נפרדות: הנחה שגויה, אף שזו החלטת תכנון של הפרדת אחריות ואינה כפייה של השפה כאן
- ד. מדובר במימוש של תבנית ה-Singleton: הנחה שגויה, אף שהחלוקה נובעת מעקרון האחריות היחידה ולא מתבנית Singleton כאן
✓ תשובה: ב) SRP — Single Responsibility Principle מ-SOLID. כל class = אחריות אחת. יתרון: אם Report ישתנה, Analyzer לא נשבר. קל לכתוב test לכל class בנפרד.