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.
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:
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:
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.