Skip to content

Design Patterns

felicia edited this page Nov 16, 2024 · 39 revisions

Strategy Pattern

The Strategy pattern is a behavioral design pattern that enables the selection of an algorithm's behaviour at runtime by encapsulating a family of interchangeable algorithms. It allows algorithm to vary independently from the clients that use them. This pattern is used to load mark scheme weightings from a file. It offers flexibility by allowing the system to be extended to support reading from different file formats such as .json, ensuring adaptability and scalability.

The MarkSchemeLoaderStrategy interface defines the loadWeightings() method, specifying the contract for loading mark scheme weightings into a Map<String, Map<String, Integer>>. Classes implementing this interface provide specific logic while adhering to this contract.

public interface MarkSchemeLoaderStrategy {
    public abstract Map<String, Map<String, Integer>> loadWeightings();
}

The MarkSchemeLoader implements this interface, using the Yaml utility class to load weightings from a .yaml file into a Map<String, Map<String, Integer>>.

public class MarkSchemeLoader implements MarkSchemeLoaderStrategy {
    public Map<String, Map<String, Integer>> loadWeightings() {
        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(Constants.MARK_SCHEME)) {
            if (inputStream == null) {
                throw new IllegalStateException("Could not find mark scheme resource: " + Constants.MARK_SCHEME);
            }
            return new Yaml().loadAs(inputStream);
        } catch (Exception e) {
            throw new IllegalStateException("Failed to load mark scheme weightings", e);
        }
    }
}

The GradeConfigLoader class acts as the context, selecting and executing the appropriate MarkSchemeLoaderStrategy to load mark scheme weightings.

public class GradeConfigLoader {
    private static final Map<String, Map<String, Integer>> weightings;
    private static final MarkSchemeLoaderStrategy markScheme = new MarkSchemeLoader();

    static {
        weightings = markScheme.loadWeightings();
    }

    public static Map<String, Integer> getWeightings(String gradeType) {
        return weightings.getOrDefault(gradeType, new HashMap<>());
    }
}

Command Pattern

The Command pattern is a behavioral design pattern which encapsulates a request and its details into an object, allowing for the parameterization of the request. In this system, the pattern is used for the compilation of Java source files. The compiling of each file is a request made.

The CompileCommand interface specifies the compile() method, which returns a boolean indicating success(true) or failure(false). This allows for different implementations for compiling different types of files.

public interface CompileCommand {
    public abstract boolean compile();
}

The JavaCompileCommand class implements CompileCommand and specifies the logic for compiling a .java file. The class validates the system's Java compilers' availability and obtains it using ToolProvider.getSystemJavaCompiler(). The class confirms if the submissions directory exists and is a directory. It then adds the file path of each file ending in .java to the javaFiles list. The class prepares the compilation environment by setting up the classpath to point to the submission directory and bundles all discovered Java files together into the compiler arguments. The compilation is executed by the compiler.run() method, which processes all source files in a single operation and returns zero if this successful. This value is used to return true or false for the compile() method. This implementation demonstrates the encapsulation of complex compilation logic while adhering to the CompileCommand interface.

public class JavaCompileCommand implements CompileCommand {
    @Override
    public boolean compile() {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null)
            return false;

        File directory = new File(submissionDirectory);
        List<String> javaFiles = new ArrayList<>();

        if (!directory.exists() || !directory.isDirectory())
            return false;
        for (File file : directory.listFiles())
            if (file.isFile() && file.getName().endsWith(".java"))
                javaFiles.add(file.getPath());

        if (javaFiles.isEmpty()) {
            System.out.println("No Java source files found for compilation.");
            return false;
        }

        List<String> compilerArgs = new ArrayList<String>();
        compilerArgs.add("-classpath");
        compilerArgs.add(submissionDirectory);
        compilerArgs.addAll(javaFiles);
        String[] argsArray = compilerArgs.toArray(new String[0]);
        int compilationResult = compiler.run(null, null, null, argsArray);
        return compilationResult == 0;
    }
}

Facade Pattern

