-
Notifications
You must be signed in to change notification settings - Fork 0
Design Patterns
The Strategy pattern is a behavioral design pattern for selecting an algorithm's behavior at runtime by defining a family of algorithms and making them interchangeable by encapsulation allowing the algorithm to vary independently from the clients that use them.The pattern is used to load the mark scheme weightings from a mark_scheme.yaml file by allowing for multiple ways to load the weightings.
The MarkSchemeLoaderStrategy interface defines a loadWeightings() method for loading mark scheme weightings from mark_scheme.yaml into a Map<String,Map<String ,Integer>>.Any class which loads weightings must implement this interface and conform to the return type specified by the interface.
public interface MarkSchemeLoaderStrategy {
public abstract Map<String, Map<String, Integer>> loadWeightings();
}MarkSchemeLoader is a realization of the interface and provides the specific logic for loading mark scheme weightings, loading marks into the Map<String,Map<String,Integer> variable with use of the Yaml utility class.
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 command pattern is a behavioral design pattern that turns a request into an object containing all request details, allowing for parameterization of the request.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). The specific compilation details are abstracted allowing for different implementations of compiling a java source file.
public interface CompileCommand {
public abstract boolean compile();
}The JavaCompileCommand class is a realization of the interface and provides the specific logic for compiling Java source files in a directory. The class first validates if the compiler is available and obtains the system's Java compiler using ToolProvider.getSystemJavaCompiler(). The submission directory is scanned for .java files, where the class performs essential validation to ensure both the directory exists and contains valid Java source files. If any of these conditions fail, the compilation process is halted early.
Once the source files are identified, all Java files in the directory are wrapped into a collection for batch processing. 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 actual compilation is executed using the Java compiler's run method, which processes all source files in a single operation. The compilation is considered successful only if the compiler returns an exit code of zero, indicating no errors were encountered during the process.
This implementation demonstrates how the Command Pattern can encapsulate complex compilation logic while maintaining a simple interface. The class handles all the necessary details of file discovery, validation, and compilation while adhering to the single responsibility principle by focusing solely on Java compilation tasks.
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 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);
}
}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);The generic type allows the factory to create objects of any specified type promoting reusability.
GradeFactory is a realization which 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);
}
}
}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
package com.gophers.structures.grades;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import com.gophers.interfaces.Grade;
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 = 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, false);
}
}
public int getMarks() {
return this.marksEarned;
}
public int getTotalMarks() {
return this.totalMarks;
}
public Map<String, Boolean> getTestFeedback() {
return feedbackMap;
}
protected abstract void allocateWeightings();
protected abstract void allocateFeedback();
}There are many realizations for GradeTemplate one of which is ChatBotGeneratorGrade where allocateWeightings() and allocateFeedback() is overridden from the superclass to implement behavior specific to grading a ChatBotGenerator class in a submission.
package com.gophers.structures.grades;
import org.junit.runner.Result;
import com.gophers.services.handlers.GradeConfigLoader;
import com.gophers.utilities.Constants;
public class ChatBotGeneratorGrade extends GradeTemplate {
private static final int totalMarks = 7;
public ChatBotGeneratorGrade(Result result) {
super(result, totalMarks);
}
@Override
protected void allocateWeightings() {
super.testMarks = GradeConfigLoader.getWeightings(Constants.CHATBOT_GENERATOR_GRADE);
}
@Override
protected void allocateFeedback() {
for (String key : super.testMarks.keySet()) {
super.feedbackMap.put(key, true);
}
}
}
## Singleton Pattern
The Singleton pattern is a structural pattern which ensures a class only has one instance throughout the application's lifecycle and provides a global access point to the instance. The pattern is used in this project to ensure only a single submission exists at any given time, preventing grading clashes by handling each submission separately
### Implementation
the `Submission` class ensures that only one instance is available to the application at any time by holding that instance as a private static variable and only creates a new instance if the current instance is null
```java
public class Submission {
private static boolean isCompiled;
private static Submission instance;
private static String submissionDirectory;
private static URLClassLoader classLoader;
public static Submission getInstance(String submissionDirectory) {
if (instance == null)
instance = new Submission(submissionDirectory);
return instance;
}it also provides a resetInstance() to make sure that when a submission is completely processed, it can be replaced with another submission.
- S - Single Responsibility Principle
- O - Open/Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
A class should have only one reason to change, meaning it should handle only one responsibility, allowing easier maintenance. The Yaml class handles only the responsibility of parsing a .yaml file, ensuring that it remains focused on one specific task, which makes it easier to maintain and test
public class Yaml {
Map<String, Map<String, Integer>> result = new HashMap<>();
Map<String, Integer> currentSection = null;
public Map<String, Map<String, Integer>> loadAs(InputStream inputStream) throws IOException {
String content = new String(inputStream.readAllBytes());
for (String line : content.split("\n")) {
String trimmed = line.trim();
if (trimmed.endsWith(":")) {
result.put(trimmed.substring(0, trimmed.length() - 1), currentSection = new HashMap<>());
} else if (trimmed.contains(":") && currentSection != null) {
String[] parts = trimmed.split(":");
currentSection.put(parts[0].trim(), Integer.parseInt(parts[1].trim()));
}
}
return result;
}
}Classes should be open for extension but closed for modification, enabling new functionality without altering existing code.GradingFacade is closed for modification as it contains a GradeFactory instance which handles the logic for creating different realizations of GradeTemplate while being open for extension.
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);
}
}
public AssignmentDetails processSubmission(String submissionDirectory) {
List<Result> results = new AssignmentTestRunner().runAllTests();
AssignmentGrade assignmentGrade = new AssignmentGrade(gradeFactory.createItems(results));
StudentDetails student = new StudentDetails(submissionDirectory);
return new AssignmentDetails(student, assignmentGrade);
}
}Subtypes must be replaceable with their base types without affecting the correctness of the program. This principle is upheld in the following example. In App.java, the GradingFacade() instance is declared with the static type Facade. Since GradingFacade implements the Facade interface and provides the expected behavior (such as the processSubmissions() method), it can be used anywhere that a Facade is expected.
public class App {
public static void main(String[] args) {
Facade facade = new GradingFacade();
facade.processSubmissions(Constants.SUBMISSIONS);
}Clients should not be forced to implement interfaces they don’t use; instead, use smaller, more specific interfaces. The interfaces in this project do not force implementations of unwanted methods and express clear separation of concerns.
Grade Interface:
public interface Grade {
public abstract int getMarks();
public abstract int getTotalMarks();
public abstract Map<String, Integer> getTestFeedback();
}PDF Interface:
public interface PDF {
public abstract void generate(AssignmentDetails assignmentDetails);
}"The Grade and PDF interfaces are designed to be specific to the functionalities they represent, ensuring that implementations only need to deal with the methods relevant to them and avoid unnecessary complexity."
High-level modules should depend on abstractions, not concrete implementations, promoting decoupling and flexibility. App.java contains a GradingFacade instance but does not depend on it since the static type of the object is Facade, meaning that App depends on the abstraction and is decoupled from the concrete GradingFacade class.
public class App {
public static void main(String[] args) {
Facade facade = new GradingFacade();
facade.processSubmissions(Constants.SUBMISSIONS);
}
}