-
Notifications
You must be signed in to change notification settings - Fork 0
Design Patterns
Design patterns are standardized, reusable solutions to common problems encountered in software design Three core types of Design Patterns:
- Creational Patterns: patterns which focus on efficient creation and management of objects
- Structural Patterns: patterns which deal with object organization and composition
- Behavioral Patterns: patterns which focus on object interaction and communication.
This system implements the following patterns:
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. This pattern is used to dynamically create Grade objects, offering 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) and createItem(Result result, int index), which accept JUnit results and return a list and an individual item of type T, respectively.
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. It provides a default implementation for creating a list of Grade objects via createItems, while leaving createItem to be defined by its subclasses.
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 extends AbstractGradeFactory, implementing the logic for creating Grade objects based on an index. The createItem method utilises a switch statement to map each index to a specific grade type, such as ProgramGrade or ChatBotGeneratorGrade, allowing the factory to dynamically return the appropriate Grade object.
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);
}
}
}The Facade pattern is a structural design pattern providing a high-level simple interface to complex subsystems and promoting loose coupling by hiding internal complexity, 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 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 submission processing. The implementation handles the entire workflow by extracting the zip file at the provided path, resetting the Singleton submission instance, grading submissions, report generation, and displaying processing results to a terminal.
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);
}
}
}The Strategy pattern is a behavioral design pattern that enables the selection of an algorithm's behaviour at runtime by encapsulating interchangeable algorithms. This pattern is used to load mark scheme weightings from a file, offering flexibility and enabling the system to be easily extended to support 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>>.
public interface MarkSchemeLoaderStrategy {
public abstract Map<String, Map<String, Integer>> loadWeightings();
}The MarkSchemeLoader implements this interface, using a 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<>());
}
}The Command pattern is a behavioural design pattern that encapsulates compilation requests as objects, with different commands handling specific file types (e.g., .java). This allows for flexible parameterization and execution of compilation operations.
The CompileCommand interface specifies the compile() method, returning a boolean to indicate success or failure and allowing for implementations which compile different file types.
public interface CompileCommand {
public abstract boolean compile();
}The JavaCompileCommand class implements CompileCommand to compile .java files. It validates the Java compiler and submission directory, collects all .java files, and executes compilation with the submission directory in classpath. The compile() method returns true if the compiler's execution is successful (returns zero), false otherwise.
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;
}
}The Template pattern is a behavioural 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 is an abstract class which implements the Grade interface, providing a common structure for grading implementations. It defines shared behaviours such as handling test marks, feedback, and total marks. The class implements setMarksEarned(Result result) while allowing the subclasses to provide the specific implementation of allocateWeightings() and allocateFeedback().
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() uses the static method getWeightings() from the GradeConfigLoader to retrieve a map of weightings for each test for the ChatBot class, using the constant Constants.CHATBOT_GRADE as a parameter. This populates the testMarks map with the appropriate weightings for each test. The allocateFeedback() method populates the feedbackMap with the testMarks map. Following this pattern allows subclasses to share common methods such as setMarksEarned(Result result) and adjustMarksForFailures(List <Failure> failures>), ensuring consistency in grading behaviour.