Skip to content

Design Patterns

Brandon C edited this page Nov 16, 2024 · 39 revisions

Design Patterns

Design patterns are standardized, reusable solutions to common problems encountered in software design. They represent best practices refined over time to address recurring challenges in a flexible, scalable, and efficient way. Design patterns may be categorized as either creational patterns which focus on efficient creation and management of objects, structural patterns which deal with object organization and composition or behavioral patterns which focus on object interaction and communication. This system uses the following patterns:

Factory Pattern

The Factory pattern is a creational design pattern that provides an interface for creating objects while allowing subclasses to alter the type of objects created. It encapsulates the object creation process, decoupling the system from how objects are created and represented. This pattern is used to dynamically create Grade objects, o flexibility in handling different grade types and reducing tight coupling with business logic.

In this context, the Factory<T> interface specifies the methods createItems(List<Result> results) which returns a List<T> and createItem(Result result, int index) which returns a single T. All subclasses must implement these methods, allowing them to define their own behaviour for creating items and item lists.

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

AbstractGradeFactory is an abstract class that implements the Factory<Grade>. It provides a default implementation for creating a list of Grade objects via createItems, while leaving the creation of individual Grade objects to be defined by subclasses through the abstract createItem method.

public abstract class AbstractGradeFactory implements Factory<Grade> {
    public List<Grade> createItems(List<Result> results) {
        List<Grade> grades = new ArrayList<>();
        for (int i = 0; i < results.size(); i++)
            grades.add(createItem(results.get(i), i));
        return grades;
    }

    public abstract Grade createItem(Result result, int index);
}

GradeFactory provides a concrete implementation of AbstractGradeFactory, defining the logic for creating Grade objects based on the index corresponding to the class being graded for the assignment such as ProgramGrade, ChatBotGeneratorGrade, and other specific grade types which hold the results of the test cases.

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);
        }
    }
}

Facade Pattern

The Facade Pattern is a structural design pattern that provides a simplified, high-level interface to complex subsystems, promoting loose coupling by hiding internal complexities, making subsystems easier to use. In this system, the pattern is used to simplify the interface provided to clients for processing submissions.

The Facade interface specifies the processSubmissions(String zipFilePath) method for realizations to implement, which accepts a path to a zip file containing submissions.

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

The GradingFacade class implements Facade, providing a streamlined interface for processing student submissions. The implementation handles the entire workflow, extracting the zip file at the provided path into an output directory. The Singleton submission instance is reset before each submission is processed. After a submission is graded, an AssignmentDetails instance is made to capture the details of the submission and is organized into a separate output directroy. A PDF instance used to generate the PDF containing the details from the result of processing and the toString() method of each result is also used to display results in the terminal. This method eliminates the need for clients to interact with subsystems required for submission processing, providing a simple interface for clients to use.

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);
        }
    }
}

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;
    }
}

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. Letting subclasses redefine steps of an algorithm without changing its structure. In this system the pattern is used define a template for each type of grade for a submission.

GradeTemplate acts as the template abstraction, specifying the shared details of each implementation. Each subclass of the template will inherit all its state and methods and will implement the specific details of each step.

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

    public GradeTemplate(Result result, int totalMarks) {
        this.totalMarks = totalMarks;
        this.allocateWeightings();
        this.allocateFeedback();
        this.setMarksEarned(result);
    }

    private void setMarksEarned(Result result) {
        this.marksEarned = this.totalMarks;
        if (!result.wasSuccessful())
            adjustMarksForFailures(result.getFailures());
    }

    private void adjustMarksForFailures(List<Failure> failures) {
        for (Failure failure : failures) {
            String methodName = failure.getDescription().getMethodName();
            this.marksEarned -= testMarks.getOrDefault(methodName, 0);
            feedbackMap.put(methodName, 0);
        }
    }

    public int getMarks() {
        return this.marksEarned;
    }

    public int getTotalMarks() {
        return this.totalMarks;
    }

    public Map<String, Integer> getTestFeedback() {
        return feedbackMap;
    }

    protected abstract void allocateWeightings();

    protected abstract void allocateFeedback();
}

Subclasses of GradeTemplate implement the abstract methods allocateWeightings() and allocateFeedback() differently. The ChatBotGrade class extends GradeTemplate and implements the abstract method as shown

public class ChatBotGrade extends GradeTemplate {
	private static final int totalMarks = Constants.CHATBOT_GRADE_TOTAL_MARKS;

	public ChatBotGrade(Result result) {
		super(result, totalMarks);
	}

	@Override
	protected void allocateWeightings() {
		super.testMarks = GradeConfigLoader.getWeightings(Constants.CHATBOT_GRADE);
	}

	protected void allocateFeedback() {
		super.feedbackMap = new HashMap<String, Integer>(super.testMarks);
	}
}

The allocateWeightings() implementation when called uses the static method getWeightings() method from the GradeConfigLoader class to obtain the map of weighting for each test to be run on the ChatBot class by passing the constant string Constants.CHATBOT_GRADE as a parameter. This results in the testMarks map being populated with entries for each test to be run on the ChatBot class and the corresponding weight for each test. The allocateFeedback() implementation populates the feedback map by initializing a HashMap<String, Integer> and passing the testMarks map as a parameter. This use of the pattern allows sub classes to share a similar template method(s), in this case, setMarksEarned(Result result and adjustMarksForFailures(List <Failure> failures>).

Clone this wiki locally