Skip to content
Part IA Michaelmas Term

2023 Paper 1 Question 3 — Variance, Cohesion/Coupling, Observer and Strategy

Question

(a) This question covers the concept of variance in Java.

  • (i) Explain what is meant by Java arrays being covariant. [2 marks]
  • (ii) Provide a code example which type checks at compile time but yields a runtime exception due to covariant arrays. [2 marks]
  • (iii) Explain what is meant by Java generics being invariant and how it contrasts to the previous example. [2 marks]

(b) Explain the notion of cohesion in the context of classes. What is meant by high cohesion and low cohesion? Why is high cohesion desirable? [2 marks]

(c) Explain the notion of coupling in the context of classes. What is meant by high coupling and loose coupling? Why is low coupling desirable? [2 marks]

(d) Suppose we have a stock trading application that needs to notify users of changes in stock prices but allows users to choose how they want to be notified (e.g. via email or text message).

  • (i) Explain the intent and relevance of the observer design pattern for this problem. [1 mark]
  • (ii) Explain the intent and relevance of the strategy design pattern for this problem. [1 mark]
  • (iii) Draw a UML diagram describing the key components of the observer design pattern. You do not need to recall the exact UML specification, instead you may provide a key explaining your notation. [2 marks]
  • (iv) Provide a skeleton of Java code for the subject as a class called Stock. Detail any methods required to notify and manage the users as well as an example of setting up at least two notification strategies for two different users. Explain any assumptions and tradeoffs of your approach. [6 marks]

Worked Solution

(a)(i) Java arrays are covariant — 2 marks

Model answer: Covariance means that if B is a subtype of A, then B[] is a subtype of A[]. In Java, Integer[] is a subtype of Number[], and String[] is a subtype of Object[]. This means you can assign an Integer[] to a Number[] variable:

Integer[] ints = {1, 2, 3};
Number[] nums = ints;           // valid — arrays are covariant

Why 2 marks: Define the subtype relationship clearly with a concrete example. Distinguish from invariance (where List<Integer> is NOT a subtype of List<Number>).

(a)(ii) Covariant arrays causing runtime exception — 2 marks

Model answer:

Integer[] ints = {1, 2, 3};
Number[] nums = ints;           // compiles fine (covariance)
nums[0] = 3.14;                 // compiles fine (Double is a Number)
                                 // THROWS ArrayStoreException at runtime!

The problem: nums is typed as Number[] at compile time, so storing a Double appears legal. But at runtime, the array’s actual type is Integer[], and the JVM checks every array store — storing a Double into an Integer[] fails with ArrayStoreException. The compile-time type system did not protect us; the error surfaces only at runtime.

Why 2 marks: Show both the compile-time acceptance and the runtime exception. The key point is that covariant arrays need runtime type checks on every store, which is both a performance cost and a potential source of runtime errors.

(a)(iii) Generics are invariant — 2 marks

Model answer: Invariance means List<Integer> is NOT a subtype of List<Number>, even though Integer is a subtype of Number. The compiler rejects the assignment:

List<Integer> ints = new ArrayList<>();
List<Number> nums = ints;       // COMPILE ERROR: incompatible types

This contrasts with arrays: the covariant array assignment compiles but fails at runtime; the invariant generic assignment fails at compile time. Generics give compile-time safety at the cost of less flexibility — you must use wildcards (List<? extends Number>) to express the subtype relationship explicitly.

Why generics chose invariance: If generics were covariant, you could add a Double to a List<Integer>, producing a heap pollution error that would manifest much later (potentially in unrelated code). The designers chose invariance to catch this class of errors at compile time.

(b) Cohesion — 2 marks

Model answer: Cohesion measures how closely related the responsibilities within a single class are.

High cohesion: All methods and fields serve a single, well-defined purpose. Example: a BankAccount class with deposit(), withdraw(), and getBalance() — everything relates to the account’s balance. High cohesion means the class is focused, easy to understand, and has few reasons to change.

Low cohesion: A class does many unrelated things. Example: a BankAccount class that also formats HTML reports, sends emails, and connects to a database. Low cohesion means the class is confusing, hard to maintain, and violates SRP.

Why high cohesion is desirable: Changes are localised — modifying one piece of functionality does not risk breaking unrelated functionality. Classes are easier to understand, test, and reuse.

(c) Coupling — 2 marks

Model answer: Coupling measures the degree of interdependence between classes.

High (tight) coupling: Classes know about each other’s internal implementation details. Changing one class forces changes in many others. Example: a Customer class directly accessing another class’s private fields (if that were possible) or depending on specific concrete subclasses rather than interfaces.

Loose (low) coupling: Classes interact through well-defined interfaces and know as little as possible about each other. Example: a ReportGenerator depends on a DataProvider interface, not on a specific database implementation. Loose coupling means components can be developed, tested, and modified independently.

Why low coupling is desirable: Changes are isolated — modifying class A does not ripple through to class B, C, and D. The system is more modular, testable, and maintainable.

(d)(i) Observer pattern intent and relevance — 1 mark

Model answer: The Observer pattern defines a one-to-many dependency: when the Stock subject’s price changes, all registered observers (users) are automatically notified. The subject does not need to know the concrete types of its observers — it only depends on the Observer interface.

Relevance to the stock application: Stock prices change frequently. Multiple users watching the same stock need to know about price changes. Observer lets the Stock notify all registered watchers with a single notifyObservers() call, without the Stock class knowing whether users want email, SMS, or push notifications.

(d)(ii) Strategy pattern intent and relevance — 1 mark

Model answer: The Strategy pattern defines a family of interchangeable algorithms, encapsulated behind a common interface, letting the client choose which algorithm to use at runtime.

