Skip to content
Part IA Michaelmas Term

2021 Paper 1 Question 4 — OOP Principles and the Decorator Pattern

Question

A program which decrypts files under the Swap Encryption Scheme by swapping pairs of characters is given below. Some code has been omitted and you do not need to understand the operation of the algorithm.

class Swapper extends Reader {
    private final PushbackReader pushBack;
    Swapper(PushbackReader p) { pushBack = p; }
    @Override
    public int read(char[] cbuf, int off, int len) {
        int r = wrap.read(cbuf, off, len);
        if (r % 2 == 1) { pushBack.unread(cbuf, off + --r, 1); }
        for (int i = 0; i < r; i += 2) { swap(cbuf, i, i + 1); }
        return r;
    }
}

class Decryptor {
    static List<String> read(String fileName) {
        try (BufferedReader r = new BufferedReader(new Swapper(
            new PushbackReader(new FileReader(fileName))))) {
            return readLines(r);
        }
    }
}

(a) The four principles of object-oriented programming are encapsulation, abstraction, inheritance of code and polymorphism. Explain how the program above makes use of each of them, with reference to specific lines in the code. [2 marks each]

(b) The program attempts to use Swapper as part of the Decorator pattern. What changes would you make to improve the design? [2 marks]

(c) Explain how this program demonstrates the open-closed principle. [2 marks]

(d) How would you change this implementation to allow users to specify an arbitrary operation to apply to pairs of characters (rather than just swapping them)? [4 marks]

(e) Explain why this design does not satisfy the open/closed principle with respect to adding support for decrypting images. What are the implications of this for object-oriented program design? [4 marks]


Worked Solution

(a) Four principles — 2 marks each (8 marks total)

(i) Encapsulation

Model answer: The Swapper class encapsulates the swapping algorithm. The field pushBack is declared private, preventing external code from directly manipulating the pushback buffer (line 2). The Decryptor class encapsulates the file-reading logic — its read method hides the complexity of constructing the reader chain. The client calls Decryptor.read(fileName) without knowing about BufferedReader, PushbackReader, or Swapper (lines 15–18).

The internal state (pushBack) and the implementation details of how decryption works are hidden behind a clean public API (read(char[], int, int)).

Tripos trap: Encapsulation is not just “using private” — it is about hiding implementation details behind a stable interface. Make sure to reference the specific lines where this happens.

(ii) Abstraction

Model answer: Swapper extends Reader (line 1), which is an abstract class that provides a general contract for reading character streams. The Decryptor.read method line 16 wraps the concrete Swapper as a BufferedReader — the code works with the Reader abstraction, not the specific Swapper type. The readLines helper presumably works with a BufferedReader, never knowing it contains a Swapper inside.

The client code Decryptor.read("file.txt") works at a high level of abstraction — “decrypt this file” — without knowing about character arrays, offsets, lengths, or pushback buffers.

Tripos trap: Abstraction in the OOP “four principles” sense means inheritance of interface — using a general type (Reader) to refer to a specific implementation (Swapper). Do not confuse it with abstract classes specifically (though Reader happens to be abstract here).

(iii) Inheritance of code

Model answer: Swapper extends Reader (line 1) and inherits its concrete methods (e.g., read() single-character variant, skip(), ready(), close()). Swapper only overrides the read(char[], int, int) method (line 6) — all other Reader methods work out of the box without reimplementation. The @Override annotation confirms this is code reuse through inheritance.

Similarly, PushbackReader extends FilterReader which extends Reader, inheriting all the stream management machinery. The program builds on this inheritance hierarchy to reuse existing I/O infrastructure.

Tripos trap: This is “inheritance of code” (implementation), not “inheritance of interface”. The question deliberately lists them as separate principles. Swapper gets both: inherits code from Reader and inherits the Reader interface (which is an abstraction).

(iv) Polymorphism

Model answer: The BufferedReader wraps a Reader reference (line 15 — the constructor BufferedReader(Reader in) accepts any Reader). At runtime, when BufferedReader calls read() on its wrapped Reader, dynamic dispatch ensures the Swapper’s overridden read method executes (not the default Reader.read). This is runtime polymorphism — the same BufferedReader.read() call behaves differently depending on which concrete Reader subclass is wrapped inside.

The try-with-resources block (line 15) calls close() on BufferedReader, which calls close() on the wrapped Reader — again, dynamic dispatch routes to the correct implementation.

Tripos trap: For inheritance-based polymorphism, you must reference dynamic dispatch — the specific method that runs depends on the runtime type, not the compile-time type of the reference. The BufferedReader holds a Reader reference but the Swapper.read() method runs.

(b) Decorator pattern improvements — 2 marks

Model answer: The Swapper is set up to decorate a PushbackReader, which is not the standard Decorator approach. A proper Decorator should:

  1. Implement the same interface as the component it decoratesSwapper should extend Reader and wrap any Reader, not specifically a PushbackReader. Currently, the constructor takes PushbackReader p (line 3) — this couples Swapper to one specific concrete type, defeating the Decorator’s flexibility.

  2. Store a reference to the abstract component type — Change the constructor to Swapper(Reader r) and store a Reader reference. This way, Swapper can decorate any Reader (a FileReader, a StringReader, another Swapper, etc.).

class Swapper extends Reader {
    private final Reader inner;        // was: PushbackReader pushBack;

    Swapper(Reader inner) {            // was: Swapper(PushbackReader p)
        this.inner = inner;
    }

