Programming lesson
Mastering Java MVC with Compose: Building a Munro Quiz App
Learn how to build a modular Java application using the MVC pattern, interfaces, and dependency injection by creating a Munro quiz app. Hands-on tutorial for COMPSCI5092.
Introduction: Why MVC and Composition Matter in Modern Java Development
In the world of software engineering, writing maintainable and testable code is a superpower. The Model-View-Controller (MVC) pattern is a cornerstone of professional development, and Java's strong typing makes it an excellent language to implement it. In this tutorial, we'll build a quiz app that tests your knowledge of the 282 Munros—Scotland's highest peaks—using a modular design. Think of it as assembling a team of specialists: a View handles the user interface, a Service fetches data, and a Controller orchestrates the logic. By composing objects with interfaces, you can swap implementations without breaking your code. This is exactly how modern apps like Instagram or Spotify manage features: they separate concerns so that changing a UI library or a data source doesn't require a full rewrite.
Understanding the MVC Architecture
MVC divides your application into three interconnected components:
- Model: Represents the data and business logic. In our case, the Munro data (name, height, location).
- View: Handles the user interface—displaying images, buttons, and text fields.
- Controller: Acts as the middleman, responding to user input and updating the model or view.
But wait—in the assignment, you have a Service class instead of a Model. That's because the data comes from a remote service (like an API). The Service is part of the model layer. This separation allows you to later replace the remote service with a local database without touching the UI code. It's like swapping the engine of a car without changing the steering wheel.
Setting Up Your Project with Java 17 and JavaFX
Before diving into code, ensure you have the right tools. You'll need Java 17 SDK, JavaFX SDK 17, Hamcrest 2.2, and JUnit. Follow the setup guide in your course notes (DevelopmentTools.pdf). Once you have a clean template project, you're ready. Open the app.properties file located in src/main/resources/properties. This file lets you choose different implementations. For example:
# Choose View: SimpleView or ImageView
View=SimpleView
# Choose Service: LocalService or RemoteService
Service=RemoteService
# Choose Controller: SimpleController or QuizController
Controller=SimpleControllerChange the Controller to QuizController and run the app. You'll see a random Munro image and a text field to guess its name. The app tells you if you're correct. This is your starting point.
Creating Interfaces for Flexibility
Interfaces define contracts. They specify what methods a class must implement without dictating how. In your project, you have interfaces for View, Service, and Controller. For instance:
public interface View {
void displayImage(Image image);
String getUserInput();
void showResult(boolean correct);
}
public interface Service {
Munro getRandomMunro();
Image fetchImage(Munro munro);
}
public interface Controller {
void start();
void handleGuess();
void nextMunro();
}Now, any class that implements View must provide these methods. This allows you to swap a JavaFX-based view with a console-based view without changing the rest of the code. It's like having a universal remote that works with different TV brands.
Composing Objects: The Dependency Injection Pattern
Composition means building complex objects by combining simpler ones. Instead of inheriting behavior, you pass dependencies (like Service and View) into the Controller via its constructor. This is called dependency injection (DI). Example:
public class QuizController implements Controller {
private final View view;
private final Service service;
private Munro currentMunro;
public QuizController(View view, Service service) {
this.view = view;
this.service = service;
}
@Override
public void start() {
currentMunro = service.getRandomMunro();
view.displayImage(service.fetchImage(currentMunro));
}
@Override
public void handleGuess() {
String guess = view.getUserInput();
boolean correct = guess.equalsIgnoreCase(currentMunro.getName());
view.showResult(correct);
}
}Notice that QuizController doesn't create its own View or Service. They are provided from outside. This makes testing easy: you can pass mock objects. For example, you can test the controller's logic without needing a real JavaFX window.
Implementing a Custom View with JavaFX
Let's create a simple ImageView class that displays the Munro picture and a text field. JavaFX makes this straightforward:
public class ImageView implements View {
private Stage stage;
private TextField inputField;
private Label resultLabel;
private ImageView imageView;
public ImageView() {
// Initialize JavaFX components
// In a real app, you'd load an FXML or build the UI programmatically
}
@Override
public void displayImage(Image image) {
imageView.setImage(image);
}
@Override
public String getUserInput() {
return inputField.getText();
}
@Override
public void showResult(boolean correct) {
resultLabel.setText(correct ? "Correct!" : "Wrong!");
}
}You can also implement a ConsoleView that uses System.out and Scanner. This is great for testing or when you don't need a GUI.
Working with Remote Services
The Service interface abstracts data retrieval. A RemoteService might fetch Munro data from an API like walkhighlands.co.uk. A LocalService could read from a file. Here's a skeleton:
public class RemoteService implements Service {
private HttpClient client;
public RemoteService() {
client = HttpClient.newHttpClient();
}
@Override
public Munro getRandomMunro() {
// Call API endpoint to get random Munro
// Parse JSON response
return new Munro("Ben Nevis", 1345, "Lochaber");
}
@Override
public Image fetchImage(Munro munro) {
// Fetch image from URL
return new Image(munro.getImageUrl());
}
}To switch between services, just change the app.properties file. No code changes needed.
Testing with JUnit and Hamcrest
Testing is crucial. Use JUnit 5 and Hamcrest matchers for expressive assertions. For example, test the controller's guess logic:
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
public class QuizControllerTest {
@Test
public void testCorrectGuess() {
// Arrange
View mockView = mock(View.class);
Service mockService = mock(Service.class);
when(mockService.getRandomMunro()).thenReturn(new Munro("Ben Nevis"));
QuizController controller = new QuizController(mockView, mockService);
when(mockView.getUserInput()).thenReturn("Ben Nevis");
// Act
controller.start();
controller.handleGuess();
// Assert
verify(mockView).showResult(true);
}
}Using mocks (like Mockito) isolates the controller from real dependencies. This is a professional practice.
Putting It All Together: The Property File as a Configuration Tool
The app.properties file acts as a simple IoC (Inversion of Control) container. Your main application reads this file and instantiates the appropriate classes. Example:
public class App {
public static void main(String[] args) {
Properties props = loadProperties();
View view = createView(props.getProperty("View"));
Service service = createService(props.getProperty("Service"));
Controller controller = createController(props.getProperty("Controller"), view, service);
controller.start();
}
}This is a lightweight alternative to frameworks like Spring. It teaches you the fundamentals of DI.
Extending the App: Adding a Quiz Timer (Like a Game Show)
To make it more engaging, add a timer feature. Create a TimedController that extends QuizController and uses a ScheduledExecutorService to limit guess time. This is similar to how quiz apps on TikTok or Instagram Stories work—they keep users engaged with time pressure. You can swap the controller in the property file to switch between quiz modes.
Documenting Your Code with Javadoc
Professional code includes documentation. Use Javadoc comments for classes and methods:
/**
* Controller for the Munro quiz game.
* Uses dependency injection for View and Service.
*/
public class QuizController implements Controller {
// ...
}Generate HTML docs with javadoc command. This is a requirement in COMPSCI5092.
Conclusion: From Munros to Modern Apps
By applying MVC, interfaces, and composition, you've built a flexible, testable application. These skills transfer directly to enterprise Java development, Android apps, and even game development. The next time you use an app like Strava (which tracks hill climbs) or a language-learning app like Duolingo, notice how they separate concerns. You're now equipped to design systems that can evolve without breaking. Happy coding, and may you conquer all 282 Munros—in code and in spirit!