UML and Java exercise: Find the cheaper meat brand

The Problem

We will write a program that helps us with the following problem: ground meat from brand A costs $3.60 for 450 grams, while ground meat from brand B costs $3.95 for 500 grams. Which brand is cheaper?

Create a “Brand” class. Suggest attributes and methods that are relevant for this problem.

Make an activity diagram that shows how a client object (client program) can use instances of this class to find out which of the two brands is cheaper.

Write the client program.

Discussion

From the problem description, we can draw following properties: brandName, cost, and weight. The brandName is the identity of the brand, so we can make it immutable. The other two properties cost and weight do not have much use in the context of the problem. It would be much better if we had costPerKilogram property, so we replace the two properties cost and weight with one costPerKilogram. Given the cost of some weight it is easy to find the cost per kilogram:

double costPerKilogram = cost/weight;

The only important thing here is that the weight must be in kilograms. We can introduce further abstraction by using unit instead of weight and discuss costPerUnit. This will make it possible the client program to use diferent units, for example grams, pieces, meters, etc. We will not go so far.

The above discussion can be expressed by the following class diagram:

We provided the MeatBrand class with two constructors to make it easier to create objects when we know the price per kilogram or the cost of a given weight. getCostPerKilogram() and getBrandName() are accessors to costPerKilogram and brandName properties. At this time we do not need to implement mutators for these properties. As discussed above, the brandName property is immutable (by adding the final modificator).

The activity diagram below illustrates one possible way for a client program to find the cheaper brand. As you can see from the diagram doesn't discuss the case when both brands have equal prices. Because the problem states that we need to find the cheaper brand, this means that the program wants always to make a choice. We decided that if both prices are equal, that we will always take the brandB as the cheaper brand.

For the exceptional case when both prices are equal, there can be other consideratations to drive the choice:

  1. We can pick a random brand,
  2. We can select brand based on its package,
  3. Quantity on stock can also be considered, etc.

The Solution

The Java code that defines the MeatBrand class follows:

 
/**
 * A class that represents a meat brand.
 *
 * @author Ivan Georgiev
 */
public class MeatBrand {
	final private String theBrandName;
	private double theCostPerKilogram;
 
	public MeatBrand(String aBrandName, double aCostPerKilogram) {
		theBrandName = aBrandName;
		theCostPerKilogram = aCostPerKilogram;
	}
 
	public MeatBrand(String aBrandName, double weight, double weightCost) {
		MeatBrand(aBrandName, weightCost/weight);
	}
 
	public String getBrandName() {
		return theBrandName;
	}
 
	public double getCostPerKilogram() {
		return theCostPerKilogram;
	}
}

Following application implements the discussed activity diagram:

/**
 * @(#)GroundMeat.java
 *
 * GroundMeat application
 *
 * This application finds which meat brand is cheaper.
 *
 * @author Ivan Georgiev
 * @version 1.00 2008/9/3
 */
import javax.swing.JOptionPane;
 
public class GroundMeat {
 
    public static void main(String[] args) {
    	MeatBrand brandA = new MeatBrand("A", 3.60, 0.45);
    	MeatBrand brandB = new MeatBrand("B", 3.95, 0.50);
 
    	// We can also rewrite the following using the ternary
    	// conditional operator:
    	// MeatBrand cheaperBrand = 
    	//			  (brandA.getCostPerKilogram() < brandB.getCostPerKilogram()) 
    	//			? brandA : brandB;
    	MeatBrand cheaperBrand;
 
    	if ( brandA.getCostPerKilogram() < brandB.getCostPerKilogram() ) {
    		cheaperBrand = brandA;
    	} else {
    		cheaperBrand = brandB;
    	}
 
    	JOptionPane.showMessageDialog(null, "Brand " + cheaperBrand.getBrandName() 
    			+ " is cheaper.");
    }
}

You might notice that the MeatBrand is not fully covered by tests. By executing only one case we do not make sure the class works. We have only to cases to check:

  1. Create MeatBrand instance with given name and costPerKilogram - The result is that a new instance is created and initialized with corresponding brand name and cost per kilogram.
  2. Create MeatBrand instance with given name, weight and cost per weight - The result is that a new instance is created and initializaed with corresponding brand name and cost per kilogram, properly calculated from given weight and cost per weight.

We can implement tests using JUnit, but for our case we found it is more appropriate to incorporate tests into the application.

/**
 * @(#)GroundMeat.java
 *
 * GroundMeat application
 *
 * This application finds which meat brand is cheaper.
 *
 * @author Ivan Georgiev
 * @version 1.00 2008/9/3
 */
import javax.swing.JOptionPane;
 
public class GroundMeat {
	private static String failureMessage;
	/**
	 * Performs tests on MeatBrand class.
	 * If tests fail, the failureMessage member is set.
	 * @return boolean Returns true upon success.
	 */
	public static boolean testMeatBrandClass() {
		boolean result = true;
		double dobuleTolerance = 0.000001;
 
		// Testing create brand instance
		MeatBrand brand = new MeatBrand("brandA", 100);
		if (result) {
			if (!brand.getBrandName().equals("brandA")) {
				failureMessage = "Brand name doesn't match.";
				result = false;
			}
		}
		if (result) {
			if (Math.abs(brand.getCostPerKilogram() - 100) > dobuleTolerance) {
				failureMessage = "Cost per kilogram doesn't match.";
				result = false;
			}
		}
 
		// Testing create brand with overloaded constructor
		if (result) {
			brand = new MeatBrand("brandC", 5, 25.5);
			if (!brand.getBrandName().equals("brandC")) {
				failureMessage = "Brand name doesn't match";
				result = false;
			}
		}
		if (result) {
			if (Math.abs(brand.getCostPerKilogram() - 5.1) > dobuleTolerance) {
				failureMessage = "Cost per kilogram not calculated properly.";
				result = false;
			}
		}
		return result;
	}
 
    public static void main(String[] args) {
 
    	if ( testMeatBrandClass() ) {
    		JOptionPane.showMessageDialog(null, "Test PASSED");
    	} else {
    		JOptionPane.showMessageDialog(null, "Test FAILED with message: \n"
    				+ failureMessage);
    		return;
    	}
 
    	MeatBrand brandA = new MeatBrand("A", 3.60, 0.45);
    	MeatBrand brandB = new MeatBrand("B", 3.95, 0.50);
    	MeatBrand cheaperBrand = 
    			  (brandA.getCostPerKilogram() < brandB.getCostPerKilogram()) 
    			? brandA : brandB;
 
    	JOptionPane.showMessageDialog(null, "Brand " + cheaperBrand.getBrandName() 
    			+ " is cheaper.");
    }
}

When checking price per kilogram we took in account the lost of precision because of limited floating point representation. The latest version also uses ternary conditional operator ?: as a selector.

 
uml/excercises/groundmeat.txt · Last modified: 2009/10/31 23:39 (external edit)
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki