[go: up one dir, main page]

0% found this document useful (0 votes)
41 views17 pages

Guide To Getting Started

The document provides a staged guide to developing a Magic Bakery simulation. It discusses setting up the test environment and necessary packages. It guides implementing the Ingredient and Layer classes with their relationships and attributes. It also covers implementing the CustomerOrder class to handle orders.

Uploaded by

Iris
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
41 views17 pages

Guide To Getting Started

The document provides a staged guide to developing a Magic Bakery simulation. It discusses setting up the test environment and necessary packages. It guides implementing the Ingredient and Layer classes with their relationships and attributes. It also covers implementing the CustomerOrder class to handle orders.

Uploaded by

Iris
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 17

Getting Started: The Magic Bakery

version 1.43

Typos/errors? Email sarah.clinch@manchester.ac.uk and I'll get them fixed.

Table of Contents

1. Preliminaries
1.1 Some warnings and notes before you start

2. A Staged Guide to Development


2.1 The right environment
2.1.1 The Test environment
2.1.2 The Driver program
2.1.3 Packages

2.2 First Implementation Steps (Stage 1)


2.2.1 Ingredients and Layers
2.2.2 The CustomerOrder class
2.2.3 The MagicBakery class

2.3 Laying the Foundations for File and Console IO (Stage 2)


2.3.1 Creating Ingredients, Layers and CustomerOrders from the CSV files
2.3.2 CustomerOrderStatus
2.3.3 Console IO
2.3.4 The Player Class

2.4 Turn-taking
2.5 Collection/List type cleanup
2.6 ConsoleUtils
2.7 Ingredient and Layer card operations
2.7.3 The Pantry
2.7.3 Layers
2.7.3 Baking

3. Need More Help?


1. Preliminaries

You do not have to follow the instructions in this document if you do not want to, and the
instructions below may not give you a complete solution. They're intended to be a general
guide for those who want some help knowing how to approach the problem.

Your solution must meet the provided specification, so you should take time to carefully read
all instructions and UML class diagrams. Do this before progressing any further with the
instructions in this document!

1.1 Some warnings and notes before you start!

A Note on List and Collection Types: In these early steps of the Getting Started
guide, the UML diagrams and instructions use the ArrayList type in places where
the full UML has a Collection or List type. This is to allow you to get
functional(ish) implementations of classes prior to release of the course materials on
Data Structures. After the first steps are completed, we instruct you to replace these
with the correct types -- make sure that you do this!

List vs. Collection vs. ...? We're often asked why we use the Collection and
List interfaces as types in our UML diagrams rather than specific implementations
of those interfaces. This is because we want to challenge you to establish the correct
concrete implementation based on the description of the semantics that we give you in
the specification. E.g. What classes are appropriate for a LIFO structure? We also want
you to be aware that you can declare a variable with one type, and then instantiate it
with any compatible type (subclass or implementation). E.g.
List<String> foo = new ArrayList<String>(); . When you write your
implementations, you must declare the variable with the type specified in the
UML. You can then instantiate it with the type you think is most appropriate.

Note that in most of the instructions we suggest some println statements and
other method calls to help you ensure each small step is working as expected. We
don't explicitly tell you when to remove these, but you should remove them when they
are no longer needed.

2. A Staged Guide to Development

2.1 The right environment

2.1.1 The Test environment

Familiarise yourself with the mechanisms for testing/running code.


1. Clone the GitLab repository for this coursework. Download the updated test classes
from Blackboard and use these to replace existing files in src/test/test .

Run the test.sh file. It might seem counterintuitive to start by running the tests
before any code, but once you know you can run the tests then it's easy to track
your progress. Familiarise yourself with the test output, it should end as follows:

[ 26 containers found ]
[ 0 containers skipped ]
[ 26 containers started ]
[ 0 containers aborted ]
[ 13 containers successful ]
[ 13 containers failed ]
[ 532 tests found ]
[ 0 tests skipped ]
[ 110 tests started ]
[ 0 tests aborted ]
[ 0 tests successful ]
[ 110 tests failed ]

Here, the structural tests and the javadoc tests should run successfully (but fail).
The functional tests won't run at all because they won't compile without functioning
classes in the bakery and util packages. Hence the total number of
failing tests is not the same as the number of tests we tell you that there are
in the Manual.

Do not assume that just because you have zero failing tests, you must be passing
everything -- check the number of successful tests matches the total number
given in the manual.

If you scroll back through the output, you'll see it starts with all the compilation errors for the
functional tests, followed by something like this:

Each of the red lines represented one failing test, and the blue lines are the classes in which
the failing tests can be found. Below this you can view the Stack trace for each failing test:

Note that in this document we only have you write class/method signatures as you
come to write associated the functionality. This means that (due to interdependencies
between classes) functional tests may not compile until very late on. To avoid this, you
could choose to write the class/method signatures now, and then work through adding
functionality in accordance with the rest of this guide.

2.1.2 The Driver program

1. Open the provided driver program ( BakeryDriver.java ). Compile and run the
driver -- nothing will happen because the main method is empty.

2.1.3 Packages

In order to get the tests to pass, you're going to need to get to grips with packaging.

1. Following the materials signposted on this topic, start by creating the bakery
package containing one empty class: Ingredient classes.
2. Re-run the tests, hopefully, at one test now passes.
3. Make your first commit and then push your commit back to GitLab. Use gradescope to
run the tests again, this time remotely. The output should be the same as your local
tests.

2.2 First Implementation Steps (Stage 1)

At the end of Section 2.2.3, your solution should match the following Stage 1 UML
diagram:
2.2.1 Ingredients and Layers

I'd suggest starting your implementation by implementing the super/subclass relationship


between the Ingredient and Layer classes.

The above UML for those two classes involves one potentially unfamiliar type,
ArrayList . (As per the above A Note on List and Collection Types this isn't exactly the
same type as is used in the final UML, but it's a good starting point and won't require huge
changes to modify later).

A Quick Diversion into ArrayLists

ArrayList is a class defined in the Java API that behaves similarly to an array, but
with dynamic (i.e. not fixed) sizing.

Like an array, you create an ArrayList to hold a given type of object (in this
case Ingredient objects)
Unlike an array, ArrayList is defined outside of the java.lang package
so you will need to import it.
Unlike an array, ArrayList is a class which means that you need to call a
constructor to create it. For example, the following creates an ArrayList to
hold String objects:

ArrayList<String> myList = new ArrayList<String>();

Unlike an array, the most common way to add items to an ArrayList does not
involve specifying the index at which they will be added, instead, they're just
added at the next available index.

myList.add("Hello");
myList.add("World");

However, like an array, you can get , set , or remove the item at a given
index (using the linked methods rather than square-bracket indexing). The length
of an ArrayList is retrieved using the size() method.

