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

בחזרה למכולת עריכה

נחזור לתוכנית המכולת, עם כמה שינויים קלים. הממשק שלנו יראה כך:

// Adds an item with the given details to the stock. Returns true if that item added, false if not
boolean addItem(String itemName, String description, double price);
 // Search the stock for a given item. Returns true only if item with the same name is in stock
boolean contains(String itemName);
// Remove an item from the stock. Returns true if the item removed, false if not
boolean removeItem(String itemName);
// Print all items in the stock
void printStock();
// Calculates the value of the whole stock
double sumStock();

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

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

שלב ראשון - בחירת מבנה הנתונים עריכה

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

פתרון
// Stock.java

public class Stock {
	
	private static final int STOCK_SIZE = 10; 
	private Item _items[];
	
	public Stock() {
		_items = new Item[STOCK_SIZE];
	}
	
}


שלב שני - מימוש החוזה עריכה

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

שיטות עזר עריכה

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

// Search for a given item name and returns its location in array. Returns -1 if not found
int findItem(String itemName);

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

// Search for the first empty array cell. Returns -1 if all cells are full
int findEmptyCell();

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

פתרון
private int findItem(String itemName) {
	for(int i=0; i<STOCK_SIZE; i++) {
		if(_items[i] != null) {
			if(_items[i].getName().equals(itemName))
				return i;
		}
	}
	return -1;
}

private int findEmptyCell() {
	for(int i=0; i<STOCK_SIZE; i++) {
		if(_items[i] == null) 
			return i;
	}
	return -1;
}


חיפוש עריכה

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

פתרון
public boolean contains(String item) {
	return (findItem(item) != -1);
}


הוספה והסרה עריכה

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

פתרון
boolean addItem(String itemName, String description, double price) {
	int index = findEmptyCell();
	if(index == -1) return false;
	_items[index] = new Item(itemName, description, price);
	return true;
	
}
boolean removeItem(String itemName) {
	int index = findItem(itemName);
	if(index == -1) return false;
	_items[index] = null;
	return true;
}


שיטות אחרונות עריכה

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

פתרון
void printStock() {
	for(int i=0; i<STOCK_SIZE; i++) {
		if(_items[i] != null) 
			_items[i].printItem();
	}
}

double sumStock() {
	double sum = 0;
	for(int i=0; i<STOCK_SIZE; i++) {
		if(_items[i] != null) 
			sum+=_items[i].getPrice();
	}	
	return sum;
}


שלב אחרון - בדיקות עריכה

אם כתבתם את השיטות כמו שצריך, התוכנית תעבוד היטב עם קטע הקוד הבא, ולבסוף תדפיס את ההודעה "Well done!". אם התוכנית כלל לא מצליחה לעבור הידור - וודאו שכל השיטות של החוזה ממומשות; גם הבדל של אות קטנה במקום גדולה (ולהפך) הוא משמעותי - השתמשו בהעתק/הדבק. במקרה של חוזה שמומש בצורה שגויה - התוכנית תדפיס הודעה ותעצור.

// TestStock.java

public class TestStock {
	
	private static void check(boolean value, boolean expected) {
		if(!(value == expected)) {
			System.err.println("Problem found on test!");
			System.exit(1);
		}
		else System.out.println("OK");
	}
	
	private static void check(double value, double expected) {
		if(!(value == expected)) {
			System.err.println("Problem found on test!");
			System.exit(1);
		}
		else System.out.println("OK");
	}
	
	public static void main(String[] args) {
		Stock stck = new Stock();
		System.out.print("Check initial value of stock: ");
		check(stck.sumStock(), 0);
		System.out.print("Check add item 1: ");
		check(stck.addItem("Tomato", "Red", 1.5), true);
		System.out.print("Check add item 2: ");
		check(stck.addItem("Cucumber", "Rotten", 3.5), true);
		System.out.print("Check add item 3: ");
		check(stck.addItem("Onion", "White", 4), true);
		System.out.print("Check sum value function: ");
		check(stck.sumStock(), 9);
		System.out.println("Print stock: ");
		stck.printStock();
		System.out.print("Check remove item 1: ");
		check(stck.removeItem("Tomato"), true);
		System.out.print("Check remove item 2: ");
		check(stck.removeItem("Onion"), true);
		System.out.print("Try to remove item not in stock: ");
		check(stck.removeItem("Gold"), false);
		System.out.print("Try to remove item that already removed: ");
		check(stck.removeItem("Tomato"), false);
		System.out.print("Check remove item 3: ");
		check(stck.removeItem("Cucumber"), true);
		System.out.println("Print stock again (Should be empty): ");
		stck.printStock();
		System.out.print("Check sum value function again: ");
		check(stck.sumStock(), 0);
		System.out.println("Well done!");
	}

}

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

פתרון (חלקי)

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


מחשבות על התכנון עריכה

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

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

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