---
title: "Jam 05 - Exercise 3"
tags:
- 3 ๐งช in testing
- 4 ๐ฅณ done
- classes
- uml
- arraylist
- documentation
---
<!-- markdownlint-disable line-length single-h1 no-inline-html -->
<!-- markdownlint-configure-file { "ul-indent": { "indent": 4 }, "link-fragments": {"ignore_case": true} } -->
{%hackmd dJZ5TulxSDKme-3fSY4Lbw %}
# Exercise 3 - Working with Collections
## Overview - Exercise 3
In this exercise, you'll implement the `ProductCatalog` class that manages a collection of `Product` objects using ArrayList. This will teach you:
1. How to work with Java collections
2. Managing relationships between classes
3. Basic operations with ArrayList
4. Proper encapsulation of collections
:::info
๐ **Key Concepts**
- ArrayList basics and operations
- Collection management patterns
- Basic object lookup
- Defensive copying
- Test-driven development
:::
## Understanding ArrayList
We've talked about ArrayList in lecture and the next ZyBooks assignment (ZB11) will cover ArrayList in more detail. If you want to get ahead, you can complete ZyBooks section 7.17 which introduces ArrayList concepts. For now, let's quickly review some key ArrayList concepts before diving into implementation:
1. **What is ArrayList?**
- Dynamic-size array implementation
- Part of Java Collections Framework
- Grows/shrinks automatically
- Provides rich set of operations
2. **Common Operations**
```java
List<Product> products = new ArrayList<>(); // Create empty list
products.add(product); // Add item
products.remove(product); // Remove item
products.size(); // Get count
products.isEmpty(); // Check if empty
products.contains(product); // Check for item
```
3. **Important Considerations**
- Use generics (the diamond operator `<Product>`) to specify type (`ArrayList<Product>`)
- Return new copies of collections to prevent direct modification
- Consider null checks and validation
- Maintain proper encapsulation
## Implementing ProductCatalog
Let's implement `ProductCatalog` following our UML design and using the test-driven development approach:
1. Open the empty `ProductCatalog.java` file you've already created.
2. Using the UML diagram, add the instance field(s).
3. Add the constructor.
- You can use IntelliJ's generator again but check your UML diagram to make sure you're adding the correct parameter(s)!
- Think about what the Catalog is doing. It's managing a collection of products. What does that mean about the ArrayList of Products? You should have some code in the constructor. What do you think it should be?
Let's implement each method incrementally, guided by the tests in `ProductCatalogTest.java`.
### Step 1 - Basic Operations
Open up the `ProductCatalogTest.java` file and uncomment `testAddProduct`. Try to run it. What happens? Why?
Go back to your `ProductCatalog.java` file and add the missing methods.
Once those tests pass, uncomment `testCountAndEmpty` and try to run it. What happens? Why?
Go back and fix that.
:::info
๐ก **Hint:** Many of our methods mirror ArrayList's built-in methods. For example:
- `isEmpty()` โ `ArrayList.isEmpty()`
- `size()` โ `ArrayList.size()`
- `add()` โ `ArrayList.add()`
- `remove()` โ `ArrayList.remove()`
So our implementations can be very simple, often just delegating to the corresponding ArrayList method:
For example, our `isEmpty()` method could just be:
```java
public boolean isEmpty() {
return products.isEmpty();
}
```
:::
Continue the process of uncommenting a test and implementing the missing methods. You can use this in conjunction with your UML diagram to and pick which tests you want to work on next.
You are done when you pass all the original tests.
- There are 5 tests originaly in `ProductTest`
- There are 7 tests originaly in `ProductCatalogTest`
:::warning
๐จ **Important Collection Tips**
1. **Defensive Copying**
- Always return copies of collections
- Prevents external modification
- Maintains encapsulation
Example:
```java
// BAD - exposes internal list
return products;
// GOOD - returns copy
return new ArrayList<>(products);
```
2. **Null Handling**
- Check input parameters
- Document null behavior
- Use clear error messages
:::
๐ **Test-Driven Development vs Our Approach**
Test-Driven Development (TDD) follows a "Red-Green-Refactor" cycle, often visualized using traffic light colors:
๐ด **Red** - Write a failing test first
- Write a test for functionality that doesn't exist yet
- Run the test to see it fail (red)
- This verifies the test is actually testing something
๐ข **Green** - Write minimal code to pass
- Write just enough code to make the test pass
- Don't worry about elegance yet
- Get to green as quickly as possible
๐ต **Refactor** - Improve the code
- Clean up and optimize while keeping tests green
- Remove duplication
- Improve readability
- Maintain functionality
Our approach here is similar but slightly different:
1. Tests are already written for us
2. We uncomment tests one at a time (๐ด Red)
3. Implement code to make the test pass (๐ข Green)
4. Clean up our implementation if needed (๐ต Refactor)
5. Move on to the next test
While this isn't pure TDD, it still provides many of the same benefits:
- Tests guide our implementation
- We work in small, focused iterations
- We have confidence our code works
- The tests document expected behavior
The main difference is that in true TDD, you write the tests yourself, which helps you think through the design before implementation. This involves writing multiple test cases for each method that:
- Start with basic functionality tests
- Add edge cases and error conditions
- Refactor tests to be more comprehensive
- Consider different input combinations
- Test boundary conditions
For example, when testing a search method, you might:
1. First write a simple test for exact match (๐ด)
2. Implement basic matching (๐ข)
3. Refactor if needed (๐ต)
4. Add test for partial matches (๐ด)
5. Expand implementation (๐ข)
6. Refactor again (๐ต)
7. Continue with case sensitivity, null inputs, etc. You end up with a LOT of tests per method but they are thorough and can end up handling a lot of edge cases making your code very robust.
This iterative test writing process helps you discover edge cases and design better APIs before writing the implementation. Here, the tests serve more as pre-defined specifications and requirements.
### Last-Minute Requirement Change
The store manager just requested a new feature for the ProductCatalog - they need to be able to find all products within a specific price range. This is a common need when:
- Customers want to shop within their budget
- Running sales reports for different price tiers
- Planning promotional discounts
This is a great example of how requirements evolve during development. When requirements change, we need to:
1. **Update Documentation**
- Add new features to specifications
- Update UML diagrams to reflect changes
- Document new methods and parameters
- Update any affected documentation
2. **Add/Update Tests**
- Write tests for new functionality
- Update existing tests if behavior changes
- Consider edge cases and error conditions
- Ensure test coverage remains high
3. **Implement the Code**
- Add new methods/classes as needed
- Modify existing code carefully
- Keep changes focused and minimal
- Maintain existing functionality
This process helps ensure changes are:
- Well documented
- Thoroughly tested
- Properly implemented
- Don't break existing features
The test-driven approach is especially valuable when requirements change, as it helps us:
- Understand the new requirements clearly
- Catch issues early
- Maintain code quality
- Have confidence in changes
#### Update the UML diagram
First, lets think about the new method. What type of parameters do you think this method will have? What return type? What should the name of the method be? What should the name of the parameters be? What should the return type be?
We know that our requirements team already wrote a test for use and they called the method `findProductsInPriceRange`. Based on the requirements document, we know that the method will have two parameters: a minimum price and a maximum price (and you already know what type `price` is!). The return type will be a list of products (So literally List\<Products\>).
1. Go to draw.io and open the `Jam05-uml.drawio.png` file.
2. Add the new method `findProductsInPriceRange` to the UML diagram. Don't forget to add the parameters and return type!
3. You don't need to re-export the UML diagram at this time. We will work with this UML file again soon.
#### Update the Test
Good new! The requirements team gave you the test method that will test a `findProductsInPriceRange` method. Add this test to your `ProductCatalogTest.java` file!
```java
/**
* Test getting products in price range.
*/
@Test
void testGetProductsInPriceRange() {
// Add test products with various prices
Product p1 = new Product("Low", "SKU1", 5.99);
Product p2 = new Product("Mid", "SKU2", 15.99);
Product p3 = new Product("High", "SKU3", 25.99);
catalog.addProduct(p1);
catalog.addProduct(p2);
catalog.addProduct(p3);
// Test exact range match
List<Product> range = catalog.findProductsInPriceRange(15.99, 15.99);
assertEquals(1, range.size());
assertEquals(p2, range.getFirst());
// Test range with multiple products
range = catalog.findProductsInPriceRange(0, 20.00);
assertEquals(2, range.size());
assertTrue(range.contains(p1));
assertTrue(range.contains(p2));
// Test range with no products
range = catalog.findProductsInPriceRange(100, 200);
assertEquals(0, range.size());
// Test range boundaries
range = catalog.findProductsInPriceRange(5.99, 25.99);
assertEquals(3, range.size());
}
```
#### Implement the Method
Add the new method to ProductCatalog. Here is the start of it:
```java
/**
* Finds all products within the specified price range (inclusive).
*
* @param minPrice the minimum price (inclusive)
* @param maxPrice the maximum price (inclusive)
* @return a new list containing all products with prices between minPrice and maxPrice (inclusive)
*/
public List<Product> findProductsInPriceRange(double minPrice, double maxPrice)
```
> ๐ **Checkpoint**
>
> - `ProductCatalog.java` created with ArrayList field
> - Constructor properly initializes empty catalog
> - All basic operations implemented:
> - add/remove methods
> - size/isEmpty methods
> - find/search methods
> - Collections properly protected (defensive copying)
> - All ~~7~~ 8 ProductCatalog tests pass
> - All methods properly documented
## Save Your Work - Exercise 3
Verify what files are uncommitted:
```bash
git status
```
Stage your changes:
```bash
git add src/main/java/jam05/ProductCatalog.java src/test/java/jam05/ProductCatalogTest.java
```
Commit your work:
```bash
git commit -m "jam05: Implement ProductCatalog with ArrayList"
```
Your working directory should now be clean.
:::success
๐ **Key Takeaways**
- ArrayList provides flexible collection management
- Proper encapsulation protects internal state
- Test-driven development guides implementation
- Defensive copying maintains object integrity
- Clear documentation improves maintainability
:::
# Summary
Throughout this jam, you've built a strong foundation in object-oriented programming concepts:
1. **Class Design & UML**
- Created professional UML class diagrams
- Learned proper class design principles
- Mastered draw.io for documentation
- Understood relationships between classes
2. **Object-Oriented Programming**
- Implemented encapsulation with private fields and public methods
- Created constructors and getter methods
- Used toString for object representation
- Managed object relationships
3. **Collection Management**
- Worked with ArrayList for dynamic collections
- Implemented search and management operations
- Applied defensive copying principles
- Handled collection validation
4. **Professional Development**
- Used test-driven development
- Followed documentation best practices
- Applied proper error handling
- Created maintainable, well-structured code
## Looking Ahead
In the next jam, we'll build on these foundations as we explore:
- Inheritance and polymorphism
- Abstract classes and interfaces
- More complex object relationships
- Advanced collection operations
# Project Structure
The following new files should be in your repository:
```text
csci205_jams/
โโโ src/
โ โโโ main/
โ โ โโโ java/
โ โ โโโ jam05/
โ โ โโโ Product.java
โ โ โโโ ProductCatalog.java
โ โ โโโ Jam05-uml.drawio.png
โ โโโ test/
โ โโโ java/
โ โโโ jam05/
โ โโโ ProductTest.java
โ โโโ ProductCatalogTest.java
```
# Submission
Before submitting your work, complete this final checklist:
1. **Code Completeness**
- All required files are present and in correct locations:
- `Product.java` with working implementation
- `ProductCatalog.java` with collection management
- `Jam05-uml.drawio.png` UML diagram
- All test files in correct location
- All files compile without errors
- All programs run correctly
2. **Documentation**
- UML diagram is clear and complete
- All code follows course standards
3. **Testing**
- All test cases pass (5 Product tests, 8 ProductCatalog tests)
- Product class works correctly
- ProductCatalog manages collections properly
- New price range feature works as expected
4. **Version Control**
First, push your feature branch as a backup:
```bash
# Ensure all changes are committed
git status
# Push your jam05 branch to remote
git push origin jam05
```
Then, merge your work into main:
```bash
# Switch to main branch
git checkout main
# Merge your jam05 branch into main
git merge jam05
# Push main to remote
git push
```
:::warning
๐จ **Important Submission Notes**
1. Branch Management:
- Main branch should contain your final, working submission
- Keep your `jam05` branch until grading is complete
2. Repository Status:
- Both branches should be pushed to GitLab
- Working directory should be clean
- The instructors can only grade what's in your GitLab repository
:::
:::warning
๐ง **Important**
Always verify your work appears on [gitlab.bucknell.edu](https://gitlab.bucknell.edu/). The instructors and graders can only see and grade what you've properly pushed to your remote repository.
:::