    @Override
    public int read(char[] cbuf, int off, int len) {
        int r = inner.read(cbuf, off, len);
        // ... swapping logic remains the same
    }
}

Tripos trap: The Decorator pattern’s defining feature is that decorators and the decorated component share the same interface/abstract class. The constructor should accept the interface type, not a concrete type. If Swapper only works with PushbackReader, it is not a true Decorator — it is just a subclass.

(c) Open-closed principle — 2 marks

Model answer: The program demonstrates OCP because new decryption algorithms can be added by creating new Reader subclasses without modifying existing code. Swapper extends Reader — you could write Shifter extends Reader, XORDecryptor extends Reader, etc. The Decryptor.read() method builds a chain of Reader decorators (line 15–16) — you can insert new processing steps into the chain by wrapping a new decorator around the existing chain, without modifying Swapper, Decryptor, or BufferedReader.

The key OCP insight: the class hierarchy is open for extension (new Reader subclasses) but closed for modification (the existing classes do not change when new functionality is added).

(d) Arbitrary operation on character pairs — 4 marks

Model answer: Use the Strategy pattern to parameterise the pair-wise operation:

interface PairOperation {
    void apply(char[] buf, int i, int j);
}

class Swapper extends Reader {
    private final Reader inner;
    private final PairOperation operation;

    Swapper(Reader inner, PairOperation operation) {
        this.inner = inner;
        this.operation = operation;
    }

    @Override
    public int read(char[] cbuf, int off, int len) {
        int r = inner.read(cbuf, off, len);
        for (int i = 0; i < r - 1; i += 2) {
            operation.apply(cbuf, off + i, off + i + 1);
        }
        return r;
    }
}

// Built-in swapping operation
class SwapOperation implements PairOperation {
    public void apply(char[] buf, int i, int j) {
        char tmp = buf[i];
        buf[i] = buf[j];
        buf[j] = tmp;
    }
}

// Alternative: XOR operation
class XOROperation implements PairOperation {
    public void apply(char[] buf, int i, int j) {
        buf[i] ^= buf[j];
        buf[j] ^= buf[i];
    }
}

Usage:

Reader decryptor = new Swapper(new FileReader("file.txt"), new SwapOperation());
Reader encryptor = new Swapper(new FileReader("file.txt"), new XOROperation());

Why this works: The PairOperation interface defines the variable behaviour. The Swapper is no longer coupled to swapping — it applies whatever operation it is given. New operations can be added by implementing PairOperation without touching Swapper (satisfying OCP). The Swapper class itself should be renamed to something like PairwiseTransformer to reflect its more general role.

Alternative approach (Strategy via lambda / functional interface):

class PairwiseTransformer extends Reader {
    private final Reader inner;
    private final BiConsumer<char[], Integer> strategy;

    PairwiseTransformer(Reader inner, BiConsumer<char[], Integer> strategy) {
        this.inner = inner;
        this.strategy = strategy;
    }

    @Override
    public int read(char[] cbuf, int off, int len) {
        int r = inner.read(cbuf, off, len);
        for (int i = 0; i < r - 1; i += 2) {
            strategy.accept(cbuf, off + i);
        }
        return r;
    }
}

Why 4 marks: You need to (1) identify Strategy as the right pattern, (2) define the interface, (3) show how Swapper is parameterised by it, (4) show at least one alternative operation. Bonus: discuss how this satisfies OCP.

(e) OCP violation for images and implications — 4 marks

Model answer: The design is not open for extension with respect to data types. The entire pipeline — Reader, Swapper, BufferedReader, Decryptor — is built on the Reader abstraction, which operates on character streams (char[]). Images are binary data (bytes), not characters. To decrypt images, you would need to duplicate the entire architecture using InputStream instead of Reader:

// Would need a completely parallel hierarchy:
class ImageSwapper extends InputStream { ... }
class ImageDecryptor {
    static byte[] read(String fileName) {
        try (BufferedInputStream bis = new BufferedInputStream(
            new ImageSwapper(new PushbackInputStream(
                new FileInputStream(fileName))))) {
            return readBytes(bis);
        }
    }
}

This is a type-level coupling problem: the algorithm (Swapper) is coupled to the Reader/char data type. Despite using inheritance of interface (polymorphism through Reader), the design only works for character data.

Implications for OOP design:

  1. Inheritance hierarchies are tied to a specific abstraction — When you build a system around Reader, you have made a bet that all future uses involve characters. If a new requirement involves bytes, you must rebuild the hierarchy.

  2. The Strategy pattern alone does not solve this — Parameterising the operation (part d) lets you vary what happens to a pair, but does not let you vary what type of data the pipeline processes. The Swapper still extends Reader, so it is still fundamentally character-oriented.

  3. A more general solution would use generics or composition — You could design a generic Transformer<T> that works on Stream<T> for any type T, with PairOperation<T> parameterised by type. This would be open for extension for any data type.

  4. Design for the most general abstraction you can reasonably support — The lesson is that tying a design to a specific type hierarchy (Reader/char) limits extensibility in ways you may not anticipate. The OCP works within a given abstraction level but breaks when you need to cross abstraction boundaries.

Tripos trap: Many students answer this by saying “just use Strategy pattern” (from part d), but the Strategy pattern only addresses behaviour variation, not data type variation. The question is testing whether you understand that OCP has limits based on the abstractions you choose.