Assignment 2: Unit Conversion, Recipe and Instruction Classes
Overview
In this assignment, you'll expand the CookYourBooks domain model by implementing unit conversion, recipe scaling, and the core recipe structure. Building on Assignment 1's Quantity and Ingredient hierarchies, you'll create a flexible conversion system that supports standard metric/imperial conversions, ingredient-specific density conversions (like "1 cup flour = 125 grams"), and custom "house" overrides.

The core challenge is designing how domain objects and the ConversionRegistry service work together to enable recipe transformations. The ConversionRegistry intelligently selects conversion rules based on priority (house > recipe > standard) and specificity (ingredient-specific > generic).
You'll implement the conversion service and the Recipe and Instruction domain classes. Critically, you must design the API for recipe transformations yourself:
- Should
Recipehave additional transformation methods? If so, what should their signatures and specifications be? - How do transformation operations maintain loose coupling with the service layer?
How you achieve this is up to you. Your design decisions will determine extensibility for future requirements like recipe export formats, display customizations, and bulk conversions.
Due: Thursday, January 29, 2026 at 11:59 PM Boston Time
Prerequisites: This assignment builds on the A1 solution (provided). You should be familiar with the Quantity, Ingredient, and Unit hierarchies from Assignment 1.
Learning Outcomes
By completing this assignment, you will demonstrate proficiency in:
- Designing for changeability by creating modular, loosely-coupled components (L7: Coupling and Cohesion)
- Applying information hiding to encapsulate design decisions that are likely to change (L6: Modularity and Information Hiding)
- Implementing immutable transformations that return new objects rather than mutating existing ones
- Designing method specifications with clear preconditions and postconditions (L4: Specifications and Contracts)
- Implementing
equals()andhashCode()correctly for value objects
AI Policy for This Assignment
AI coding assistants (such as GitHub Copilot, ChatGPT, Claude, etc.) should NOT be used for this assignment.
This assignment focuses on design decisions that require understanding tradeoffs—something that benefits from working through the problem yourself. You may:
- Use official Java documentation
- Consult your textbook and course materials
- Ask questions in office hours or on the course discussion board
- Discuss high-level approaches with classmates (but write your own code)
Report any AI usage in the Reflection section.
Grading Infrastructure Security
Your code executes in a containerized environment with filesystem and network access. Do not attempt to access, exfiltrate, or reverse-engineer grading infrastructure, instructor test suites, or other non-distributed course materials. All submissions are recorded in an immutable audit trail, and we have automated tooling to detect such attempts. Violations will be referred to OSCCR. See the syllabus for full details.
If something seems wrong with the autograder, ask us—don't try to debug it yourself by inspecting the grading environment.
Technical Specifications
Package Organization
This assignment uses a package structure that organizes classes by responsibility:
src/main/java/app/cookyourbooks/
├── model/ # Core domain entities (A1 classes + new A2 classes)
│ ├── Quantity.java # FROM A1 - fully implemented
│ ├── ExactQuantity.java # FROM A1 - fully implemented
│ ├── FractionalQuantity.java # FROM A1 - fully implemented
│ ├── RangeQuantity.java # FROM A1 - fully implemented
│ ├── Unit.java # FROM A1 - fully implemented
│ ├── UnitSystem.java # FROM A1 - fully implemented
│ ├── UnitDimension.java # FROM A1 - fully implemented
│ ├── Ingredient.java # PROVIDED - fully implemented
│ ├── MeasuredIngredient.java # PROVIDED - fully implemented
│ ├── VagueIngredient.java # PROVIDED - fully implemented
│ ├── IngredientRef.java # PROVIDED - mostly complete (record)
│ ├── Instruction.java # STUB - implement this
│ └── Recipe.java # STUB - implement this
├── conversion/ # Unit conversion logic
│ ├── ConversionRule.java # STUB - implement this (record)
│ ├── ConversionRulePriority.java # PROVIDED - fully implemented (enum)
│ ├── ConversionRegistry.java # PROVIDED - interface definition only
│ ├── LayeredConversionRegistry.java # STUB - implement this
│ └── StandardConversions.java # PROVIDED - fully implemented
└── exception/ # Custom exceptions
└── UnsupportedConversionException.java # PROVIDED - fully implemented
Starter Code: All classes exist with proper Javadoc and method signatures. Classes marked STUB have method bodies that throw UnsupportedOperationException—you must implement them. Classes marked PROVIDED or FROM A1 are fully functional.
Domain Concepts
Unit Conversion
Unit conversion in cooking is more complex than simple mathematical ratios. Consider these scenarios:
- Standard conversions follow fixed ratios: 1 cup = 236.588 mL, 1 pound = 453.592 grams
- Ingredient-specific conversions account for density: 1 cup of flour ≠ 1 cup of honey in weight
- House overrides reflect personal preferences or equipment: "In my kitchen, 1 oz = 30 mL" (rounded for convenience)
Your conversion system must support all three, with this priority order (highest to lowest):
- House conversions - User-defined overrides that always take precedence
- Recipe-specific conversions - Conversions defined within a particular recipe
- Global conversions - Standard conversions available to all recipes
Within a priority level, prefer more specific rules (those that specify an ingredient name) over generic rules (those that do not specify an ingredient name). If multiple rules with the same priority level apply, prefer the rule that was added first.
Conversions may span different dimensions when appropriate ingredient context is provided:
- Volume ↔ Volume (cups to mL): Always possible within same dimension
- Weight ↔ Weight (oz to grams): Always possible within same dimension
- Volume ↔ Weight (cups to grams): Requires ingredient-specific density information (e.g., "1 liter of water = 1 kilogram of water")
- Count ↔ Weight (whole eggs to grams): Requires ingredient-specific information
Impossible conversions (e.g., weight to volume without density information) should throw an UnsupportedConversionException (a checked exception provided in the handout).
Recipe Transformations
Your implementation must support three types of recipe transformations (defined as methods on the Recipe class):
-
Scale by multiplier: Scale all
MeasuredIngredientquantities by a factor (e.g., 2x doubles everything).VagueIngredients remain unchanged. Servings (if present) should also scale. -
Scale to ingredient target: Scale a recipe as defined above, but choosing a scaling factor such that a specific ingredient reaches a target amount.
- Example: Recipe has "2 cups flour". Scaling to "500g flour" requires:
- Converting the ingredient's current quantity (2 cups) to the target unit (grams) using a density conversion rule: 2 cups × 125 g/cup = 250g
- Calculating the scale factor: 500g / 250g = 2.0
- Scaling the entire recipe by that factor
- The target ingredient ends up in the target unit (grams in the example above)
- Other ingredients that can be converted to the target unit should also be converted to the target unit as well
- Must handle cross-dimension conversions (cups ↔ grams) when conversion rules exist
- Must use recipe-specific conversion rules at
RECIPEpriority
- Example: Recipe has "2 cups flour". Scaling to "500g flour" requires:
-
Convert to unit: Convert all
MeasuredIngredientquantities to a target unit.VagueIngredients remain unchanged. Servings are never converted.- Must automatically use recipe-specific conversion rules at
RECIPEpriority - Should throw
UnsupportedConversionExceptionif anyMeasuredIngredientcannot be converted
- Must automatically use recipe-specific conversion rules at
Design considerations:
- All transformations must maintain immutability (return new objects)
- Transformations must also update any
IngredientRefs in instructions - Quantity type behavior:
RangeQuantitystaysRangeQuantity, fractional quantities becomeExactQuantity
Service Interface: ConversionRegistry
The ConversionRegistry interface (provided) defines the contract for the complex conversion service. You must implement it in a class called LayeredConversionRegistry.
Key responsibilities:
- Convert quantities between units using prioritized rules
- Support ingredient-specific conversions (e.g., "1 cup flour" → grams using flour density)
- Maintain immutability (adding rules returns a new registry)
- Search rules in priority order: HOUSE → RECIPE → STANDARD
- Within each priority level, prefer ingredient-specific rules over generic rules
Why this is the service layer:
- This separation enables testing conversion logic independently from domain objects, and provides a reasonable design space for you to work within.
- Future requirements (batch conversions, export to different systems, UI preferences) might benefit from this service
Your implementation (LayeredConversionRegistry) must handle:
- Rule storage organized by priority level
- Rule matching that respects both priority and specificity
- Conversion execution that throws appropriate exceptions when conversions fail
- Immutable operations where each
withRule/withRulescreates a new registry
See the full interface documentation in the source code. The interface is already provided—you implement the class.
Class Design
You must implement the following classes. Classes from A1 are shown in gray for context.
Implementation Details
Read the code! The starter code includes complete Javadoc and method signatures for all classes. The specifications below provide high-level context, but you should read the source files for detailed contracts, preconditions, and postconditions.
Key Design Decisions
The Recipe class defines transformation methods (scale(), scaleToTarget(), convert()) that you must implement. As you implement these methods and the supporting classes, consider these design principles:
-
Transformation API Design:
- Why are transformation methods on
Reciperather than in a separate service class? - Why do these methods take a
ConversionRegistryparameter rather than creating one internally? - How does this design maintain loose coupling with the conversion service?
- Why are transformation methods on
-
Where should transformation logic live?
- Should
MeasuredIngredienthave helper methods for creating transformed copies? (e.g.,withScaledQuantity(),withConvertedQuantity()?) - Should
Quantityhave convenience methods for scaling? (e.g.,scale(factor)that returns a new quantity?) - How do you iterate over ingredients and create new transformed objects while maintaining separation from the conversion service?
- What's the right balance between putting logic in domain objects vs. keeping them as simple data holders?
- Adding well-designed helper methods can improve your design—but choose wisely!
- Should
-
What visibility should your methods have?
- Which if any new transformation methods should be
public(part of the API)? - Which helper methods should be
private(internal implementation)? - Method visibility choices reveal your understanding of information hiding
- Which if any new transformation methods should be
-
How does
LayeredConversionRegistryorganize rules internally?- What data structure efficiently supports priority-based search?
- How do you maintain immutability while enabling rule additions?
- How do you handle the "first added takes precedence" requirement at each priority level?
-
Immutability:
- How do the transformation methods ensure full immutability?
- How do they create new recipes, ingredients, instructions, and ingredient refs?
- Why is defensive copying important throughout?
These design decisions will be reflected in your code structure and discussed in your reflection. There is no single "correct" design—what matters is that your design:
- Maintains good separation of concerns
- Supports the required functionality
- Follows the design constraints (no breaking changes)
- Can articulate tradeoffs in your reflection
Most importantly: Your choices about what public methods to add and where to add them will be the primary focus of design quality evaluation.
equals() and hashCode() Requirements
Implement equals() and hashCode() for the following classes:
Instruction: Equal if same step number, text, and referenced ingredientsRecipe: Equal if same title, servings, ingredients (in order), instructions (in order), and conversion rules (in order)
Note: ConversionRule is a Java record and automatically generates correct equals() and hashCode(). The provided A1 solution includes equals() and hashCode() for the Quantity subclasses. The Ingredient hierarchy (Ingredient, MeasuredIngredient, VagueIngredient) is fully provided in the A2 starter code, including equals() and hashCode().
Design Requirements
- Immutability: All domain objects (
Recipe,Instruction,Quantitysubclasses) must be immutable. Transformation methods return new objects. - Information hiding: Internal representation of conversion rules, ingredient references, etc. should not be exposed through the API
- Defensive copying: Getters returning collections must return unmodifiable views or copies
- Null safety: Use
@NonNulland@Nullableannotations from JSpecify to document nullability (we provide package-level default NullMarked annotation). You do not need to add runtime null checks for@NonNullparameters—the annotations serve as documentation and enable static analysis tools. - Documentation: Javadoc for all public classes, methods, constructors with
@param,@return,@throwstags. Use good specifications that demonstrate restrictiveness, generality, and clarity.
Design Constraints
What you CAN do:
- ✅ Add new public methods to domain classes (
Recipe,MeasuredIngredient,Quantity, etc.) - ✅ Add new private methods and fields to domain classes
- ✅ Create new classes in the
model,conversion, or other packages - ✅ Create new interfaces if your design requires them
- ✅ Add helper/utility classes for transformation logic
What you CANNOT do:
- ❌ Modify existing method signatures in provided classes (changing parameters, return types, or throws clauses)
- ❌ Modify the
ConversionRegistryinterface (you implement it, but cannot change it) - ❌ Remove or rename existing methods from provided classes
- ❌ Change existing constructors in provided stub classes
- ❌ Modify provided interfaces (
ConversionRegistry) or enums (ConversionRulePriority)
Suggested Implementation Order
The starter code provides stubs for all classes that compile but throw UnsupportedOperationException. This allows you to implement incrementally while keeping the project in a compilable state. The Recipe transformation method signatures (scale(), scaleToTarget(), convert()) are defined in the stub—you must implement them.
Phase 1: Foundation Classes (Model Layer)
Complete these first—they have no dependencies on other A2 classes:
IngredientRef(record) — Already mostly completeInstructionRecipe(basic getters andequals/hashCodeonly—no transformation methods yet - think about transformation API design as described in Phase 3)
We provide sample tests for Instruction (only toString() tests are included in the handout). The autograder runs additional comprehensive tests for Instruction and IngredientRef that are not included in the handout. You are welcome to add your own tests for these classes if you find them helpful, but they are not graded. For Recipe, we provide starter tests including a few for scaling and conversion (see Testing Requirements below).
Run tests as you go: ./gradlew test --tests "InstructionTest" and ./gradlew test --tests "RecipeTest" to check your progress on the foundation classes.
Phase 2: Conversion Rules
Autograder note: For this and the remaining tasks on this assignment, the autograder is configured not to provide feedback on your implementation until you provide a plausible test suite for that functionality (as you experienced with Assignment 1).
ConversionRule(record)- Implement compact constructor validation (factor > 0, nulls)
- Implement
canConvert()— check units match and ingredient matches (case-insensitive) - Hold off on
convert()until you make a conscious design decision about how to implement it
Run tests as you go: ./gradlew test --tests "ConversionRuleTest" to verify your ConversionRule implementation. We provide complete tests for ConversionRule—the autograder runs these to verify your implementation.
Phase 3: Design Your Transformation API
Now that you understand the domain from implementing the foundation classes and ConversionRule, design how recipe transformations will work:
- Plan Your Approach
- Review the three required transformation types (see Recipe Transformations)
- Decide: Where do transformation methods go? What are their signatures?
- Consider: What helper methods might be needed?
- Sketch out your design before coding
Phase 4: Implement Scaling
Implement and test scaling separately from unit conversion—you can earn credit for scaling even if conversion isn't working:
- Implement and test scaling by Multiplier
- Scale all
MeasuredIngredientquantities by a factor VagueIngredients remain unchanged- Servings (if present) should also scale
- Ingredient references in instructions must also be transformed
- Enhance the starter tests in
RecipeTest.javafor scaling behavior
- Scale all
Phase 5: Implement Conversion
LayeredConversionRegistry(implementsConversionRegistry)- Start with
withRule()andwithRules()to build the rule collection - Then implement
convert()methods with priority ordering - Test through the
ConversionRegistryinterface (priority ordering, specificity, exceptions)
- Start with
Write and run tests as you go: ./gradlew test --tests "ConversionRegistryTest" to verify your registry implementation handles priority ordering, ingredient-specific rules, and edge cases correctly. We provide a few tests for ConversionRegistry in the handout, and you must enhance these tests as you go. The handout has a hint of the first test you'll need to write in order to unlock feedback on your implementation.
-
Implement Scaling to Ingredient Target
- Calculate the scale factor needed for a target ingredient to reach a specific amount
- Handle edge cases: ingredient not found, ingredient appears multiple times
- This requires using the
ConversionRegistryto convert between units when needed - Enhance the starter tests in
RecipeTest.javafor target scaling
-
Implement Recipe Unit Conversion
- Convert all
MeasuredIngredientquantities to a target unit - Must delegate to
ConversionRegistry.convert()for each ingredient - Recipe-specific conversion rules should be used at
RECIPEpriority - Throw
UnsupportedConversionExceptionif any conversion fails - Enhance the starter tests in
RecipeTest.javafor recipe unit conversion behavior
- Convert all
Already Provided (No Implementation Needed)
The following are fully implemented in the starter code:
Ingredient,MeasuredIngredient,VagueIngredient— Complete Ingredient hierarchyConversionRulePriority— Enum withHOUSE,RECIPE,STANDARDStandardConversions— Pre-computed conversion rules for all within-dimension unit pairsUnsupportedConversionException— Checked exception with static factory methods
Testing Requirements
Your tests should verify both individual components and the service interface (ConversionRegistry) which is the primary focus of this assignment.
Focus on testing behavior and requirements, not specific method signatures.
Provided Test Files
We provide starter tests for the foundation classes and conversion components. Run these tests as you implement each class to verify your progress:
ConversionRuleTest.java— complete tests forConversionRulerecord (constructor validation,canConvert(),convert(), equality). The autograder runs these same tests.InstructionTest.java— sample tests forInstructionclass (onlytoString()tests are included). The autograder runs additional comprehensive tests not included in the handout. You are welcome to add your own tests for these classes if you find them helpful, but they are not graded.RecipeTest.java— starter tests forRecipeclass, including a few tests forscale(),scaleToTarget(), andconvert(). You must enhance these tests.ConversionRegistryTest.java— starter tests for theConversionRegistryinterface. You must enhance these tests.
Run individual tests with ./gradlew test --tests "TestClassName" or run all provided tests with ./gradlew test.
Required Test Files
You must enhance tests in the following files:
-
RecipeTest.java- Enhance the starter tests for thescale(),scaleToTarget(), andconvert()methods. Graded for fault-finding (20 points). Your tests should verify:- Scaling by factor works correctly for
MeasuredIngredients VagueIngredients remain unchanged when scaling or converting- Servings are scaled appropriately
IngredientRefs in instructions are updatedscaleToTarget()finds the correct ingredient and calculates the right factorconvert()converts allMeasuredIngredientquantities to the target unit- Recipe-specific conversion rules are used at
RECIPEpriority - Edge cases like ingredient not found, unsupported conversions
- Scaling by factor works correctly for
-
ConversionRegistryTest.java- Enhance the starter tests for theConversionRegistryinterface. Graded for fault-finding (20 points). Your tests must use only theConversionRegistryinterface methods—do not test implementation-specific details ofLayeredConversionRegistry. Your tests should cover:convert(Quantity, Unit)— generic conversions without ingredient contextconvert(Quantity, Unit, String)— ingredient-specific conversions
For both methods, verify priority ordering (HOUSE > RECIPE > STANDARD), specificity handling (ingredient-specific > generic at same priority), correct conversion mechanics, and exception handling.
As on assignment 1, we will grade your test cases based on their ability to find faults in our own implementations. Your tests must not depend on any of your own implementation details. The tests must utilize only the public APIs as provided in the assignment handout.
Reflection
Update REFLECTION.md to address:
-
API Design & Coupling: Why are transformation methods on
Reciperather than in a separate service? What type of coupling exists betweenRecipeandConversionRegistry, and how does the design keep it loose? -
Responsibility Assignment: Where did you put the logic for transforming ingredients? Did you add helper methods to domain classes? How did you decide what belongs where?
-
Information Hiding: What design decisions are hidden behind the
ConversionRegistryinterface? What could change inLayeredConversionRegistrywithout affecting code that uses the interface? -
Immutability: What are the benefits and costs of requiring immutable
Recipeobjects? How did immutability affect your transformation implementation? -
Extensibility: Pick one future requirement (e.g., bulk conversion to metric, export with unit preferences, a
RecipeBookclass). How well does your design support it? What would need to change? -
AI Usage: (Ungraded) Did you use AI assistance? If so, describe how and reflect on whether it helped or hindered your learning of design principles.
Quality Requirements
Your submission should demonstrate:
- Correctness: Code compiles, follows specifications, passes tests
- Design Quality: Appropriate use of interfaces, immutability, information hiding
- Testing: Meaningful tests that verify behavior and detect faults
- Documentation: Clear Javadoc with preconditions, postconditions, and design rationale
- Code Quality: Clean, readable code following course style conventions
Grading Rubric (100 points)
Automated Grading — Implementation Correctness (50 points)
Model Layer Foundation (10 points)
IngredientRef(2 points)Instruction(4 points)Recipebasic functionality (4 points) — constructor, getters, equals/hashCode, immutability
Conversion Components (16 points)
ConversionRule(6 points)LayeredConversionRegistry(10 points)
Recipe Transformations (24 points)
Recipe.scale()(12 points)Recipe.scaleToTarget()(7 points)Recipe.convert()(5 points)
Automated Grading — Test Quality (40 points)
Tests are graded on fault-finding ability against instructor mutants.
RecipeTest.java — Recipe Transformations (20 points)
scale()(8 points)scaleToTarget()(6 points)convert()(6 points)
ConversionRegistryTest.java — Conversion Service (20 points)
convert(Quantity, Unit)(10 points)convert(Quantity, Unit, String)(10 points)
Reflection (10 points)
- API Design & Coupling (2 points)
- Responsibility Assignment (2 points)
- Information Hiding (2 points)
- Immutability (2 points)
- Extensibility (2 points)
Manual Grading — Subtractive (max -20 points)
- Coupling & Separation of Concerns (max -6 points)
- Information Hiding (max -4 points)
- Immutability Violations (max -4 points)
- Documentation & Specifications (max -3 points)
- Code Quality (max -3 points)
Summary
| Category | Points |
|---|---|
| Implementation Correctness | 50 |
| Test Quality (fault-finding) | 40 |
| Reflection | 10 |
| Total | 100 |
| Manual Grading (subtractive) | up to -20 |
| Final Score Range | 80–100 |
Note: Students who pass all automated tests and write good reflections earn 100 points. Excellent design quality means no deductions (score stays at 100). Poor design choices result in deductions down to a minimum of 80 points.
Submission
Submit via Pawtograder (via GitHub). As with assignment 1, there is a limit of 15 submissions per-24-hour period. Submissions that receive a score of "0" will not count towards your limit.
Good luck! Remember: understanding the design decisions in the provided code and articulating tradeoffs in your reflection are necessary to receive full marks - not just passing the tests.