CodeGym /בלוג Java /Random-HE /מוצק: חמישה עקרונות בסיסיים של עיצוב כיתות ב-Java
John Squirrels
רָמָה
San Francisco

מוצק: חמישה עקרונות בסיסיים של עיצוב כיתות ב-Java

פורסם בקבוצה
שיעורים הם אבני הבניין של יישומים. ממש כמו לבנים בבניין. שיעורים כתובים בצורה גרועה עלולים לגרום בסופו של דבר לבעיות. מוצק: חמישה עקרונות בסיסיים של עיצוב כיתות ב-Java - 1כדי להבין האם מחלקה כתובה כהלכה, ניתן לבדוק כיצד היא עומדת ב"תקני איכות". בג'אווה, אלו הם מה שנקרא עקרונות SOLID, ואנחנו הולכים לדבר עליהם.

עקרונות מוצקים בג'אווה

SOLID הוא ראשי תיבות שנוצרו מהאותיות הגדולות של חמשת העקרונות הראשונים של OOP ועיצוב כיתה. העקרונות באו לידי ביטוי על ידי רוברט מרטין בתחילת שנות ה-2000, ואז הקיצור הוצג מאוחר יותר על ידי מייקל Feathers. להלן העקרונות המוצקים:
  1. עיקרון אחריות יחידה
  2. עיקרון סגור פתוח
  3. עקרון ההחלפה של ליסקוב
  4. עקרון הפרדת ממשק
  5. עקרון היפוך תלות

עיקרון אחריות יחידה (SRP)

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

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
מודול זה עשוי להשתנות משלוש סיבות. ראשית, ההיגיון לעיבוד הזמנות עשוי להשתנות. שנית, אופן שמירת ההזמנות (סוג מסד הנתונים) עשוי להשתנות. שלישית, אופן שליחת האישור עשוי להשתנות (לדוגמה, נניח שעלינו לשלוח הודעת טקסט ולא אימייל). עקרון האחריות היחידה מרמז ששלושת ההיבטים של בעיה זו הם למעשה שלוש אחריות שונות. זה אומר שהם צריכים להיות במחלקות או מודולים שונים. שילוב של מספר גופים שיכולים להשתנות בזמנים שונים ומסיבות שונות נחשב להחלטה עיצובית לקויה. הרבה יותר טוב לפצל מודול לשלושה מודולים נפרדים, שכל אחד מהם מבצע פונקציה אחת:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

עיקרון סגור פתוח (OCP)

עיקרון זה מתואר באופן הבא: ישויות תוכנה (מחלקות, מודולים, פונקציות וכו') צריכות להיות פתוחות להרחבה, אך סגורות לשינוי . המשמעות היא שאמור להיות אפשרי לשנות התנהגות חיצונית של מחלקה מבלי לבצע שינויים בקוד הקיים של המחלקה. על פי עיקרון זה, מחלקות מתוכננות כך שהתאמה של מחלקה כך שתתאים לתנאים ספציפיים פשוט דורשת הרחבתה ועקוף פונקציות מסוימות. המשמעות היא שהמערכת חייבת להיות גמישה, מסוגלת לעבוד בתנאים משתנים מבלי לשנות את קוד המקור. המשך הדוגמה שלנו הכוללת עיבוד הזמנות, נניח שעלינו לבצע כמה פעולות לפני עיבוד הזמנה וכן לאחר שליחת דוא"ל האישור. במקום לשנות את OrderProcessorהמחלקה עצמה, נרחיב אותה כדי להשיג את המטרה שלנו מבלי להפר את העיקרון הפתוח הסגור:
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }

    private void beforeProcessing() {
        // Take some action before processing the order
    }

    private void afterProcessing() {
        // Take some action after processing the order
    }
}

עיקרון החלפת ליסקוב (LSP)

