CodeGym /בלוג Java /Random-HE /דפוס עיצוב אסטרטגי
John Squirrels
רָמָה
San Francisco

דפוס עיצוב אסטרטגי

פורסם בקבוצה
היי! בשיעור של היום, נדבר על דפוס אסטרטגיה. בשיעורים הקודמים כבר התוודענו בקצרה למושג הירושה. למקרה ששכחת, אזכיר לך שהמונח הזה מתייחס לפתרון סטנדרטי למשימת תכנות נפוצה. ב-CodeGym, אנחנו אומרים לעתים קרובות שאתה יכול לחפש בגוגל את התשובה כמעט לכל שאלה. הסיבה לכך היא שהמשימה שלך, מה שהיא תהיה, כנראה כבר נפתרה בהצלחה על ידי מישהו אחר. דפוסים הם פתרונות בדוקים למשימות הנפוצות ביותר, או שיטות לפתרון מצבים בעייתיים. אלו הם כמו "גלגלים" שלא צריך להמציא מחדש לבד, אבל כן צריך לדעת איך ומתי להשתמש בהם :) מטרה נוספת לדפוסים היא לקדם אדריכלות אחידה. קריאת קוד של מישהו אחר אינה משימה קלה! כל אחד כותב קוד שונה, כי ניתן לפתור את אותה משימה בדרכים רבות. אבל השימוש בתבניות עוזר למתכנתים שונים להבין את היגיון התכנות מבלי להתעמק בכל שורת קוד (גם כאשר רואים אותה בפעם הראשונה!) כיום אנו מסתכלים על אחת מתבניות העיצוב הנפוצות ביותר שנקראות "אסטרטגיה". דפוס עיצוב: אסטרטגיה - 2תארו לעצמכם שאנחנו כותבים תוכנית שתעבוד באופן פעיל עם אובייקטי שינוע. זה לא ממש משנה מה בדיוק התוכנית שלנו עושה. יצרנו היררכיית כיתות עם כיתת אב אחת של Conveyance ושלוש כיתות ילדים: סדאן , משאית ו- F1Car .
public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {
}

public class Truck extends Conveyance {
}

public class F1Car extends Conveyance {
}
כל שלוש כיתות הילד יורשות שתי שיטות סטנדרטיות מהאב: go() ו- stop() . התוכנית שלנו פשוטה מאוד: המכוניות שלנו יכולות רק להתקדם ולהפעיל את הבלמים. בהמשך עבודתנו, החלטנו לתת למכוניות שיטה חדשה: fill() (כלומר, "למלא את מיכל הדלק"). הוספנו אותו לכיתת האב של Conveyance :
public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }

   public void fill() {
       System.out.println("Refueling!");
   }
}
האם באמת יכולות להתעורר בעיות במצב כל כך פשוט? למעשה, כבר יש להם... דפוס עיצוב: אסטרטגיה - 3
public class Stroller extends Conveyance {

   public void fill() {

       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
לתוכנית שלנו יש כעת מנוע (עגלת תינוק) שלא משתלב יפה בקונספט הכללי. זה יכול להיות דוושות או להיות נשלט באמצעות רדיו, אבל דבר אחד בטוח - לא יהיה לו איפה לשפוך גז. היררכיית המחלקות שלנו גרמה לשיטות נפוצות לעבור בירושה למחלקות שאינן זקוקות להן. מה עלינו לעשות במצב זה? ובכן, נוכל לעקוף את שיטת fill() במחלקה Stroller כך ששום דבר לא יקרה כשאתה מנסה לתדלק את העגלה:
public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
אבל זה בקושי יכול להיקרא פתרון מוצלח אם אין סיבה אחרת מלבד קוד כפול. לדוגמה, רוב המחלקות ישתמשו בשיטה של ​​מחלקת האב, אך השאר ייאלצו לעקוף אותה. אם יש לנו 15 מחלקות ועלינו לעקוף את ההתנהגות ב-5-6 מהן, שכפול הקוד יהפוך לנרחב למדי. אולי ממשקים יכולים לעזור לנו? לדוגמה, כך:
public interface Fillable {

   public void fill();
}
אנו ניצור ממשק Fillable עם שיטת fill() אחת . לאחר מכן, אותם כלי שינוע שצריך לתדלק יממשו את הממשק הזה, בעוד כלי שינוע אחרים (למשל, עגלת התינוק שלנו) לא. אבל האופציה הזו לא מתאימה לנו. בעתיד, ההיררכיה המעמדית שלנו עשויה לגדול ולהיות גדולה מאוד (רק תארו לעצמכם כמה סוגים שונים של כלי שינוע יש בעולם). נטשנו את הגרסה הקודמת הכוללת ירושה, כי אנחנו לא רוצים לעקוף את שיטת fill() הרבה מאוד פעמים. עכשיו אנחנו צריכים ליישם את זה בכל שיעור! ומה אם יש לנו 50? ואם יבוצעו שינויים תכופים בתוכנית שלנו (וזה כמעט תמיד נכון לתוכניות אמיתיות!), נצטרך למהר לעבור את כל 50 השיעורים ולשנות באופן ידני את ההתנהגות של כל אחד מהם. אז מה, בסופו של דבר, עלינו לעשות במצב הזה? כדי לפתור את הבעיה שלנו, נבחר בדרך אחרת. כלומר, נפריד את התנהגות הכיתה שלנו מהכיתה עצמה. מה זה אומר? כידוע, לכל אובייקט יש מצב (סט של נתונים) והתנהגות (סט של שיטות). ההתנהגות של מחלקת ההעברה שלנו מורכבת משלוש שיטות: go() , stop() ו- fill() . שתי השיטות הראשונות טובות בדיוק כפי שהן. אבל נעביר את השיטה השלישית ממחלקת ההעברה . זה יפריד בין ההתנהגות למחלקה (ליתר דיוק, זה יפריד רק חלק מההתנהגות, מכיוון ששתי השיטות הראשונות יישארו במקומן). אז איפה עלינו לשים את שיטת fill() שלנו ? שום דבר לא עולה בראש :/ נראה שזה בדיוק איפה שהוא צריך להיות. נעביר אותו לממשק נפרד: FillStrategy !
public interface FillStrategy {

   public void fill();
}
למה אנחנו צריכים ממשק כזה? הכל פשוט. כעת אנו יכולים ליצור מספר מחלקות המיישמות את הממשק הזה:
public class HybridFillStrategy implements FillStrategy {

   @Override
   public void fill() {
       System.out.println("Refuel with gas or electricity — your choice!");
   }
}

public class F1PitstopStrategy implements FillStrategy {

   @Override
   public void fill() {
       System.out.println("Refuel with gas only after all other pit stop procedures are complete!");
   }
}

public class StandardFillStrategy implements FillStrategy {
   @Override
   public void fill() {
       System.out.println("Just refuel with gas!");
   }
}
יצרנו שלוש אסטרטגיות התנהגותיות: אחת עבור מכוניות רגילות, אחת עבור היברידיות ואחת עבור מכוניות מרוץ פורמולה 1. כל אסטרטגיה מיישמת אלגוריתם תדלוק שונה. במקרה שלנו, אנחנו פשוט מציגים מחרוזת בקונסולה, אבל כל שיטה יכולה להכיל לוגיקה מורכבת. מה נעשה עכשיו?
public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }

}
אנו משתמשים בממשק FillStrategy שלנו כשדות במחלקת האב של Conveyance . שימו לב שאנחנו לא מציינים יישום ספציפי - אנחנו משתמשים בממשק. כיתות המכוניות יזדקקו להטמעות ספציפיות של ממשק FillStrategy :
public class F1Car extends Conveyance {

   public F1Car() {
       this.fillStrategy = new F1PitstopStrategy();
   }
}

public class HybridCar extends Conveyance {

   public HybridCar() {
       this.fillStrategy = new HybridFillStrategy();
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       this.fillStrategy = new StandardFillStrategy();
   }
}
בואו נראה מה יש לנו!
public class Main {

   public static void main(String[] args) {

       Conveyance sedan = new Sedan();
       Conveyance hybrid = new HybridCar();
       Conveyance f1car = new F1Car();

       sedan.fill();
       hybrid.fill();
       f1car.fill();
   }
}
פלט מסוף:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
גדול! תהליך התדלוק עובד כמו שצריך! אגב, שום דבר לא מונע מאיתנו להשתמש באסטרטגיה כפרמטר בקונסטרוקטור! לדוגמה, כך:
public class Conveyance {

   private FillStrategy fillStrategy;

   public Conveyance(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }

   public void fill() {
       this.fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       super(new StandardFillStrategy());
   }
}



public class HybridCar extends Conveyance {

   public HybridCar() {
       super(new HybridFillStrategy());
   }
}

public class F1Car extends Conveyance {

   public F1Car() {
       super(new F1PitstopStrategy());
   }
}
בואו נריץ את שיטת ה-main() שלנו (שנשאר ללא שינוי). אנחנו מקבלים את אותה תוצאה! פלט מסוף:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
דפוס עיצוב האסטרטגיה מגדיר משפחה של אלגוריתמים, מקפל כל אחד מהם ומבטיח שהם ניתנים להחלפה. זה מאפשר לך לשנות את האלגוריתמים ללא קשר לאופן שבו הם משמשים את הלקוח (הגדרה זו, שנלקחה מהספר "דפוסי עיצוב ראש ראשון", נראית לי מצוינת). דפוס עיצוב: אסטרטגיה - 4כבר פירטנו את משפחת האלגוריתמים שאנו מעוניינים בהם (דרכים לתדלוק מכוניות) בממשקים נפרדים עם יישומים שונים. הפרדנו אותם מהמכונית עצמה. כעת, אם נצטרך לבצע שינויים כלשהם באלגוריתם תדלוק מסוים, זה לא ישפיע על מחלקות המכוניות שלנו בשום צורה. וכדי להשיג ניתנות להחלפה, אנחנו רק צריכים להוסיף שיטת מגדיר יחידה למחלקת ההעברה שלנו :
public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }

   public void setFillStrategy(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }
}
כעת אנו יכולים לשנות אסטרטגיות תוך כדי תנועה:
public class Main {

   public static void main(String[] args) {

       Stroller stroller= new Stroller();
       stroller.setFillStrategy(new StandardFillStrategy());

       stroller.fill();
   }
}
אם עגלות תינוק יתחילו פתאום לרוץ על בנזין, התוכנית שלנו תהיה מוכנה להתמודד עם התרחיש הזה :) וזהו בערך! למדת עוד דפוס עיצובי אחד שללא ספק יהיה חיוני ויעזור בעבודה על פרויקטים אמיתיים :) עד הפעם הבאה!
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION