תכנות מתקדם ב-Java/עבודה לפי ממשק

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

עקרון הכימוסעריכה

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

למה זה טוב?עריכה

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

הימנעות משגיאותעריכה

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

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

מודולריותעריכה

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

כיצד זה עובדעריכה

הרשאה ציבורית ופרטיתעריכה

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

getters/settersעריכה

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

// Get item's name
public String getName() {
	return name;
}

יתרונותעריכה

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


 

כדאי לדעת:

בחלק מסביבות העבודה ניתן ליצור את שיטות ה-get וה-set באופן אוטומטי.

החזרה של מערכים ואובייקטיםעריכה

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

public class Blackbox {

	private int someField;	
	
	public int getField() { 
		return someField; 
	}	
	
	public void setField(int val) { 
		someField = val; 
	}
}

המחלקה ClosedObject תיראה כך:

public class ClosedObject {
	
	private Blackbox box;
	
	public ClosedObject() {
		box = new Blackbox();
		box.setField(10);
	}
	
	public Blackbox getBox() {
		return box;
	}
	
	public void printBox() {
		System.out.println("What is in the box? "+box.getField());
	}
}

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

ClosedObject obj = new ClosedObject();
obj.printBox();
Blackbox myBlackBox = obj.getBox();
myBlackBox.setField(11);
obj.printBox();

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

public Blackbox getBox() {
	Blackbox temp = new Blackbox();
	temp.setField(box.getField());
	return temp;
}

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

עבודה על פי ממשקעריכה

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

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

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

לסיכוםעריכה

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

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


הפרק הקודם:
אובייקטים
עבודה לפי ממשק
תרגילים
הפרק הבא:
תיעוד