זוהי וריאציה של עקרון סגור פתוח שהזכרנו קודם לכן. ניתן להגדיר זאת באופן הבא: ניתן להחליף אובייקטים באובייקטים של תת-מחלקות מבלי לשנות את המאפיינים של תוכנית. המשמעות היא שמחלקה שנוצרה על ידי הרחבת מחלקה בסיס חייבת לעקוף את השיטות שלה כך שהפונקציונליות לא תיפגע מנקודת מבטו של הלקוח. כלומר, אם מפתח מרחיב את המחלקה שלך ומשתמש בה באפליקציה, הוא או היא לא צריכים לשנות את ההתנהגות הצפויה של מתודות כלשהן. תת-מחלקות חייבות לעקוף את השיטות של מחלקת הבסיס כדי שהפונקציונליות לא תישבר מנקודת המבט של הלקוח. אנו יכולים לחקור זאת בפירוט בדוגמה הבאה. נניח שיש לנו מחלקה שאחראית לאמת הזמנה ולבדוק האם כל הסחורה בהזמנה במלאי. למחלקה הזו יש isValid()שיטה שמחזירה true או false :
public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
נניח גם שהזמנות מסוימות צריכות להיות מאומתות בצורה שונה משל אחרות, למשל עבור הזמנות מסוימות עלינו לבדוק אם כל הסחורה בהזמנה נמצאת במלאי והאם כל הסחורה ארוזה. לשם כך, אנו מרחיבים את OrderStockValidatorהמחלקה על ידי יצירת OrderStockAndPackValidatorהמחלקה:
public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
אבל כאן הפרנו את עקרון ההחלפה של Liskov, כי במקום להחזיר false אם ההזמנה נכשלת באימות, השיטה שלנו זורקת IllegalStateException. לקוחות המשתמשים בקוד זה אינם מצפים לכך: הם מצפים לערך החזרה של true או false . זה יכול להוביל לשגיאות זמן ריצה.

עיקרון הפרדת ממשק (ISP)

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

עקרון היפוך תלות (DIP)

ב-Java, עיקרון SOLID זה מתואר באופן הבא: תלות בתוך המערכת נבנות על סמך הפשטות . מודולים ברמה גבוהה יותר אינם תלויים במודולים ברמה נמוכה יותר. הפשטות לא אמורות להיות תלויות בפרטים. הפרטים צריכים להיות תלויים בהפשטות. תוכנה צריכה להיות מתוכננת כך שהמודולים השונים יהיו עצמאיים ומחוברים זה לזה באמצעות הפשטה. יישום קלאסי של עיקרון זה הוא מסגרת האביב. ב-Spring Framework, כל המודולים מיושמים כרכיבים נפרדים שיכולים לעבוד יחד. הם כל כך אוטונומיים שניתן להשתמש בהם באותה קלות במודולי תוכנית שאינם מסגרת האביב. זה מושג הודות לתלות של העקרונות הסגורים והפתוחים. כל המודולים מספקים גישה רק להפשטה, שבה ניתן להשתמש במודול אחר. בואו ננסה להמחיש זאת באמצעות דוגמה. אם כבר מדברים על עיקרון האחריות היחידה, שקלנו את OrderProcessorהמעמד. בואו נסתכל שוב על הקוד של המחלקה הזו:
public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
בדוגמה זו, OrderProcessorהמחלקה שלנו תלויה בשתי מחלקות ספציפיות: MySQLOrderRepositoryו ConfirmationEmailSender. נציג גם את הקוד של השיעורים האלה:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
השיעורים הללו רחוקים ממה שהיינו מכנים הפשטות. ומנקודת המבט של עקרון היפוך התלות, עדיף להתחיל ביצירת כמה הפשטות שנוכל לעבוד איתן בעתיד, ולא יישומים ספציפיים. בואו ניצור שני ממשקים: MailSenderו OrderRepository). אלו יהיו ההפשטות שלנו:
public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
כעת אנו מיישמים את הממשקים הללו במחלקות שכבר הוכנו לכך:
public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
עשינו את עבודת ההכנה כך שהכיתה שלנו OrderProcessorתלויה, לא בפרטים קונקרטיים, אלא בהפשטות. נשנה את זה על ידי הוספת התלות שלנו לבנאי המחלקה:
public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
כעת הכיתה שלנו תלויה בהפשטות, לא בהטמעות ספציפיות. אנו יכולים לשנות בקלות את התנהגותו על ידי הוספת התלות הרצויה בזמן OrderProcessorיצירת אובייקט. בחנו את עקרונות העיצוב SOLID ב-Java. תלמד עוד על OOP בכלל ועל יסודות תכנות Java - שום דבר משעמם ומאות שעות של תרגול - בקורס CodeGym. הגיע הזמן לפתור כמה משימות :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION