עקרון היפוך התלות בג'אווה

1. סקירה כללית

עקרון היפוך התלות (DIP) מהווה חלק מאוסף עקרונות התכנות מונחי האובייקטים הידועים בכינויו SOLID.

בעצמות החשופות, מח"ש הוא פרדיגמת תכנות פשוטה אך עוצמתית בה אנו יכולים להשתמש כדי להטמיע רכיבי תוכנה מובנים, מנותקים מאוד, וניתנים לשימוש חוזר.

במדריך זה, נחקור גישות שונות ליישום ה- DIP - אחת ב- Java 8 ואחת ב- Java 11 באמצעות JPMS (Java Platform Module System).

2. הזרקת תלות והיפוך שליטה אינן מימוש DIP

בראש ובראשונה, בואו נעשה הבחנה מהותית כדי להשיג את היסודות הנכונים: מח"ש אינו הזרקת תלות (DI) או היפוך שליטה (IoC). למרות זאת, כולם עובדים נהדר יחד.

במילים פשוטות, DI נועד ליצור רכיבי תוכנה שיצהירו במפורש על התלות שלהם או על משתפי פעולה באמצעות ה- API שלהם, במקום לרכוש אותם בעצמם.

ללא DI, רכיבי התוכנה מצמידים זה את זה היטב. לפיכך, הם קשים לשימוש חוזר, החלפה, לעג ולבדוק, מה שמביא לעיצובים נוקשים.

עם DI, האחריות לספק תלות רכיבים וגרפי אובייקט חיווט מועברת מהרכיבים למסגרת ההזרקה הבסיסית. מנקודת מבט זו, DI היא רק דרך להשיג IoC.

מצד שני, IoC הוא דפוס שבו השליטה בזרימת היישום הופכת. בעזרת מתודולוגיות תכנות מסורתיות, הקוד המותאם אישית שלנו שולט בזרימת היישום. לעומת זאת, עם IoC, השליטה מועברת למסגרת או מיכל חיצוניים.

המסגרת היא בסיס קוד הניתן להרחבה, המגדיר נקודות וו לחיבור קוד משלנו.

בתורו, המסגרת מחזירה את הקוד שלנו דרך תת-מחלקה מיוחדת אחת או יותר, באמצעות יישומי ממשקים ובאמצעות ביאורים. מסגרת האביב היא דוגמה נחמדה לגישה אחרונה זו.

3. יסודות מח"ש

כדי להבין את המוטיבציה העומדת מאחורי מח"ש, נתחיל בהגדרתו הרשמית, שניתנה על ידי רוברט סי. מרטין בספרו, פיתוח תוכנה זריז: עקרונות, דפוסים ושיטות עבודה:

  1. מודולים ברמה גבוהה לא צריכים להיות תלויים במודולים ברמה נמוכה. שניהם צריכים להיות תלויים בהפשטות.
  2. הפשטות לא צריכות להיות תלויות בפרטים. פרטים צריכים להיות תלויים בהפשטות.

אז ברור שבבסיס, מח"ש עוסק בהיפוך התלות הקלאסית בין רכיבים ברמה גבוהה ונמוכה על ידי הפשטת האינטראקציה ביניהם.

בפיתוח תוכנה מסורתי, רכיבים ברמה גבוהה תלויים ברמה נמוכה. לפיכך, קשה לעשות שימוש חוזר ברכיבים ברמה הגבוהה.

3.1. אפשרויות עיצוב ומח"ש

בואו ניקח בחשבון פשוט StringProcessor כיתה שמקבלת חוּט ערך באמצעות a StringReader רכיב, וכותב אותו במקום אחר באמצעות a StringWriter רְכִיב:

מחלקה ציבורית StringProcessor {private final StringReader stringReader; גמר פרטי StringWriter stringWriter; ציבור StringProcessor (StringReader stringReader, StringWriter stringWriter) {this.stringReader = stringReader; this.stringWriter = stringWriter; } printString () ריק ריק () {stringWriter.write (stringReader.getValue ()); }} 

למרות היישום של StringProcessor הכיתה היא בסיסית, יש כמה אפשרויות עיצוב שאנחנו יכולים לעשות כאן.

בואו נפרוס כל בחירה בעיצוב לפריטים נפרדים, כדי להבין בבירור כיצד כל אחת יכולה להשפיע על העיצוב הכללי:

  1. StringReader ו StringWriter, המרכיבים הנמוכים, הם מחלקות בטון המונחות באותה אריזה.StringProcessor, הרכיב ברמה הגבוהה ממוקם באריזה אחרת. StringProcessor תלוי ב StringReader ו StringWriter. אין היפוך של תלות, ומכאן StringProcessor אינו ניתן לשימוש חוזר בהקשר אחר.
  2. StringReader ו StringWriter הם ממשקים הממוקמים באותה חבילה יחד עם היישומים. StringProcessor עכשיו תלוי בהפשטות, אך הרכיבים ברמה הנמוכה לא. עדיין לא השגנו היפוך תלות.
  3. StringReader ו StringWriter הם ממשקים הממוקמים באותה חבילה יחד עם StringProcessor. עַכשָׁיו, StringProcessor יש בעלות מפורשת על ההפשטות. StringProcessor, StringReader, ו StringWriter הכל תלוי בהפשטות. השגנו היפוך תלות מלמעלה למטה על ידי הפשטת האינטראקציה בין הרכיבים.StringProcessor ניתן לשימוש חוזר בהקשר אחר.
  4. StringReader ו StringWriter הם ממשקים הממוקמים בחבילה נפרדת מ- StringProcessor. השגנו היפוך תלות, וזה גם קל יותר להחליף StringReader ו StringWriter יישומים. StringProcessor ניתן לשימוש חוזר גם בהקשר אחר.

מכל התרחישים הנ"ל, רק פריטים 3 ו -4 הם יישומים תקפים של מח"ש.

3.2. הגדרת הבעלות על ההפשטות

פריט 3 הוא יישום מח"ש ישיר, כאשר הרכיב ברמה הגבוהה וההפשטה ממוקמים באותה חבילה. לָכֵן, הרכיב ברמה הגבוהה הוא הבעלות על ההפשטות. ביישום זה, הרכיב ברמה הגבוהה אחראי על הגדרת הפרוטוקול המופשט דרכו הוא מתקשר עם הרכיבים הנמוכים.

כמו כן, פריט 4 הוא יישום DIP מנותק יותר. בגרסה זו של התבנית, לא לרכיב ברמה הגבוהה ולא לרמה הנמוכה יש את הבעלות על ההפשטות.

ההפשטות ממוקמות בשכבה נפרדת, מה שמקל על החלפת הרכיבים ברמה הנמוכה. יחד עם זאת, כל הרכיבים מבודדים זה מזה, מה שמניב אנקפסולציה חזקה יותר.

3.3. בחירת הרמה הנכונה של הפשטה

ברוב המקרים, בחירת ההפשטות בהן ישתמשו הרכיבים הגבוהים צריכה להיות פשוטה למדי, אך עם אזהרה אחת שכדאי לשים לב אליה: רמת ההפשטה.

בדוגמה שלמעלה השתמשנו ב- DI כדי להזריק a StringReader הקלד לתוך ה- StringProcessor מעמד. זה יהיה יעיל כל עוד רמת ההפשטה של StringReader הוא קרוב לתחום של StringProcessor.

לעומת זאת, היינו פשוט מפספסים את היתרונות הפנימיים של מח"ש אם StringReader הוא, למשל, א קוֹבֶץ אובייקט שקורא א חוּט ערך מקובץ. במקרה כזה, רמת ההפשטה של StringReader יהיה נמוך בהרבה מרמת התחום של StringProcessor.

במילים פשוטות, רמת ההפשטה שהרכיבים הגבוהים ישתמשו בהם בכדי לשתף פעולה עם הרמות הנמוכות צריכה להיות תמיד קרובה לתחום של הקודם.

4. יישומי Java 8

כבר הסתכלנו לעומק על מושגי המפתח של DIP, אז עכשיו נחקור כמה יישומים מעשיים של התבנית ב- Java 8.

4.1. יישום DIP ישיר

בואו ניצור אפליקציית הדגמה שתביא לקוחות מסוימים משכבת ​​ההתמדה ומעבדת אותם בדרך נוספת.

