דפוסי עיצוב יצירתי ב- Core Java

1. הקדמה

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

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

במאמר זה נבחן מחדש כמה דפוסי עיצוב יצירתי נפוצים. נראה איך הם נראים ואיפה ניתן למצוא אותם ב- JVM או בספריות הליבה האחרות.

2. שיטת המפעל

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

class SomeImplementation מיישם SomeInterface {// ...} 
מחלקה ציבורית SomeInterfaceFactory {public SomeInterface newInstance () {להחזיר SomeImplementation חדש (); }}

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

2.1. דוגמאות ב- JVM

יתכן שהדוגמאות הידועות ביותר לדפוס זה ה- JVM הן שיטות בניית האוסף ב- אוספים כיתה, כמו קְלָף בּוֹדֵד(), singletonList (), ו singletonMap (). כל אלה מקרים החוזרים של האוסף המתאים - מַעֲרֶכֶת, רשימה, או מפה - אבל הסוג המדויק אינו רלוונטי. בנוסף, ה- זרם של() השיטה והחדש Set.of (), רשימה של(), ו Map.ofEntries () השיטות מאפשרות לנו לעשות את אותו הדבר עם אוספים גדולים יותר.

יש הרבה דוגמאות נוספות לכך, כולל Charset.forName (), שיחזיר מופע אחר של ה- ערכת בכיתה בהתאם לשם המבוקש, וכן ResourceBundle.getBundle (), אשר יטען חבילת משאבים אחרת בהתאם לשם המסופק.

גם לא כל אלה צריכים לספק מקרים שונים. חלקן פשוט מופשטות להסתרת פעולות פנימיות. לדוגמה, Calendar.getInstance () ו NumberFormat.getInstance () תמיד תחזיר את אותה מופע, אך הפרטים המדויקים אינם רלוונטיים לקוד הלקוח.

3. מפעל מופשט

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

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

ממשק FileSystem {// ...} 
מחלקה LocalFileSystem מיישמת את FileSystem {// ...} 
class NetworkFileSystem מיישם את FileSystem {// ...} 

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

ממשק FileSystemFactory {FileSystem newInstance (); } 
מחלקה LocalFileSystemFactory מיישמת את FileSystemFactory {// ...} 
מחלקה NetworkFileSystemFactory מיישמת את FileSystemFactory {// ...} 

יש לנו שיטת מפעל אחרת להשיג את המפעל המופשט דרכו נוכל להשיג את המופע בפועל:

דוגמה למחלקה {static FileSystemFactory getFactory (מחרוזת fs) {FileSystemFactory מפעל; if ("local" .equals (fs)) {factory = new LocalFileSystemFactory (); אחרת אם ("רשת". שווה (fs)) {מפעל = NetworkFileSystemFactory חדש (); } להחזיר מפעל; }}

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

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

3.1. דוגמאות ב- JVM

ישנן דוגמאות רבות לדפוס עיצוב זה המשמש בכל רחבי ה- JVM. הנפוצים ביותר הם סביב חבילות ה- XML ​​- למשל, DocumentBuilderFactory, TransformerFactory, ו XPathFactory. לכל אלה יש מיוחד newInstance () שיטת המפעל כדי לאפשר לקוד שלנו להשיג מופע של המפעל המופשט.

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

לאחר שהקוד שלנו התקשר ל- newInstance () בשיטה, יהיה לו מופע של המפעל מספריית ה- XML ​​המתאימה. מפעל זה בונה את הכיתות בפועל בהן אנו רוצים להשתמש מאותה ספרייה.

לדוגמה, אם אנו משתמשים ביישום ה- Xerces המוגדר כברירת מחדל של JVM, נקבל מופע של com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImplאבל אם היינו רוצים להשתמש במקום אחר, ואז להתקשר newInstance () היה מחזיר זאת בשקיפות במקום.

4. בונה

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

class CarBuilder {make String private = "Ford"; דגם מחרוזת פרטי = "פיאסטה"; דלתות כניסה פרטיות = 4; צבע מחרוזת פרטי = "לבן"; בניית רכב ציבורי () {להחזיר רכב חדש (יצרן, דגם, דלתות, צבע); }}

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

4.1. דוגמאות ב- JVM

ישנן דוגמאות מרכזיות מאוד לדפוס זה בתוך ה- JVM. ה StringBuilder ו StringBuffer שיעורים הם בונים המאפשרים לנו לבנות זמן רב חוּט על ידי אספקת חלקים קטנים רבים. כמה שיותר לאחרונה Stream.Builder הכיתה מאפשרת לנו לעשות בדיוק את אותו הדבר בכדי לבנות a זרם:

בונה Stream.Builder = Stream.builder (); builder.add (1); builder.add (2); אם (תנאי) {builder.add (3); builder.add (4); } builder.add (5); זרם זרם = builder.build ();

5. אתחול עצלן

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

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

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

מחלקה LazyPi {מחשבון ספק פרטי; ערך כפול פרטי; ציבורי מסונכרן כפול getValue () {if (value == null) {value = calculator.get (); } ערך החזרה; }}

מחשוב pi הוא פעולה יקרה ואולי לא נצטרך לבצע אותה. האמור לעיל יעשה זאת בפעם הראשונה בה אנו מתקשרים getValue () ולא לפני כן.

5.1. דוגמאות ב- JVM

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

למרות זאת, הדור בפועל של הנחל עצמו יכול להיות עצלן גם כן. Stream.generate () לוקח פונקציה להתקשר בכל פעם שיש צורך בערך הבא ונקרא רק אי פעם כשצריך. אנו יכולים להשתמש בזה כדי לטעון ערכים יקרים - למשל על ידי ביצוע שיחות API של HTTP - ואנחנו משלמים את העלות רק בכל פעם שיש צורך באלמנט חדש:

Stream.generate (BaeldungArticlesLoader חדש ()) .filter (מאמר -> article.getTags (). מכיל ("java-streams")) .מפה (מאמר -> article.getTitle ()) .findFirst ();

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

6. בריכת חפצים

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

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

6.1. דוגמאות ב- JVM

הדוגמה העיקרית לתבנית זו ב- JVM היא השימוש בבריכות חוטים. An שירות ExecutorService ינהל מערך שרשורים ויאפשר לנו להשתמש בהם כאשר משימה צריכה לבצע אחת. השימוש בזה פירושו שאנחנו לא צריכים ליצור שרשורים חדשים, עם כל העלויות הכרוכות בכך, בכל פעם שאנחנו צריכים להוליד משימה אסינכרונית:

מאגר ExecutorService = Executors.newFixedThreadPool (10); pool.execute (חדש SomeTask ()); // פועל על שרשור מבריכת הבריכה. Execute (AnotherTask חדש () חדש); // פועל על חוט מהבריכה

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

7. אב טיפוס

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

ל- Java יש רמת תמיכה לכך על ידי יישום ה- ניתנת לשיבוט ממשק סמן ואז שימוש Object.clone (). זה ייצור שיבוט רדוד של האובייקט, יצירת מופע חדש והעתקת השדות ישירות.

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

יישום אב-טיפוס ברמה הציבורית ניתן לשכפל {תוכן מפה פרטי = HashMap חדש (); public void setValue (String key, String value) {// ...} public String getValue (String key) {// ...} @ Override public prototype clone () {Prototype result = new Prototype (); this.contents.entrySet (). forEach (entry -> result.setValue (entry.getKey (), entry.getValue ())); תוצאת החזרה; }}

7.1. דוגמאות ב- JVM

ל- JVM יש כמה דוגמאות לכך. אנו יכולים לראות את אלה על ידי ביצוע השיעורים המיישמים את ה- ניתנת לשיבוט מִמְשָׁק. לדוגמה, PKIXCertPathBuilderResult, PKIXBuilderParameters, PKIX פרמטרים, PKIXCertPathBuilderResult, ו PKIXCertPathValidatorResult כולם ניתנת לשיבוט.

דוגמא נוספת היא ה java.util.Date מעמד. יש לציין, זה עוקף את לְהִתְנַגֵד.שיבוט () שיטה להעתקה גם על פני שדה חולף נוסף.

8. סינגלטון

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

מעמד ציבורי סינגלטון {מופע סינגלטון פרטי סטטי = null; סינגלטון סטטי ציבורי getInstance () {if (מופע == null) {מופע = סינגלטון חדש (); } חזרה מופע; }}

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

8.1. דוגמאות ב- JVM

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

בנוסף, הרבה ממשק ה- API של השתקפות Java עובד עם מקרים בודדים. אותה מחלקה בפועל תמיד מחזירה את אותו מופע של מעמד, לא משנה אם ניתן לגשת אליו באמצעות Class.forName (), מחרוזת.מעמד, או באמצעות שיטות השתקפות אחרות.

באופן דומה, אנו עשויים לשקול את פְּתִיל מופע המייצג את השרשור הנוכחי להיות יחיד. לרוב יהיו מקרים רבים לכך, אך בהגדרה, יש מופע אחד לכל שרשור. יִעוּד Thread.currentThread () מכל מקום ביצוע באותו אשכול תמיד יחזיר את אותו מופע.

9. סיכום

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


$config[zx-auto] not found$config[zx-overlay] not found