תכנון שדות ושיטות

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

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

תכנון שדות

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

כפי שמשתמשים כאן, תכונה היא מאפיין מובהק של אובייקט או מחלקה. שתי תכונות של CoffeeCupאובייקט, למשל, יכולות להיות:

  • כמות הקפה שהכוס מכילה
  • בין אם הכוס נקייה או מלוכלכת

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

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

// בחבילת המקור בשדות קבצים / ex1 / מחלקה CoffeeCup.java CoffeeCup {private int innerCoffee; ציבורי בוליאני isReadyForNextUse () {// אם כוס קפה לא נשטפת, אז היא // לא מוכנה לשימוש הבא אם (innerCoffee == 500) {return false; } להחזיר נכון; } בטל פומבי setCustomerDone () {innerCoffee = 500; // ...} שטיפה בטלנית ציבורית () {innerCoffee = 0; // ...} // ...}

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

לדוגמא, אם מאוחר יותר מישהו יוסיף כוס 20 גרם למנחות הקפה הווירטואלי, יהיה אפשר להחזיק עד 592 מיליליטר (מ"ל) קפה בכוס. אם מתכנת מוסיף את גודל הכוס החדש מבלי להבין שאתה משתמש ב- 500 מ"ל כדי לציין כי כוס זקוקה לכביסה, סביר להניח שתוכנס באג. אם לקוח בבית הקפה הווירטואלי שלך קנה כוס של 20 אונקיות ואז היה לוקח לגימה גדולה של 92 מ"ל, הוא או היא יישארו בדיוק 500 מ"ל בכוס. הלקוח היה המום ולא מרוצה כאשר, לאחר שתיית 92 מ"ל בלבד, הכוס נעלמה מידו והופיעה בכיור, מוכנה לשטיפה. וגם אם המתכנת שעושה את השינוי הבין שאתה משתמש בערך מיוחד, יהיה צורך לבחור ערך מיוחד אחר עבור התכונה הלא רחוצה.

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

// בחבילת המקור בשדות קבצים / ex2 / מחלקה CoffeeCup.java CoffeeCup {private int innerCoffee; צרכים בוליאניים פרטיים כביסה; ציבורי בוליאני isReadyForNextUse () {// אם כוס קפה לא נשטפת, אז היא // לא מוכנה להחזרת השימוש הבא! needsWashing; } בטל פומבי setCustomerDone () {needsWashing = true; // ...} שטיפה ציבורית בטלה () {needsWashing = false; // ...} // ...}

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

שימוש בקבועים

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

אם (cup.getSize () == CoffeeCup.TALL) {} 

מאשר להבין זאת:

אם (cup.getSize () == 1) {} 

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

קבועים ומהדר Java

דבר שימושי שיש לדעת על מהדר Java הוא שהוא מתייחס לשדות סופיים סטטיים (קבועים) באופן שונה משאר סוגי השדות. הפניות למשתנים סופיים סטטיים שאותחלו לקבוע זמן הידור נפתרות בזמן הידור לעותק מקומי של הערך הקבוע. זה נכון לגבי קבועים מכל הסוגים הפרימיטיביים והסוגים java.lang.String.

בדרך כלל, כאשר הכיתה שלך מתייחסת למחלקה אחרת - נניח, מחלקה java.lang.Math- מהדר Java ממקם הפניות סמליות Mathלמחלקה בקובץ הכיתה עבור הכיתה שלך. לדוגמא, אם שיטה מהמחלקה שלך קוראת Math.sin(), קובץ הכיתה שלך יכיל שני אזכורים סמליים ל Math:

  • התייחסות סמלית אחת למעמד Math
  • אחת התייחסות סמלית Mathשל" sin()שיטה

כדי לבצע את הקוד הכלול בכיתה שלך שמתייחס אליו Math.sin(), ה- JVM יצטרך לטעון את הכיתה Mathכדי לפתור את ההפניות הסמליות.

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

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

שלושה סוגים של שיטות

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

שיטת השירות

שיטת שירות היא שיטת מחלקה שאינה משתמשת או משנה את המצב (משתני מחלקות) של המחלקה שלה. סוג זה של שיטה פשוט מספק שירות שימושי הקשור למחלקת האובייקט שלו.

כמה דוגמאות לשיטות שימוש מממשק ה- API של Java הן:

  • (בכיתה Integer) public static int toString(int i)- מחזיר Stringאובייקט חדש המייצג את המספר השלם שצוין ברדיקס 10
  • (בכיתה Math) public static native double cos(double a)- מחזיר את הקוסינוס הטריגונומטרי של זווית

שיטת השקפת המדינה

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

כמה דוגמאות לשיטות תצוגת מדינה מממשק ה- API של Java הן:

  • (בכיתה Object) public String toString()- מחזיר ייצוג מחרוזת של האובייקט
  • (בכיתה Integer) public byte byteValue()- מחזיר את ערך Integerהאובייקט כבת
  • (בכיתה String) public int indexOf(int ch)- מחזיר את האינדקס בתוך המחרוזת של המופע הראשון של התו שצוין

שיטת שינוי המדינה

The state-change method is a method that may transform the state of the class in which the method is declared, or, if an instance method, the object upon which it is invoked. When a state-change method is invoked, it represents an "event" to a class or object. The code of the method "handles" the event, potentially changing the state of the class or object.

Some examples of state-change methods from the Java API are:

  • (In class StringBuffer) public StringBuffer append(int i) -- appends the string representation of the int argument to the StringBuffer
  • (In class Hashtable) public synchronized void clear() -- clears the Hashtable so that it contains no keys
  • (In class Vector) public final synchronized void addElement(Object obj) -- adds the specified component to the end of the Vector, increasing its size by one

Minimizing method coupling

Armed with these definitions of utility, state-view, and state-change methods, you are ready for the discussion of method coupling.

As you design methods, one of your goals should be to minimize coupling -- the degree of interdependence between a method and its environment (other methods, objects, and classes). The less coupling there is between a method and its environment, the more independent that method is, and the more flexible the design is.

Methods as data transformers

To understand coupling, it helps to think of methods purely as transformers of data. Methods accept data as input, perform operations on that data, and generate data as output. A method's degree of coupling is determined primarily by where it gets its input data and where it puts its output data.

Figure 1 shows a graphical depiction of the method as data transformer: A data flow diagram from structured (not object-oriented) design.

Input and output

A method in Java can get input data from many sources:

  • It can require that the caller specify its input data as parameters when it is invoked
  • It can grab data from any accessible class variables, such as the class's own class variables or any accessible class variables of another class
  • If it is an instance method, it can grab instance variables from the object upon which it was invoked

Likewise, a method can express its output in many places:

  • It can return a value, either a primitive type or an object reference
  • It can alter objects referred to by references passed in as parameters
  • It can alter any class variables of its own class or any accessible class variables of another class
  • If it is an instance method, it can alter any instance variables of the object upon which it was invoked
  • It can throw an exception

Note that parameters, return values, and thrown exceptions are not the only kinds of method inputs and outputs mentioned in the above lists. Instance and class variables also are treated as input and output. This may seem non-intuitive from an object-oriented perspective, because access to instance and class variables in Java is "automatic" (you don't have to pass anything explicitly to the method). When attempting to gauge a method's coupling, however, you must look at the kind and amount of data used and modified by the code, regardless of whether or not the code's access to that data was "automatic."

Minimally coupled utility methods

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

  1. לוקח קלט רק מהפרמטרים שלו
  2. מבטא את תפוקתו רק באמצעות הפרמטרים שלו או ערך ההחזר שלו (או על ידי הטלת חריג)
  3. מקבל כקלט רק נתונים שנדרשים בפועל לפי השיטה
  4. מחזירה כפלט רק נתונים שמופקים בפועל על ידי השיטה

שיטת שירות טובה

לדוגמה, השיטה convertOzToMl()המוצגת להלן מקבלת intאת הקלט היחיד שלה ומחזירה intאת הפלט היחיד שלה: