Module 2: Class Design
Module Overview
Learn about principles of good class design and object-oriented programming.
Learning Objectives
- Apply encapsulation by using private instance variables and public methods
- Implement getter and setter methods to provide controlled access to instance variables
- Create JavaDoc comments to document class behavior
- Implement the ToString method to create a string representation of an object
- Create well-designed classes that balance cohesion and coupling
- Design classes that follow the single responsibility principle
- Implement validation logic in methods to maintain object consistency
Key Concepts Explained
Encapsulation
Encapsulation is one of the four pillars of OOP. It involves bundling data (attributes) and methods that operate on that data within a single unit (a class), and restricting direct access to some of the object's components.
public class Student {
// Private instance variables - encapsulated
private String name;
private double gpa;
// Public methods to access and modify the encapsulated data
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getGpa() {
return gpa;
}
public void setGpa(double gpa) {
// Validation to maintain consistency
if (gpa >= 0.0 && gpa <= 4.0) {
this.gpa = gpa;
} else {
throw new IllegalArgumentException("GPA must be between 0.0 and 4.0");
}
}
}
Single Responsibility Principle
A class should have only one reason to change, meaning it should have only one responsibility or job.
// Good: Each class has a single responsibility
public class Order {
private List- items;
private Customer customer;
// Order-related methods
}
public class OrderProcessor {
public void processOrder(Order order) {
// Logic for processing orders
}
}
public class OrderPrinter {
public void printOrder(Order order) {
// Logic for printing orders
}
}
JavaDoc Comments
JavaDoc is a documentation generator that extracts comments from Java source code and generates formatted HTML documentation.
/**
* Represents a bank account with basic operations like deposit and withdraw.
*
* @author Jane Doe
* @version 1.0
*/
public class BankAccount {
private double balance;
/**
* Deposits money into the account.
*
* @param amount The amount to deposit
* @return The new balance after deposit
* @throws IllegalArgumentException if amount is negative
*/
public double deposit(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Cannot deposit a negative amount");
}
balance += amount;
return balance;
}
}
ToString Method
The toString() method returns a string representation of an object and is automatically called when an object is printed.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
Design Principles
Balancing Cohesion and Coupling
Good class design aims for high cohesion (related functionality in one place) and low coupling (minimal dependencies between classes).
High Cohesion
A class should represent a single, well-defined concept with related methods and attributes.
// High cohesion - all methods relate to customer management
public class CustomerManager {
public void addCustomer(Customer customer) { /* ... */ }
public void removeCustomer(String customerId) { /* ... */ }
public Customer findCustomer(String customerId) { /* ... */ }
public void updateCustomerDetails(Customer customer) { /* ... */ }
}
Low Coupling
Classes should have minimal knowledge of or dependencies on other classes.
// Low coupling example using interfaces
public interface PaymentProcessor {
boolean processPayment(Payment payment);
}
public class CreditCardProcessor implements PaymentProcessor {
@Override
public boolean processPayment(Payment payment) {
// Credit card specific implementation
}
}
public class Order {
private PaymentProcessor paymentProcessor;
// Constructor injection creates loose coupling
public Order(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void checkout(Payment payment) {
// Order doesn't need to know which payment processor is used
boolean success = paymentProcessor.processPayment(payment);
// Process result
}
}
Validation in Methods
Methods should validate inputs to ensure object consistency and prevent invalid states.
public class Product {
private String name;
private double price;
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Product name cannot be empty");
}
this.name = name;
}
public void setPrice(double price) {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
this.price = price;
}
}
Additional Resources
Practice Exercises
Exercise 1: Library Management System
Design a set of classes for a library management system:
- Create a Book class with proper encapsulation and validation
- Create a Library class that manages a collection of books
- Implement methods for adding, removing, and finding books
- Document all classes and methods with JavaDoc
- Implement toString() methods for all classes
Exercise 2: E-commerce System
Design a simple e-commerce system with the following classes:
- Product class with proper validation
- ShoppingCart class to manage product collections
- Customer class with contact information
- Order class that links customers, carts, and payment information
- Ensure each class has a single responsibility
Guided Projects
Complete these exercises to reinforce your understanding of classes, objects, and access modifiers.
Mastery Task 5 - UML Diagraming
Mastery Task Guidelines
Mastery Tasks are opportunities to test your knowledge and understanding through code. When a mastery task is shown in a module, it means that we've covered all the concepts that you need to complete that task. You will want to make sure you finish them by the end of the week to stay on track and complete the unit.
Each mastery task must pass 100% of the automated tests and code styling checks to pass each unit. Your code must be your own. If you have any questions, feel free to reach out for support.
AppSettings Update
Go to com.adventure.settings.AppSettings and update the story to MT5_DigEscape.
UML
You will be implementing the following UML diagram: UML Diagram Link
Working Classes
We will be working with the following classes:
- Hole - adventure.world.objects.Hole
- HoleContent (new)
Turning UML into Code
The first task is to create the HoleContent object. You should be able to use the UML diagram to guide you in creating the class.
Note: the HoleContent file does not exist yet. You'll need to create it from scratch.
Using HoleContent in Hole
Once HoleContent is created, it's time to use it in the Hole object.
The Hole constructor receives a Key which should be used in creating a HoleContent object.
Inside of the Hole class, you should update each of its functions so that it uses HoleContent to set and return the correct values.
Testing
To run the tests for this assignment, run the following:
./gradlew test --tests com.adventure.MT5
or by right-clicking the MT5 file and selecting "Run 'MT5'"
You can check your code styling by running the linter with the following command:
./gradlew checkstyleMain
You can run the game by going to the Main class and clicking on the run icon to the left of the main() method.