Project 4 - MovieTix
Categories:
16 minute read
Introduction
PayTex is a (fictitious) company that develops technologies for electronic payment and crypto-currency systems. You have been hired by them to develop several components of a system called MovieTix.
MovieTix is, essentially, a hybrid debit/credit card for purchasing movie tickets. A MovieTix card can be pre-loaded with a particular number of tickets, can be used as a credit card, or can be used like a combination of the two. The prototype MovieTix system is being developed for university students and supports types of plans with the following “default” properties (though the system must support other properties, as described below).
- Movie Plan - a plan with both pre-paid tickets and the ability to purchase tickets on credit. Any pre-paid tickets that are not used during the semester are lost (and the purchase price is not refunded).
- Tiered Plan - a plan with both pre-paid tickets and the ability to purchase tickets on credit. It differs from a Movie Plan in that the first group of "purchases" is at a different price from the subsequent "purchases".
- Limited Plan - a plan with both pre-paid tickets and the ability to purchase a limited dollar amount of tickets on credit.
Existing Applications
Another PayTex employee has written two applications that will make use of your code when you complete it, and provided you with the source code.
The UsageSummarizer
can be used to print a table of
costs-per-movie for different amounts of usage (i.e., movies seen)
for different common plans. The Planalyzer
can be used
to print a table that contains the best plan (of the same common plans)
for different amounts of usage.
System Design
The relationships between the components that encapsulate plans can be modeled using the following UML class diagram.
classDiagram
class MoviePlan:::whitebg
MoviePlan: -double movieCost {readOnly}
MoviePlan: -double planCost {readOnly}
MoviePlan: -int prepaid {readOnly}
MoviePlan: -String name {readOnly}
MoviePlan: -double spent
MoviePlan: -int numberPurchased
MoviePlan: -int punches
MoviePlan: #Interval APPROVED_MOVIE_COSTS {readOnly}$
MoviePlan: #Interval APPROVED_PLAN_COSTS {readOnly}$
MoviePlan: +MoviePlan()
MoviePlan: +MoviePlan(name String, prepaid int, planCost double, movieCost double)
MoviePlan: #costOfPurchasedMovie() double
MoviePlan: #costToDate() double
MoviePlan: +getCategory() Category {exceptions=IllegalStateException}
MoviePlan: +getCostOfNextMovie() String
MoviePlan: +getCostPerMovie() double {exceptions=IllegalStateException}
MoviePlan: +getName() String
MoviePlan: +getPlanCost() double
MoviePlan: #numberPurchased() int
MoviePlan: #numberSeen() int
MoviePlan: #remainingPrepaid() int
MoviePlan: #spent() double
MoviePlan: +toString() String
MoviePlan: +use() boolean
class TieredPlan:::whitebg
TieredPlan: -double tierCost {readOnly}
TieredPlan: -int tierLimit {readOnly}
TieredPlan: +TieredPlan()
TieredPlan: +TieredPlan(name String, prepaid int, planCost double, movieCost double, tierLimit int, tierCost double)
TieredPlan: #costOfPurchasedMovie() double
MoviePlan<|--TieredPlan
class LimitedPlan:::whitebg
LimitedPlan: -double creditLimit {readOnly}
LimitedPlan: +LimitedPlan()
LimitedPlan: +LimitedPlan(name String, int prepaid, planCost double)
LimitedPlan: +LimitedPlan(name String, prepaid int, planCost double, movieCost double, creditLimit double)
LimitedPlan: +getCostOfNextMovie() String
LimitedPlan: +use() boolean
MoviePlan<|--LimitedPlan
The supporting components can be modeled using the following UML class diagram.
classDiagram
class Interval:::whitebg
<<immutable>> Interval
Interval: -double left {readOnly}
Interval: -double right {readOnly}
Interval: -boolean leftClosed {readOnly}
Interval: -boolean rightClosed {readOnly}
Interval: +Interval(original Interval)
Interval: +Interval(leftSymbol char, left double, right double, rightSymbol char) {exceptions = IllegalArgumentException}
Interval: +contains(value double) boolean
Interval: +closestTo(value double) double
Interval: +toString() String
Interval: +toString(formatString String) String
class Category:::whitebg
<<enumeration>> Category
Category: -Interval interval {readOnly}
Category: -String description {readOnly}
Category: -String symbol {readOnly}
Category: -Category(description String, symbol String, interval Interval)
Category: +getCategoryFor(price double)$ Category
Category: +getDescription() String
Category: +getSymbol() String
Category: +toString() String
Category: BARGAIN
Category: INEXPENSIVE
Category: MODERATE
Category: EXPENSIVE
Category: OUTRAGEOUS
class PlanUtilities:::whitebg
<<utility>> PlanUtilities
PlanUtilities: +findBestPlan(plans MoviePlan...)$ MoviePlan
Larger View
The details of each class in this diagram are provided below. You may add methods but you must not override any existing methods that are not explicitly overridden in the UML diagram and you must not shadow any attributes.
Note that, in Java, the UML property modifier {readOnly}
indicates that
an attribute must be declared final
.
The Interval
Class
The Interval
class is an encapsulation of an interval on the
real line. Interval
objects are used in several places.
In addition to the other specifications included in the UML diagram,
the Interval
class must comply with the following
specifications.
Attributes
The double
attributes left
and right
contain the left and right bounds of the interval.
The boolean
attribute leftClosed
indicates
whether left
is in the interval (when true
)
or not (when false
). Similarly, the boolean
attribute rightClosed
indicates
whether right
is in the interval or not.
Interval(Interval)
The one-parameter constructor is a copy constructor. You need not validate the parameter.
Interval(char, double, double, char)
The four-parameter constructor is passed a char
to
indicate whether the interval is closed on the left
(i.e., left
is in the interval), the left and right
bounds, and a char
to indicate whether the interval
is closed on the right. A leftSymbol
of '['
indicates that the interval must be closed on the left and a
rightSymbol
of ']'
indicates that the
interval must be closed on the right. Any other char
indicates that
the interval is open on that side (though the traditional characters are
'('
and ')'
.
This constructor must throw an IllegalArgumentException
if
left
is greater than right
.
contains(double)
Must return true if the given value is contained in the interval and false otherwise. Note that this method does not use tolerances. Hence, it is not appropriate for some kinds of calculations.
closestTo(double)
Must return the value in the closure of the interval (i.e., the interval and its bounds, whether or not it is closed) that is closest to the given number. Note that this method can, if the value is in the interval, return the value itself. (This operation is sometimes described as projecting the value onto the closure of the interval.)
For example, the closure of the open interval
(3.00, 8.00)
is [3.00, 8.00]
. So,
the point in the closure of the interval
(3.00, 8.00)
that is closest to 5.30
is 5.30
,
and the point in the closure
of the same interval that is closest to 100.50
is 8.00
.
toString()
Must return a String
representation of the interval. The
result must contain a '['
or '('
as
appropriate, followed by the left bound (in a field of width 6 with 2
digits to the right of the decimal point), followed by
a ','
, followed by a space, followed by the right bound
(in a field of width 6 with 2 digits to the right of the decimal
point), followed by a ']'
or
')'
as appropriate.
toString(String)
Like the version with no parameters, this method must return a
String
representation of the interval. However, the left
and right bounds must be formatted using the String
parameter as the format specifier (instead of being in a field
of width 6 with 2 digits to the fight of the decimal point). In other
words, if this method is passed a parameter of "%3.0f"
it
must format each bound in a field of width 3 with no digits to the
right of the decimal point.
This method need not validate the String
parameter. In
other words, it may assume that the format specifier is valid.
The Category
Enum
The Category
enum encapsulates a predefined set of
price categories.
In addition to the other specifications included in the UML diagram,
the Category
enum must comply with the following
specifications.
Constructor
By default, enum constructors are private , which is conveniently aligned with this specification (see the UML). So, you should not explicitly mark the Category
constructor as private
in your source code or Checkstyle will complain about the redundancy.
Elements
The Category
enum must contain five elements:
BARGAIN
with adescription
ofBargain
, asymbol
of$
, and aninterval
of[0.00, 5.00]
;INEXPENSIVE
with attributesInexpensive
,$$
, and(5.00, 11.00]
;MODERATE
, with attributesModerate
,$$$
, and(11.00, 15.00]
;EXPENSIVE
, with attributesExpensive
,$$$$
, and(15.00, 25.00]
; andOUTRAGEOUS
with attributesOutrageous
,$$$$$
, and(25.00, ∞]
.
getCategoryFor(double)
Must return the Category
object that contains the given price
(or null
if there is no corresponding Category
).
toString()
Must return a String
containing the description
,
a single space, and the symbol
(in parentheses).
For example, BARGAIN.toString()
must return the
String
literal Bargain ($)
.
The MoviePlan
Class
The MoviePlan
class encapsulates pre-paid movie plans
with a fixed up-front cost and number of tickets, and the ability to
purchase “extra” tickets on credit at a pre-determined cost.
In addition to the other specifications included in the UML diagram,
the MoviePlan
class must comply with the following
specifications.
Attributes
APPROVED_PLAN_COSTS
contains the range of allowable plan costs
(as determined by the company PayTex). Valid plan costs must be in the
interval [0.00, 200.00]
. Similarly, APPROVED_MOVIE_COSTS
contains the range of allowable ticket costs (which, though not relevant for
MoviePlan
objects is relevant for other plans). Valid
ticket costs must be in the interval [0.00, 25.00]
.
The attributes prepaid
and punches
contain
the number of prepaid tickets in the plan and the number of pre-paid
tickets used to-date (which must be 0
initially).
The attribute numberPurchased
contains the number of
“extra” movies that have been purchased to-date (which must be 0
initially) and the attribute spent
contains the amount
spent to-date on “extra” movies (which must be 0.00
initially).
Constructors
The default constructor must initialize the attributes to the default
values as follows: the name
must be Movie Plan
,
the number of pre-paid tickets must be 5
, the plan cost must be $50.00
,
and the cost per “extra” movie must be $15.00
.
The explicit value constructor must initialize the attributes in the
obvious way, with a few exceptions. The plan cost attribute must be
assigned the value in APPROVED_PLAN_COSTS
that is closest
to the parameter planCost
and the movie cost attribute
must be assigned the value in APPROVED_MOVIE_COSTS
that
is closest to the parameter movieCost
.
costOfPurchasedMovie()
Must return the cost of a purchased (i.e., not pre-paid) movie.
Note: The use of the term “purchased” in the name of this method is
not intended to convey the past tense (i.e., it does not imply that
the movie was purchased in the past). This method could just as
easily have been named costOfPurchasingAMovie()
.
costToDate()
Must return the cost-to-date of the plan (i.e., the sum of the plan
cost and the amount spent to-date on purchased/“extra” movies). So, for example,
if the plan cost $100.00, all of the pre-paid movies have been used, and one
“extra” movie has been purchased at a price of $12.50
then this method must
return 112.50
.
getCategory()
Must return the current Category
for the plan based on
the cost per movie (to-date). For example, if the cost per movie is currently
$7.00
then this method must return the INEXPENSIVE
Category
.
If the cost per movie is undefined then this method must throw an
IllegalStateException
.
getCostOfNextMovie()
Must return a String
representation of the cost of the next
movie. If the plan still has punches then this method must return
“Free”
. Otherwise, it must return the cost of a purchased
movie (preceded by a dollar sign, in a field of width 6, with 2 digits to
the right of the decimal point).
getCostPerMovie()
If the number of movies seen to-date is 0
it must throw an
IllegalStateException
(since the cost per movie is
undefined when no movies have been seen). Otherwise, it must return
the cost-to-date divided by the number of movies seen to-date.
numberPurchased()
Must return the number of movies that have been purchased to-date (which does not include the number of pre-paid movies that have been seen).
numberSeen()
Must return the number of movies that have been seen (whether pre-paid or purchased) to-date.
remainingPrepaid()
Must return the number of unused pre-paid tickets.
spent()
Must return the amount of money that has been spent on purchased movies (which does not include the plan cost).
toString()
Must return a String
representation of the plan. The
String
must be tab-delimited and include four items (in
order): the name, the cost per movie (preceded by a dollar sign in a
field of width 6, with 2 digits to the right of the decimal point),
the String
representation of the
current Category
, and the cost of the next movie
(formatted as described above).
If the cost per movie is undefined, then this method must return: the
name, followed by two tabs, followed by the String
literal “Unused”
, followed by the cost of the next movie
(formatted as described above)
use()
The use()
method is called when a user wants to “see” a
movie. It must adjust the number of punches, the amount spent, and/or
the number purchased (as appropriate, depending on whether there are
or aren’t unused punches).
Since it is always possible to “see” a movie under this plan, it must
always return true
.
An Example
A MoviePlan
with the attributes
(“A”, 2, 20.00, 12.50)
would have the following costs per movie.
Movies | Cost To Date | Cost Per Movie |
1 | 20.00 | 20.00 |
2 | 20.00 | 10.00 |
3 | 32.50 | 10.83 |
4 | 45.00 | 11.25 |
5 | 57.50 | 11.50 |
The LimitedPlan
Class
The LimitedPlan
class is a specialization of the
MoviePlan
class in which there is a limit on the amount of money
that can be spent on credit.
In addition to the other specifications included in the UML diagram,
the LimitedPlan
class must comply with the following
specifications.
Attributes
The creditLimit
attribute must contain the limit on the amount
of money that can be spent on credit.
Constructors
The default constructor must initialize the name to “Limited Plan”, the
number of pre-paid tickets to 5
, the plan cost to $50.00
, the cost of
purchased tickets to $15.00
, and the credit limit to $100.00
The 3-parameter constructor is used to construct “pre-paid only” plans.
To that end, it must initialize the cost of purchased tickets to the maximum
possible value, and the credit limit to $0.00
.
getCostOfNextMovie()
Must return a String
representation of the cost of the next
movie. If the plan is unusable (i.e., has no available punches and
insufficient credit to purchase a ticket) then it must return
“N/A”
. Otherwise, it must exhibit the same behavior as
a MoviePlan
.
use()
The use()
method is called when a user wants to “see” a
movie. It must return false
if the plan is unusable (as
described above). Otherwise, it must exhibit the same behavior as
a MoviePlan
.
An Example
A LimitedPlan
with the attributes
(“B”, 2, 25.00, 15.00, 30.00)
would have the following costs
per movie.
Movies | Cost To Date | Cost Per Movie |
1 | 25.00 | 25.00 |
2 | 25.00 | 12.50 |
3 | 40.00 | 13.33 |
4 | 55.00 | 13.75 |
5 | N/A | N/A |
The TieredPlan
Class
The TieredPlan
class is a specialization of
a MoviePlan
in which there is a special initial tier of purchases
(at a different price from ordinary purchases).
In addition to the other specifications included in the UML diagram,
the TieredPlan
class must comply with the following
specifications.
Attributes
The tierLimit
is the number of tickets in the initial
group of tickets. The tierCost
attribute must contain the
cost (per movie) of the initial group of purchased tickets.
Constructors
The default constructor must initialize the name to “Tiered Plan”, the
number of pre-paid tickets to 5
, the plan cost to $100.00
, the normal
purchase price to $10.00
, the number of tickets in the initial group
to 5
, and the price of the initial purchases to $5.50
.
The explicit value constructor must initialize the attributes in the
obvious way, with a few exceptions. The plan cost attribute must be
assigned the value in APPROVED_PLAN_COSTS
that is closest
to the parameter planCost
, and the movie cost and tier
cost attributes must be assigned the value
in APPROVED_MOVIE_COSTS
that are closest to the
parameters movieCost
and tierCost
,
respectively.
costOfPurchasedMovie()
Must return the cost of a purchased (i.e., not pre-paid) movie. Note that the value returned by this method will depend on the number of movies that have been purchased (because of the two price tiers).
An Example
A TieredPlan
with the attributes
(“C”, 2, 30.00, 12.50, 2, 7.50)
would have the following
costs per movie.
Movies | Cost To Date | Cost Per Movie |
1 | 30.00 | 30.00 |
2 | 30.00 | 15.00 |
3 | 37.50 | 12.50 |
4 | 45.00 | 11.25 |
5 | 57.50 | 11.50 |
The PlanUtilities
Class
The PlanUtilities
class is a utility class that can be
used to perform operations on MoviePlan
objects.
In addition to the other specifications included in the UML diagram,
the PlanUtilities
class must comply with the following
specifications.
findBestPlan()
Must return the least expensive MoviePlan
(among those passed to
it) based on each plan’s current cost-per-movie.
This method must account for several special situations:
- In the event of a tie, it must return the
MoviePlan
that appears earliest in the argument list. - Any
null
parameters must be ignored. - It must return
null
if all of the parameters arenull
. - It must return
null
if there are 0 parameters. - It must ignore parameters that have an undefined cost-per-movie.
- It must return
null
if all of the parameters have an undefined cost-per-movie.
Submission and Grading
You must submit complete implementations of all classes/enums, as well as JUnit tests for each class/enum.
For full credit your submission must satisfy three conditions:
- The classes/enums must pass all of the student-provided JUnit tests.
- The student-provided unit tests must achieve 100% method, statement, and branch coverage of the classes/enums.
- The classes/enums must pass all of the instructor-provided correctness tests.
Grading
Autograder | Code Review | Files to Submit | |
---|---|---|---|
Part A: Due Tue 3/22 |
30 pts | 0* pts | Interval.java IntervalTest.java Category.java CategoryTest.java |
Part B: Due Thu 3/24 |
30 pts | 0* pts | MoviePlan.java MoviePlanTest.java |
Part C: Due Mon 3/28 |
40 pts | 0* pts | LimitedPlan.java LimitedPlanTest.java TieredPlan.java TieredPlanTest.java PlanUtilities.java PlanUtilitiesTest.java |
For all three parts, you may submit up to 10 times without penalty. Additional submissions (if any) will receive a penalty of 1 point each. Be sure to use all the development tools offline: Checkstyle, JUnit, coverage analysis, and the debugger.
The code reviews are worth 0 additional points this time. However, points may be deducted if your code is difficult to understand. For example: confusing variable names, too few or too many comments, etc.
Hints and Suggestions
In addition to the information above, you might find the following information helpful.
Recommended Process
You are strongly encouraged to use test-driven development (TDD) (i.e., to develop the tests for a class/method before developing the class/method itself). To facilitate that process, stubbed-out versions of all of the classes/enums are available in p4-stubs.zip .
Whether or not you use TDD, you should work on the classes/enums one at a time, in the following order:
- The
Interval
class and associated JUnit tests. - The
Category
class and associated JUnit tests. - The
MoviePlan
class and associated JUnit tests. - The
LimitedPlan
class and associated JUnit tests. - The
TieredPlan
class and associated JUnit tests. - The
PlanUtilities
class and associated JUnit tests.
After that, you should perform system testing using the
UsageSummarizer
and Planalyzer
classes.
Getting 100% Coverage
To get 100% coverage of enums and utility classes, you need to use some “tricks”. For more information, see the following EclEmma help page (which also explains how to run an entire test suite).
Questions to Think About
The following question will help you understand the material from this assignment (and from earlier in the semester). You do not have to submit your answers, but you should make sure that you can answer them.
-
The
findBestPlan()
method in thePlanUtilities
can be passed a variable number of parameters. From the invoker's standpoint, what is the advantage of using a formal parameter ofMoviePlan...
rather than a formal parameter ofMoviePlan[]
? -
Each plan in the
Planalyzer
application is declared to be aMoviePlan
object but some are instantiated as other objects. Why does the code compile? -
Given the signature of the
findBestPlan()
method, would the following fragment compile?LimitedPlan limited; TieredPlan tiered; MoviePlan best; limited = new LimitedPlan(); tiered = new TieredPlan(); best = PlanUtilities.findBestPlan(tiered, limited);
-
Suppose the following methods were added to the
PlanUtilities
class:public static void printPlanInfo(MoviePlan plan) { System.out.println("Movie Plan"); System.out.println(plan.toString()); } public static void printPlanInfo(LimitedPlan plan) { System.out.println("Tiered Plan"); System.out.println(plan.toString()); } public static void printPlanInfo(TieredPlan plan) { System.out.println("Tiered Plan"); System.out.println(plan.toString()); }
What would be printed by the following fragment?
MoviePlan a, b, c; LimitedPlan d; TieredPlan e; a = new MoviePlan(); a.use(); b = new LimitedPlan("Limited Plan", 1, 15.00); b.use(); c = new TieredPlan(); c.use(); d = new LimitedPlan("Limited Plan", 1, 15.00); d.use(); e = new TieredPlan(); e.use(); PlanUtilities.printPlanInfo(a); PlanUtilities.printPlanInfo(b); PlanUtilities.printPlanInfo(c); PlanUtilities.printPlanInfo(d); PlanUtilities.printPlanInfo(e);