Relevance to the stock application: Users choose how they want to be notified — email, SMS, push notification. Each notification channel is a strategy. The user’s observer can be configured with a NotificationStrategy (e.g., new EmailNotification(userEmail)), and when the observer receives a price update, it delegates to the strategy to deliver the notification through the chosen channel.

Key insight: The two patterns work together here — Observer handles when to notify (on price change), and Strategy handles how to notify (the notification channel). This is a classic combination.

(d)(iii) UML diagram — 2 marks

KEY:
  +-------------+
  |  ClassName  |   Box = Class or Interface
  |  (italic)   |   Italic = Abstract / Interface
  |----+--------|
  | + field      |   + = public, - = private
  | + method()   |
  +-------------+

  solid arrow with hollow triangle = "extends" (inheritance)
  dashed arrow with hollow triangle = "implements" (interface)
  solid line with open arrow = "uses/knows about" (association)
  ◇─ = aggregation (has-a, lifecycle independent)

+------------------+                  +-------------------+
|    «interface»   |                  |    «interface»    |
|     Observer     |<-----------------| NotificationStrategy |
|------------------+  observers       +-------------------+
| + update(price)  |  use strategy    | + send(msg, user) |
+------------------+  to deliver      +-------------------+
         ^                                        ^
         |                                        |
         | implements                             | implements
         |                                        |
+--------+--------+                     +---------+---------+
|  UserObserver   |                     |  EmailStrategy    |  |  SMSStrategy      |
|-----------------+                     |-------------------|
| - user : User   |                     | + send(msg, user) |
| - strategy :    |                     +-------------------+
    NotificationStrategy               (sends via email)
|-----------------+
| + update(price) |
+-----------------+
         ^
         | uses
         |
+--------+--------+
|      Stock      |
|-----------------+
| - symbol        |
| - price         |
| - observers :   |
    List<Observer>
|-----------------+
| + attach(o)     |
| + detach(o)     |
| + notify()      |
| + setPrice(p)   |
+-----------------+

Why 2 marks: Clearly label the interfaces, concrete implementations, and relationships. Include a key. The diagram must show at minimum: Stock (subject with attach/detach/notify), Observer (interface with update), UserObserver (concrete observer), and NotificationStrategy as the strategy interface with at least two implementations.

(d)(iv) Java skeleton code — 6 marks

Model answer:

interface Observer {
    void onPriceChange(String symbol, double price);
}

interface NotificationStrategy {
    void send(String message, User user);
}

class EmailNotification implements NotificationStrategy {
    public void send(String message, User user) {
        System.out.println("Emailing " + user.getEmail() + ": " + message);
    }
}

class SMSNotification implements NotificationStrategy {
    public void send(String message, User user) {
        System.out.println("SMS to " + user.getPhone() + ": " + message);
    }
}

class User {
    private String name;
    private String email;
    private String phone;

    User(String name, String email, String phone) {
        this.name = name;
        this.email = email;
        this.phone = phone;
    }

    String getName() { return name; }
    String getEmail() { return email; }
    String getPhone() { return phone; }
}

class UserObserver implements Observer {
    private final User user;
    private final NotificationStrategy strategy;

    UserObserver(User user, NotificationStrategy strategy) {
        this.user = user;
        this.strategy = strategy;
    }

    public void onPriceChange(String symbol, double price) {
        String message = "Stock " + symbol + " is now £" + price;
        strategy.send(message, user);
    }
}

class Stock {
    private final String symbol;
    private double price;
    private final List<Observer> observers = new ArrayList<>();

    Stock(String symbol, double initialPrice) {
        this.symbol = symbol;
        this.price = initialPrice;
    }

    void attach(Observer o) {
        observers.add(o);
    }

    void detach(Observer o) {
        observers.remove(o);
    }

    private void notifyObservers() {
        for (Observer o : observers) {
            o.onPriceChange(symbol, price);
        }
    }

    void setPrice(double newPrice) {
        this.price = newPrice;
        notifyObservers();
    }
}

// Usage — setting up two notification strategies for two users
User alice = new User("Alice", "[email protected]", "07700 900001");
User bob = new User("Bob", "[email protected]", "07700 900002");

Stock apple = new Stock("AAPL", 150.0);
apple.attach(new UserObserver(alice, new EmailNotification()));
apple.attach(new UserObserver(bob, new SMSNotification()));

apple.setPrice(155.0);
// Output:
//   Emailing [email protected]: Stock AAPL is now £155.0
//   SMS to 07700 900002: Stock AAPL is now £155.0

Assumptions and tradeoffs (for the 6 marks):

  1. Push model — The Stock pushes the price with each notification. The observer receives the data whether it needs it or not. An alternative would be a pull model where the observer calls getPrice() on the subject. The push model is simpler but less flexible if observers need different data.

  2. Synchronous notification — Observers are notified in the same thread. If an observer’s notification strategy is slow (e.g., sending a real email takes seconds), the setPrice() call blocks. A production system would use asynchronous notification (e.g., posting to a message queue).

  3. Notification order is unspecified — Observers are iterated in insertion order (because ArrayList), but the interface contract should not guarantee this. If order matters, observers should carry a priority.

  4. Observer-Subject coupling — The Stock only depends on the Observer interface (loose coupling), satisfying OCP. New notification strategies can be added without modifying Stock.

  5. Memory leak risk — If observers are never detached, the Stock holds references to them, preventing GC. A production system should use weak references or require explicit unsubscription when users stop watching a stock.

  6. Thread safety — The observers list is not thread-safe. If attach/detach are called from different threads than notifyObservers, use CopyOnWriteArrayList or synchronisation.

Why 6 marks: Complete skeleton code with Stock, Observer, UserObserver, NotificationStrategy, and two strategies. Setup example showing different strategies for different users. Discuss at least 3 trade-offs or assumptions.