תבנית המצב
חלק מהיחידה תכנות מונחה עצמים במסלול ה-Java של Coddy. שיעור 74 מתוך 87.
תבנית המצב היא תבנית עיצוב התנהגותית שמאפשרת לאובייקט לשנות את ההתנהגות שלו כאשר מצבו הפנימי משתנה. נראה שהמחלקה של האובייקט משתנה, אך למעשה הוא מעביר את האחריות על ההתנהגות לאובייקטי מצב שונים. חשבו על מכונה אוטומטית: התגובה שלה ללחיצה על כפתור תלויה בשאלה אם היא במצב המתנה, אם הוכנס אליה כסף או אם היא מוציאה מוצר.
במקום להשתמש במשפטי תנאי מורכבים כדי לטפל במצבים שונים, את/ה עוטפ/ת כל מצב במחלקה משלו. התבנית כוללת הקשר שמחזיק הפניה למצב הנוכחי, ומחלקות מצב שמגדירות את ההתנהגות של כל מצב:
interface State {
void handle(Context context);
}
class Context {
private State state;
public void setState(State state) {
this.state = state;
}
public void request() {
state.handle(this);
}
}כל מצב קונקרטי מממש את ההתנהגות ויכול להפעיל מעברים למצבים אחרים:
class IdleState implements State {
public void handle(Context context) {
System.out.println("Idle: Insert coin to start");
context.setState(new ActiveState());
}
}
class ActiveState implements State {
public void handle(Context context) {
System.out.println("Active: Processing...");
context.setState(new IdleState());
}
}ההקשר מעביר בקשות למצב הנוכחי שלו, וההתנהגות משתנה אוטומטית ככל שהמצב משתנה:
Context context = new Context();
context.setState(new IdleState());
context.request(); // מצב המתנה: יש להכניס מטבע כדי להתחיל
context.request(); // מצב פעיל: מעבד...תבנית State אידיאלית כאשר ההתנהגות של אובייקט תלויה במידה רבה במצב שלו וחייבת להשתנות בזמן ריצה. היא מבטלת בלוקים גדולים של תנאים והופכת הוספה של מצבים חדשים לפשוטה — פשוט יוצרים מחלקה חדשה שמממשת את הממשק State.
אתגר
קלבואו נבנה מערכת לניהול זרימת עבודה של מסמכים באמצעות תבנית State! תיצרו מערכת שמדגימה כיצד מסמך עובר בין שלבים שונים, Draft, Review ו-Published, כאשר כל מצב קובע אילו פעולות זמינות וכיצד המסמך מתנהג כשמנסים לערוך או לאשר אותו.
תארגנו את הקוד שלכם בארבעה קבצים:
DocumentState.java: הגדירו את הממשקDocumentStateשכל המצבים יממשו. עליו להצהיר על שתי שיטות:edit(Document doc)לניסיון לערוך את המסמך, ו-approve(Document doc)לניסיון לאשר את המסמך או להעביר אותו לשלב הבא.States.java: צרו שלוש מחלקות מצב קונקרטיות שמממשות אתDocumentState:DraftState- כשקוראים ל-edit, הדפיסוEditing draft.... כשקוראים ל-approve, הדפיסוDraft approved. Moving to review.והעבירו את המסמך למצבReviewState.ReviewState- כשקוראים ל-edit, הדפיסוCannot edit during review.(ללא שינוי מצב). כשקוראים ל-approve, הדפיסוReview complete. Publishing document.והעבירו את המסמך למצבPublishedState.PublishedState- כשקוראים ל-edit, הדפיסוCannot edit published document.. כשקוראים ל-approve, הדפיסוDocument already published.. אף אחת מהפעולות לא משנה את המצב.Document.java: צרו את מחלקת ההקשר שמנהלת את המצב הנוכחי. למחלקהDocumentשלכם צריכים להיות:שדה פרטי שמכיל את ה-
DocumentStateהנוכחי, ומאותחל ל-DraftStateבבנאי.שיטה
setState(DocumentState state)שמשנה את המצב הנוכחי.שיטה
edit()שמעבירה את הקריאה לשיטתeditשל המצב הנוכחי.שיטה
approve()שמעבירה את הקריאה לשיטתapproveשל המצב הנוכחי.שיטה
getStatus()שמחזירה את שם המחלקה הפשוט של המצב הנוכחי (השתמשו ב-getClass().getSimpleName()).Main.java: הדגימו את מערכת המצבים שלכם! תקבלו קלט אחד: רצף פעולות כמחרוזת המופרדת בפסיקים, כאשר כל פעולה היאeditאוapprove(לדוגמה:edit,approve,edit,approve).צרו
Documentוהדפיסו את המצב ההתחלתי שלו בתבניתStatus: [state]. לאחר מכן עבדו על כל פעולה לפי הסדר, וקראו לשיטה המתאימה במסמך. אחרי כל פעולה, הדפיסו את המצב הנוכחי.
תקבלו קלט אחד: מחרוזת של פעולות המופרדות בפסיקים.
לדוגמה, עבור הקלט edit,approve,approve, הפלט שלכם יהיה:
Status: DraftState
Editing draft...
Status: DraftState
Draft approved. Moving to review.
Status: ReviewState
Review complete. Publishing document.
Status: PublishedStateשימו לב שאותה קריאה ל-approve() מפיקה תוצאות שונות בהתאם למצב הנוכחי של המסמך. זוהי המהות של תבנית State! המסמך לא משתמש בתנאים כדי לבדוק את מצבו; במקום זאת, הוא מעביר את האחריות להתנהגות לאובייקט המצב הנוכחי, שמטפל בפעולה כראוי ומפעיל מעברים כשצריך.
נסו בעצמכם
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();
// TODO: צור Document חדש
// TODO: הדפס את המצב ההתחלתי בפורמט "Status: [state]"
// TODO: פצל את הקלט לפי פסיקים כדי לקבל פעולות נפרדות
// TODO: עבור על כל פעולה
// - אם הפעולה היא "edit", קרא ל־edit() על document
// - אם הפעולה היא "approve", קרא ל־approve() על document
// - אחרי כל פעולה, הדפס את המצב הנוכחי
}
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות התכנות מונחה העצמים
קבצים חיצונייםמבוא לתכנות מונחה עצמיםמחלקות לעומת אובייקטיםמילת המפתח thisמתודותשדות (מאפיינים)מתודת הבנאיהעמסת בנאיםחזרה – מחשבון פשוט4ירושה
ירושה בסיסית (extends)מילת המפתח superדריסת מתודות (@Override)שרשור בנאיםהמחלקה Objectירושה יחידה ורב־רמתיתלמה אין ירושה מרובה של מחלקותסיכום – היררכיית עובדים7מתודות מיוחדות ומחלקת Object
מתודת toString()equals() ו-hashCode()מתודת clone()compareTo() ו-Comparableהממשק Comparatorסיכום – מיון מותאם אישית2מצייני גישה ואנקפסולציה
סקירה של רמות הגישהמתודות Getter ו-Setterהסתרת מידעמילת המפתח finalסיכום – מנהל חשבון בנק5פולימורפיזם
יסודות העמסת מתודותדריסת מתודות (בזמן ריצה)המרה כלפי מעלה וכלפי מטההאופרטור instanceofמחלקות ומתודות מופשטותסיכום – מחשבון צורות8מושגי OOP מתקדמים
הרכבה מול ירושהאגרגציה מול הרכבהמחלקות פנימיות, מקוננות ואנונימיותEnums ומתודות של EnumRecords (Java 16+)מחלקות חתומות (Java 17+)3מאפייני מחלקה וחברים סטטיים
משתני מופע לעומת משתנים סטטייםמתודות סטטיותבלוקים סטטייםקבועים (static final)סיכום — מונה ומחלקת עזר6ממשקים ומחלקות מופשטות
מבוא לממשקיםמימוש ממשקיםמימוש של כמה ממשקיםמתודות ברירת מחדל וסטטיות בממשקמחלקות מופשטות לעומת ממשקיםממשקים פונקציונלייםסיכום – מערכת תשלומים9גנריות
מבוא לגנריותמחלקות גנריותשיטות גנריותפרמטרי טיפוס מוגבליםתווים כלליים (?, extends, super)סיכום – מכל גנרי12תבניות עיצוב — חלק 2
תבנית הפקודהתבנית המתאםתבנית העיצובתבנית המתודה התבניתיתתבנית המצבתבנית המרוכבתתבנית האיטרטורתרגלו בעצמכם: קומפיילר Java אונליין