myList.set(0, "Hi");
myList.remove(1);
System.out.println(myList.size());```

We cover ArrayLists in Week 7, alongside other data structures.

Diversion over, let's return to implementing these two classes.

1. Let's fill in the Ingredient class. You can quickly and easily implement everything
in the above UML for this class. Add a couple of lines to the BakeryDriver.java
file to create some Ingredient objects (e.g., "Flour", "Sugar", "Eggs" and "Milk")
and print out the result of calling toString on these objects.
2. Modify BakeryDriver.java to add these new Ingredient objects to an
ArrayList . Run test.sh and see what (if any) output has changed.
3. Next start the Layer class. Start with the recipe attribute, constructor and
getRecipe method. Modify BakeryDriver.java to create a Layer object
passing the ArrayList you created in Step 2 as its recipe parameter.
4. Add the getRecipeDescription method to the Layer class. There are a lot of
ways you can do this, the simplest of which would simply be to use String concatenation
("Hello " + "World") and one of the loops that we used right back in Week 1. Modify
BakeryDriver.java to print the return value from
Layer.getRecipeDescription() . Run test.sh and see what (if any) output
has changed.

2.2.2 The CustomerOrder class

Next, look at the CustomerOrder class. Having got to grips with ArrayLists in the Layer class,
there should now not be anything new here, just more of the same.

1. Have a go at implementing all of the attributes and methods outlined in the Stage 1 UML
for this class.
2. Modify BakeryDriver to use the existing Ingredient and/or Layer objects to
make up a CustomerOrder . Run test.sh and see what (if any) output has
changed.

2.2.3 The MagicBakery class

We're going to create a very simple outline for the MagicBakery class.

In this stage of the UML, the only things specified are:


1. A constructor (which can safely ignore all of its parameters for now)
2. That it instantiates CustomerOrder objects. For this, we can just move the code
currently in the BakeryDriver main method into MagicBakery .
3. Modify BakeryDriver.java so that it creates a new MagicBakery object, and
the output of running BakeryDriver should now be completely unchanged. Run
test.sh and see what (if any) output has changed.
4. Commit and push your code back to GitLab. Check the state of the JUnit tests on
Gradescope, they should match what you saw locally.

At this point, your solution should match the Stage 1 UML diagram.

2.3 Laying the Foundations for File and Console IO (Stage 2)

Stage 2 involves making use of some of the File and Console IO we covered in Weeks 5 and
3 respectively. In addition, we'll add the CustomerOrderStatus and Player types.

At the end of Section 2.3.4, your solution should match the following Stage 2 UML
diagram:

2.3.1 Creating Ingredients, Layers and CustomerOrders from the CSV files

In Week 5 we learned different types of File IO, and so we can put this knowledge into effect
next.

To do this, we need to start a new class CardUtils (in the util package). This class
provides helper methods that are designed to parse files and return instances of the various
card types.

1. Assuming we start with the simplest card type, we first need to write the
readIngredientFile and stringToIngredients methods. Each line in the
ingredients.csv file contains two comma-separated entries: a name, and a
count. The name corresponds to the name attribute of the Ingredient class, and
the count is the number of copies of that Ingredient that should be present in the
pantryDeck right at the start of the game (before any cards are placed into the
pantry . Initially, I'd suggest populating stringToIngredients with just a
System.out.println statement that prints the str parameter. Then work on
the implementation of readIngredientFile , ensuring it reads the file specified file
line-by-line and makes a call to stringToIngredients for each line of the file.
2. Modify the constructor of the MagicBakery so that the ingredientDeckFile
attribute is passed to a call to readIngredientFile . Check that the output is as
you expect and that the readIngredientFile method is suitably robust to errors.
3. Revisit the stringToIngredients method, ensuring that each of the passed
Strings is parsed and the appropriate number of Ingredient objects are
instantiated and returned. Add a call to print the size() of the return value of
readIngredientFile , compile and run BakeryDriver to ensure that this size
is as you'd expect (63 for the provided ingredients.csv file).
4. Repeat the above four steps for the Layer objects (the rules tell us that there are
four copies of each type of Layer , so 24 Layers should be created in total).
5. Finally, we'll follow the same steps for CustomerOrders . In this case, we need to
make sure that when stringToCustomerOrder parses the ingredients in a
recipe/garnish, it can tell if these are Layers or not. The parameter layers will
be used to do this, and should contain all of the unique Layer objects created by
readLayerFile (there are 25 CustomerOrder s set out in customers.csv ).
6. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.

2.3.2 CustomerOrderStatus

We're missing only one instance variable in the CustomerOrder class. This attribute
status is of type CustomerOrderStatus , which is an enum.

1. Define the CustomerOrderStatus as an inner class in CustomerOrder .


2. Add the status attribute to the CustomerOrder class and ensure it is initialised
with the correct value in the CustomerOrder constructor.
3. Add the getter and setter methods for status as well as the abandon method.
4. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.

2.3.3 Console IO

In Week 3, we used the Console class to get user input. In the ConsoleUtils class,
we create a wrapper around an instance of Console that allows us to write some helper
methods for common user prompts (e.g. "Yes or No" -- "Do you want to continue? Yes or
no?", "Do you want to overwrite an existing file? Yes or no?").

1. Create an outline ConsoleUtils class with console attribute and constructor.


2. Add both of the readLine methods -- these are straightforward wrappers around the
corresponding methods in the Console class.
The remaining methods in ConsoleUtils each take String prompt parameter, which
is the text that should prefix the final set of options. For example, given the two "Yes or No"
examples above, you'd expect calls to promptForYesNo to have different prompt
parameters that match the text above:

promptForYesNo("Do you want to continue?");


promptForYesNo("Do you want to overwrite an existing file?");

4. Two methods return a boolean value: promptForYesNo and


promptForStartLoad . Implement these first, and update your BakeryDriver
or MagicBakery class to allow you to test they behave as expected.
5. Next implement promptForFilePath . Modify your MagicBakery class to
ensure that the path to at least one of the customers , layers or
ingredients CSVs are determined based on user input.
6. The last `ConsoleUtils` method we'll concern ourselves with for now is
promptForNewPlayers , which should loop until the user has entered the names of
their desired number of players (2-5) and then return an ArrayList of Strings
representing the player's names. After implementing this method, add an outline
method startGame to the MagicBakery class and add a statement within this
method to print the value of the playerNames parameter (I find the .toString
method of the ArrayList class useful for testing purposes). Modify the main
method of BakeryDriver so that it:

i. Creates a new MagicBakery object


ii. Calls promptForNewPlayers and stores the result
iii. Calls startGame on the MagicBakery object created in (i), using the result
of (ii) as the playerNames parameter.

7. Commit and push your code back to GitLab.

2.3.4 The Player Class

Now that we have our player names, we can create some Player objects.

1. Outline the basics of the Player class including the constructor, name attribute
and toString method.
2. Add an ArrayList attribute players to the MagicBakery to hold Player
objects. Modify the MagicBakery constructor to instantiate the players variable.
3. Modify MagicBakery.startGame so that instead of printing the return value from
promptForNewPlayers it uses that same return value to create Player objects,
add them to players and print them. (Don't forget to check the number of players,
behaving as per the specification if this is not valid).
4. Add the hand attribute to the Player class and modify the constructor so that this is
initially instantiated as an empty ArrayList . Implement the addToHand ,
removeFromHand , getHand and getHandStr methods and test them using
some Ingredient objects (these could just be manually created objects such as
those we created in Stage 1).
5. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.

At this point, your solution should match the Stage 2 UML diagram.

2.4 Turn-taking

Now that we have players in the game, it makes sense to think about how those players can
each take turns as they play.

At the end of Section 2.5, your solution should match the following Stage 3 UML
diagram:

1. Now is a good time to start exploring some new data structures, starting with making an
appropriate choice for the players attribute in MagicBakery . Thinking about the
structures you encountered in Week 7, you need to choose a data structure that:

directly or indirectly implements the Collection interface


will allow you to easily cycle through its contents

2. Modify the definition of the players variable in MagicBakery so that it matches


the type specified in the UML ( Collection ). In the next step, you will instantiate it
with the type you have identified.

3. Modify the MagicBakery constructor to ensure that your instantiation of the


players variable is in accordance with the type you identified in 1.

4. Implement the getPlayers method, again making sure that the method signature
has a return type that matches the type specified in the UML, despite the fact that the
specific instance it returns is not directly of that type (although it should be an
implementation of it).
5. Implement getCurrentPlayer . The specification is fairly open here and does not
specify exactly how you should track whose turn it is. You need to make a design
decision about how you will do this, adding any variables and/or methods needed to
support you in doing this. Not however, that your came should expect endTurn to be
called before changing the current player.

6. Implement getActionsPermitted and getActionsRemaining . These


methods need you to decide how you will track the actions taken by players.

7. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.

8. Implement ConsoleUtils.promptForExistingPlayer as per the specification


(make sure you don't include the currentPlayer in the list of players that the user
can choose from). This will be the template for other similar methods in the
ConsoleUtils class, so try and avoid making design decisions that won't
generalise well. (Don't bother trying to write those other methods right now though).
Note that in a two-player game, the method doesn't actually need to ask the user
anything, it already knows who the card must be passed to.

9. Implement MagicBakery.passCard , remembering that this is a method that counts


as an action.

10. Modify BakeryDriver so that it does all of the following:

Creates a new MagicBakery instance


Prompts for player names
Calls startGame with the specified player names
In a loop (20 times)
Prints the current player's name
Asks the user to enter the name of a player to pass a card to
Passes a card to the specified player (could be a random selection, or the one
at index 0, it doesn't really matter).
Prints the current player's hand Check the print output -- is the player name
correct? Are both players' hands updated correctly after each card is passed?
After an appropriate number of card passes, does the loop terminate with the
Exception type described in the specification? (2-3 players get 3 actions per
turn, 4-5 players get 2 actions per turn).

11. Implement endTurn , ensuring that it can't be called if the player has actions
remaining, that currentPlayer is updated, and that if every player has had a turn
the code prints "New round". Compile and run everything again and check the print
output. Do the player names update after the appropriate number of actions? Does it
correctly identify the end of a round AND then correctly set currentPlayer back to
the Player who took the first turn?

12. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.

2.5 Collection/List type cleanup

Having made your first decision about exactly what datatype a given Collection should
be instantiated as, it's probably best to go back and revisit our previous List and
Collection variables that we declared as ArrayList types.

1. Begin with a list of all the ArrayList types defined in the Stage 2 UML. I think that
looks something like this:

CardUtils

The parameters for:


CardUtils.readCustomerFile
CardUtils.stringToCustomerOrder

The return types for:


CardUtils.readCustomerFile
CardUtils.readIngredientFile
CardUtils.readLayerFile
CardUtils.stringToIngredients
CardUtils.stringToLayers

ConsoleUtils

The return type for ConsoleUtils.promptForNewPlayers

Layer

The recipe attribute


The recipe parameter passed to the constructor
The return type for getRecipe

CustomerOrder

The garnish attribute


The recipe attribute
The garnish parameter passed to the constructor
The recipe parameter passed to the constructor
The return type for getGarnish
The return type for getRecipe
Player

The hand attribute


The ingredients parameter passed to addToHand
The return type for getHand

MagicBakery

The playerNames parameter passed to startGame

2. Compare each of these Stage 2 UML type definitions with those in the full UML
document. If they're the same, nothing needs to change.

3. For each type that differs:

a. Start by modifying the type declaration. I.e. if you currently have:

ArrayList<String> foo = new ArrayList<String>();

But the UML says that foo should be a Collection , then start by modifying the
type as follows:

Collection<String> foo = new ArrayList<String>();

b. Check that your new declaration compiles correctly. If there are any errors, they may
be addressed in the next step, but it might be that something else is also needed (e.g.
the addition of an import statement). c. Next consider the instantiation type. Check the
specification for any information about the exact semantics that should be exhibited by
this variable/parameter/return type (usually the latter two are determined by the former,
e.g. the behaviour of the return type of getX is determined by the behaviour of
variable x ). This could be information about which entries are removed from the
collection first, or if the collection can hold duplicate values. d. Use the teaching
materials to select the most appropriate data type for the semantics you have identified.
Ensure that the type you select is compatible with the declaration type specified in the
UML. e. Modify the instantiation of your type to match the type you have identified. For
example, if I determined that foo should actually be of Vector type (seems
unlikely, but ok...) then I'd modify the previous example as follows:

Collection<String> foo = new Vector<String>();

f. Check that your new statement(s) compile correctly, adding any missing import
statements etc. as needed.

4. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.
At this point, your solution should match the Stage 3 UML diagram.

2.6 ConsoleUtils

At the end of Section 2.7.4, your solution should match the following Stage 4 UML
diagram:

In the passCard functionality we were working on earlier, the card selection was not
based on any user input. So, let's take a moment now to fix that:

1. Using ConsoleUtils.promptForExistingPlayer as a template, implement


ConsoleUtils.promptForIngredient (which takes the ingredients to
choose between as a parameter). Again, note that if ingredients has a length of
less than two, the method doesn't actually need to ask the user anything.
2. Modify BakeryDriver so that the code in your loop asks the player which card it is
that they want to pass to the other player.
3. Commit and push your code back to GitLab.
4. Now we're going to abstract over promptForExistingPlayer and
promptForIngredient . The method
ConsoleUtils.promptEnumerateCollection should allow users to select from
any provided Collection (like the list of players, or the ingredients in a player's hand).
Implement promptEnumerateCollection based on the two previously discussed
methods.
5. Modify promptForExistingPlayer and promptForIngredient so that they
don't loop through the parameters themselves, but instead call
promptEnumerateCollection with appropriate parameters.
6. Add an implementation for ConsoleUtils.promptForCustomer , which should
also call promptEnumerateCollection .
7. There's just one more method remaining in ConsoleUtils . The method
promptForAction first requires the ActionType type to be defined. Using the
experience you have from the CustomerOrderStatus type, implement the
ActionType enum.
8. An initial implementation of promptForAction could allow users to choose
between all actions, code the method in ConsoleUtils accordingly. Note that in
future, you will need to come back and restrict the set of actions so that the user isn't
presented with the option of an action they can't take (e.g. allowing them to choose
PASS_INGREDIENT when their hand is empty).
9. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.

2.7 Ingredient and Layer card operations

You should now have a game where players can take turns, passing cards between
themselves. Let's make things more interesting by allowing them to draw from the pantry and
bake their first layers.

2.7.1 The Pantry

At the start of the game, cards are dealt from the pantryDeck into the pantry .

1. Modify your MagicBakery constructor so that its parameter seed is used to


create a new instance of java.util.Random . This will be used to ensure
predictable shuffling (this allows us to test that you're putting things in the right places in
data structures and shuffling them appropriately).
2. Add the pantry and pantryDiscard attributes, and the getPantry method.
3. Implement the MagicBakery.drawFromPantryDeck method to move a card from
the pantryDeck to the pantry (does not count as an action).
4. Modify MagicBakery.startGame so that it:

Adds players to the game


Shuffles the pantryDeck
Call drawFromPantryDeck to move cards from the pantryDeck to the
pantry
Deals each player cards from the pantryDeck
Sets whatever state is needed to determine the current player

You'll have some of this code already, it won't all be new functionality.

5. Implement the MagicBakery.refreshPantry method (counts as an action).

6. Implement the two drawFromPantry methods (counts as an action).

7. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.
2.7.2 Layers

When playing the card game, all layer cards appear face up in piles. Specifically, all the
biscuit cards are in one pile, the sponges in another etc. This means that the different layers
are all visible at any time so that players can see what they can bake and the recipes they
need to follow. Implement MagicBakery.getLayers .

2.7.3 Baking

When baking a layer, a user must exchange all the ingredients that make up the recipe (or
their duck substitutes) for a Layer card. Cards exchanged by the user will go into the
pantryDiscard pile.

1. Start by implementing getBakeableLayers . This method needs to return all layers


that can be baked given a particular set of ingredients. For example, if I can make Jam
with Sugar and Fruit, and I can make Icing with Sugar and Butter, then with Sugar, Fruit
and Butter I can make Icing or Jam. Calling getBakeableLayers with Sugar, Fruit
and Butter should therefore return a collection containing Icing and Jam.
2. Implement bakeLayer (counts as an action).
3. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.

2.7.4 Interactivity

To play the game, users need to be able to see what ingredients are in the pantry, and to
read the recipes of each layer. The StringUtils class provides methods to help with
this.

1. The method MagicBakery.printGameState uses the StringUtils methods


to show the current cards visible in the game: the layers, the ingredients, the customer
orders, and the current player's hand. At this point, we can do all of this apart from
customer orders. Implement printGameState so that it prints the return values
from calls to the StringUtils methods: layersToStrings and
ingredientsToStrings .
2. Modify BakeryDriver so that each player's turn begins with them seeing the game
state before being asked which action they want to take. Call the relevant game action
method based on their choice. Keep asking them for actions and calling the relevant
method until they run out of actions. If the game state changes (e.g. the player
refreshes the pantry), you may want to print the new state (or at least the bit that has
changed). Once the player has used all their actions it should change to the next player
(and after all players have played their turns, it should change to the next round).
3. Commit and push your code back to GitLab. Check the state of the JUnit tests locally
and on Gradescope.
At this point, your solution should match the Stage 4 UML diagram.

2.8 Customers and CustomerOrders / Serialisation

At this point, you're most of the way done. We're pretty confident you can figure out the rest
on your own, but...

3. Need More Help?

Don't forget to run the tests at regular intervals. If you're not sure why a test is failing, take a
look at the stack trace. If that's not helping, you can also look at the source code for the
relevant test.

You can also get help prior to submission by asking questions in the lab drop-ins or online
forums.

You might also like