The Facade Pattern is a structural design pattern that provides a simplified, high-level interface to complex subsystems, enhancing code readability and maintainability. It promotes loose coupling by hiding internal complexities, making subsystems easier to use such as simplifying the grading of student submissions.

The Facade interface specifies the processSubmissions() method which accepts a String parameter named zipFilePath. Realizations which process the submissions must implement this method and provide the submission zip file path.

public interface Facade {
    public void processSubmissions(String zipFilePath);
}

The GradingFacade provides a streamlined interface for processing student submissions. Its processSubmissions(String zipFilePath) method handles the entire workflow, starting with SubmissionExtractor.extractSubmissions(zipFilePath), which extracts submissions.zip into an output directory. Each student’s submission is organized into separate directories, with paths returned for further processing.

For each submission, the studentSubmission singleton instance is reset, and all tests within the submission's test suite are executed. Based on test results, grade items are generated, student information is gathered, and an AssignmentDetails instance is created to hold both student details and their corresponding grades.

Finally, the results are printed to the terminal using the toString() method of AssignmentDetails, and a PDF report is generated with result.generate(), which takes the AssignmentDetails instance as a parameter.

public class GradingFacade implements Facade {
    private final AbstractGradeFactory gradeFactory = new GradeFactory();
    private final PDF pdf = new PDFGenerator();
    public void processSubmissions(String zipFilePath) {
        List<String> studentSubmissions = SubmissionExtractor.extractSubmissions(zipFilePath);
        for (String studentSubmission : studentSubmissions) {
            Submission.resetInstance(studentSubmission);
            AssignmentDetails result = this.processSubmission(studentSubmission);
            System.out.println(result.toString());
            pdf.generate(result);
        }
    }
}

Factory Pattern

The Factory Pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It encapsulates the object creation process, enabling the system to be independent of how its objects are created, composed, and represented. The pattern is used in this project to create different subclasses of GradeTemplate.

The Factory<T> interface specifies the <T>createItems(List<Result> results) and the T createItem(Result result, int index) methods. Any Factory class which creates item lists or an item should implement these methods.

public interface Factory<T> {
    public abstract List<T> createItems(List<Result> results);
    public abstract T createItem(Result result, int index);
}

GradeFactory is a realization that provides the concrete implementation for creating subclasses of GradeTemplate.

public class GradeFactory extends AbstractGradeFactory {
    @Override
    public Grade createItem(Result result, int index) {
        switch (index) {
            case Constants.PROGRAM_GRADE_INDEX:
                return new ProgramGrade(result);
            case Constants.CHATBOT_GENERATOR_GRADE_INDEX:
                return new ChatBotGeneratorGrade(result);
            case Constants.CHATBOT_GRADE_INDEX:
                return new ChatBotGrade(result);
            case Constants.CHATBOT_PLATFORM_GRADE_INDEX:
                return new ChatBotPlatformGrade(result);
            case Constants.CHATBOT_SIMULATION_GRADE_INDEX:
                return new ChatBotSimulationGrade(result);
            default:
                throw new IllegalArgumentException("Unexpected index: " + index);
        }
    }
}

Template Pattern

The Template Pattern is a behavioral design pattern that defines the program skeleton of an algorithm in an operation, deferring some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing its structure. This pattern is particularly useful when multiple classes share a common algorithm structure but differ in specific steps. The template is used in the project to define a template for each type of grade to be made.

GradeTemplate is the abstract class specifying the shared details of each specific grade. Each subclass of the template will inherit all its state and methods and will be able to slightly override them.

public abstract class GradeTemplate implements Grade {
    protected Map<String, Integer> testMarks;
    protected Map<String, Boolean> feedbackMap;
    private int totalMarks;
    private int marksEarned;

    public GradeTemplate(Result result, int totalMarks) {
        this.feedbackMap = new HashMap<String, Boolean>();
        this.totalMarks = totalMarks;
        this.allocateWeightings();
        this.allocateFeedback();
        this.setMarksEarned(result);
    }

    private void setMarksEarned(Result result) {
        this.marksEarned = result.wasSuccessful() ? totalMarks :

Clone this wiki locally