האחסון הבסיסי של השכבה הוא בדרך כלל מסד נתונים, אך כדי לשמור על פשוט את הקוד, כאן נשתמש במישור רגיל מַפָּה.

נתחיל ב הגדרת הרכיב ברמה הגבוהה:

CustomerService בכיתה ציבורית {CustomerDao customerDao הסופי הפרטי; // קונסטרוקטור / גטר סטנדרטי ציבורי findById (int id) {return customerDao.findById (id); } רשימה ציבורית findAll () {return customerDao.findAll (); }}

כפי שאנו רואים, שירות לקוחות הכיתה מיישמת את findById () ו מצא הכל() שיטות, שמביאות לקוחות משכבת ​​ההתמדה באמצעות יישום פשוט של DAO. כמובן, היינו יכולים לתמצת יותר פונקציונליות בכיתה, אבל בואו נשמור כך ככה לשם הפשטות.

במקרה הזה, ה CustomerDao הסוג הוא ההפשטה זֶה שירות לקוחות שימושים לצריכת הרכיב ברמה נמוכה.

מכיוון שמדובר ביישום DIP ישיר, בואו נגדיר את ההפשטה כממשק באותה חבילה של שירות לקוחות:

ממשק ציבורי CustomerDao {findById אופציונלי (int id); רשימה findAll (); } 

על ידי הצבת ההפשטה באותה חבילה של הרכיב ברמה הגבוהה, אנו הופכים את הרכיב לאחראי לבעלות ההפשטה. פרט יישום זה הוא מה שבאמת הופך את התלות בין הרכיב ברמה הגבוהה לרמה הנמוכה.

בנוסף, רמת ההפשטה של CustomerDao קרוב לזה של שירות לקוחות, אשר נדרש גם ליישום טוב של מח"ש.

עכשיו, בואו ניצור את הרכיב ברמה נמוכה בחבילה אחרת. במקרה זה, זה פשוט בסיסי CustomerDao יישום:

מחלקה ציבורית SimpleCustomerDao מיישם את CustomerDao {// קונסטרוקטור סטנדרטי / getter @Override ציבור אופציונלי findById (int id) {return Optional.ofNullable (customers.get (id)) } @ רשימת רשימות ציבורית @ findAll () {החזר ArrayList חדש (customers.values ​​()); }}

לבסוף, בואו ניצור מבחן יחידה לבדיקת ה- שירות לקוחות פונקציונליות בכיתה:

@ לפני בטל הציבור setUpCustomerServiceInstance () {var לקוחות = HashMap חדש (); customer.put (1, לקוח חדש ("ג'ון")); customer.put (2, לקוח חדש ("סוזן")); customerService = שירות לקוחות חדש (SimpleCustomerDao חדש (לקוחות)); } @Test הציבור בטל givenCustomerServiceInstance_whenCalledFindById_thenCorrect () {assertThat (customerService.findById (1)). IsInstanceOf (Optional.class); } @Test ציבורי בטל givenCustomerServiceInstance_whenCalledFindAll_thenCorrect () {assertThat (customerService.findAll ()). IsInstanceOf (List.class); } @Test הציבור בטל givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect () {var לקוחות = HashMap חדש (); customer.put (1, null); customerService = שירות לקוחות חדש (SimpleCustomerDao חדש (לקוחות)); לקוח לקוח = customerService.findById (1) .orElseGet (() -> לקוח חדש ("לקוח לא קיים")); assertThat (customer.getName ()). isEqualTo ("לקוח לא קיים"); }

מבחן היחידה מפעיל את שירות לקוחות ממשק API. וזה גם מראה כיצד להזריק את ההפשטה באופן ידני לרכיב ברמה הגבוהה. ברוב המקרים, אנו משתמשים במיכל או מסגרת DI כדי להשיג זאת.

בנוסף, התרשים הבא מראה את מבנה יישום ההדגמה שלנו, מנקודת מבט חבילה ברמה גבוהה:

4.2. יישום מח"ש חלופי

כפי שדנו קודם, ניתן להשתמש בהטמעה חלופית של DIP, בה אנו ממקמים את הרכיבים ברמה הגבוהה, את ההפשטות ואת הרמות הנמוכות בחבילות שונות.

מסיבות ברורות, גרסה זו גמישה יותר, מניבה אנקפסולציה טובה יותר של הרכיבים ומקלה על החלפת הרכיבים ברמה הנמוכה.

כמובן, יישום גרסה זו של הדפוס מסתכם רק בהצבה שירות לקוחות, MapCustomerDao, ו CustomerDao בחבילות נפרדות.

לכן, דיאגרמה מספיקה כדי להראות כיצד כל רכיב מתווה עם יישום זה:

5. יישום מודולרי של Java 11

קל למדי לשנות את יישום ההדגמה שלנו ליישום מודולרי.

זוהי דרך נחמדה מאוד להדגים כיצד JPMS אוכף את שיטות התכנות הטובות ביותר, כולל אנקפסולציה חזקה, הפשטה ושימוש חוזר ברכיבים באמצעות DIP.

איננו צריכים ליישם מחדש את רכיבי הדוגמה מאפס. לָכֵן, המודולריזציה של יישום הדוגמה שלנו היא רק עניין של הצבת כל קובץ רכיבים במודול נפרד, יחד עם מתאר המודולים המתאים.

כך ייראה מבנה הפרויקט המודולרי:

ספריית בסיס פרויקטים (יכולה להיות כל דבר, כמו dipmodular) | - com.baeldung.dip.services module-info.java | - com | - baeldung | - dip | - services CustomerService.java | - com.baeldung.dip.daos module -info.java | - com | - baeldung | - dip | - daos CustomerDao.java | - com.baeldung.dip.daoimplementations module-info.java | - com | - baeldung | - dip | - daoimplementations SimpleCustomerDao.java | - com.baeldung.dip.entities module-info.java | - com | - baeldung | - dip | - entities Customer.java | - com.baeldung.dip.mainapp module-info.java | - com | - baeldung | - dip | - mainapp MainApplication.java 

5.1. מודול הרכיבים ברמה גבוהה

נתחיל בהצבת ה- שירות לקוחות בכיתה במודול משלה.

ניצור מודול זה בספריית הבסיס com.baeldung.dip.services, והוסף את מתאר המודולים, module-info.java:

מודול com.baeldung.dip.services {דורש com.baeldung.dip.entities; דורש com.baeldung.dip.daos; משתמש ב- com.baeldung.dip.daos.CustomerDao; יצוא com.baeldung.dip.services; }

מסיבות ברורות, לא נפרט על אופן הפעולה של JPMS. עם זאת, ברור לראות את התלות במודול רק על ידי הסתכלות על דורש הוראות.

הפרט הכי רלוונטי שכדאי לציין כאן הוא שימושים הוֹרָאָה. זה קובע את זה המודול הוא מודול לקוח שצורכת יישום של CustomerDao מִמְשָׁק.

כמובן, אנחנו עדיין צריכים להציב את הרכיב ברמה הגבוהה, ה- שירות לקוחות בכיתה, במודול זה. אז, בתוך ספריית הבסיס com.baeldung.dip.servicesבואו ניצור את מבנה הספריות הבא דמוי חבילה: com / baeldung / dip / services.

לבסוף, בוא נניח את CustomerService.java קובץ בספרייה זו.

5.2. מודול ההפשטה

כמו כן, עלינו למקם את CustomerDao ממשק במודול משלו. לכן, בואו ניצור את המודול בספריית השורש com.baeldung.dip.daosוהוסף את מתאר המודול:

מודול com.baeldung.dip.daos {דורש com.baeldung.dip.entities; יצוא com.baeldung.dip.daos; }

עכשיו, בוא ננווט אל ה- com.baeldung.dip.daos ספריה וצור את מבנה הספריות הבא: com / baeldung / dip / daos. בואו נציב את CustomerDao.java קובץ בספרייה זו.

5.3. מודול הרכיבים ברמה נמוכה

מבחינה הגיונית, עלינו לשים את הרכיב ברמה נמוכה, SimpleCustomerDao, גם במודול נפרד. כצפוי, התהליך נראה דומה מאוד למה שעשינו בדיוק עם המודולים האחרים.

בואו ניצור את המודול החדש בספריית הבסיס com.baeldung.dip.daoimplementations, וכולל את מתאר המודולים:

מודול com.baeldung.dip.daoimplementations {דורש com.baeldung.dip.entities; דורש com.baeldung.dip.daos; מספק com.baeldung.dip.daos.CustomerDao עם com.baeldung.dip.daoimplementations.SimpleCustomerDao; ייצוא com.baeldung.dip.daoimplementations; }

בהקשר JPMS, זהו מודול נותני שירותים, מכיוון שהוא מכריז על מספק ו עם הוראות.

במקרה זה, המודול עושה את CustomerDao שירות הזמין למודול צרכני אחד או יותר, דרך SimpleCustomerDao יישום.

בואו נזכור שמודול הצרכנות שלנו, com.baeldung.dip.services, צורכת שירות זה דרך שימושים הוֹרָאָה.

זה מראה בבירור כמה פשוט לבצע יישום DIP ישיר עם ה- JPMS על ידי הגדרת צרכנים, נותני שירותים והפשטות במודולים שונים.

כמו כן, עלינו למקם את SimpleCustomerDao.java קובץ במודול חדש זה. בוא ננווט אל ה- com.baeldung.dip.daoimplementations ספריה, וצור מבנה ספריות חדש דמוי חבילה בשם זה: com / baeldung / dip / daoimplementations.

לבסוף, בוא נניח את SimpleCustomerDao.java קובץ בספריה.

5.4. מודול הישות

בנוסף, עלינו ליצור מודול נוסף בו נוכל למקם את ה- לקוח.java מעמד. כמו שעשינו בעבר, בואו ניצור את ספריית הבסיס com.baeldung.dip.entities וכולל את מתאר המודולים:

מודול com.baeldung.dip.entities {יצוא com.baeldung.dip.entities; }

בספריית הבסיס של החבילה, בואו ניצור את הספריה com / baeldung / dip / entities והוסף את הדברים הבאים לקוח.java קוֹבֶץ:

לקוח בכיתה ציבורית {פרטי סופי שם מחרוזת; // קונסטרוקטור סטנדרטי / גטר / toString}

5.5. מודול היישום הראשי

לאחר מכן עלינו ליצור מודול נוסף המאפשר לנו להגדיר את נקודת הכניסה של יישום ההדגמה שלנו. לכן, בואו ליצור ספריית שורש נוספת com.baeldung.dip.mainapp והניחו בו את מתאר המודולים:

מודול com.baeldung.dip.mainapp {דורש com.baeldung.dip.entities; דורש com.baeldung.dip.daos; דורש יישומי com.baeldung.dip.dao; דורש com.baeldung.dip.services; יצוא com.baeldung.dip.mainapp; }

כעת, בואו ננווט לספריית הבסיס של המודול, וניצור את מבנה הספריות הבא: com / baeldung / dip / mainapp. בספרייה זו, בואו להוסיף a MainApplication.java קובץ, אשר פשוט מיישם א רָאשִׁי() שיטה:

מחלקה ציבורית MainApplication {public static void main (String args []) {var clients = HashMap new (); customer.put (1, לקוח חדש ("ג'ון")); customer.put (2, לקוח חדש ("סוזן")); CustomerService customerService = CustomerService חדש (SimpleCustomerDao חדש (לקוחות)); customerService.findAll (). forEach (System.out :: println); }}

לסיום, בואו נרכיב ולהפעיל את יישום ההדגמה - מתוך IDE שלנו או מקונסולת פקודה.

כצפוי, עלינו לראות רשימה של צרכן אובייקטים שהודפסו למסוף בעת הפעלת היישום:

לקוח {name = John} לקוח {name = Susan} 

בנוסף, התרשים הבא מציג את התלות של כל מודול ביישום:

6. מסקנה

במדריך זה, צללנו עמוק למושגי המפתח של DIP, והראינו גם מימושים שונים של התבנית ב- Java 8 ו- Java 11, כאשר האחרון משתמש ב- JPMS.

כל הדוגמאות ליישום DIP של Java 8 ולהטמעת Java 11 זמינות ב- GitHub.