If you’ve read the first blog in the series you may remember that I initially said that I needed a Report class and that “if you look at the code, you won’t find a class named Report, it was renamed Results and refactored to create a Formatter interface, the TextFormatter and HtmlFormatter classes together with the Publisher interface and EmailPublisher class”. This blog covers the design process, highlighting the reasoning behind the refactoring and how I arrived at the final implementation.
If you read on, you may think that the design logic given below is somewhat contrived. That’s because it is. The actual process of getting from the Report class to the Results class, the Formatter and Publisher interfaces together with their implementations probably only took a few seconds to dream up; however, writing it all down took some time. The design story goes like this...
If you have a class named Report then how do you define its responsibility? You could say something like this: “The Report class is responsible for generating an error report. That seems to fit the Single Responsibility Principal, so we should be okay… or are we? Saying that a Report is responsible for generating a report is rather tautological. It’s like saying that a table is responsible for being a table, it tells us nothing. We need to break this down further. What does “generating a report” mean? What are the steps involved? Thinking about it, to generate a report we need to:
- marshall the error data.
- format the error data into a readable document.
- publish the report to a known destination.
Obviously that breaks the Single Responsibility Principal because the Report class has three responsibilities instead of one; you can tell by the use of the word 'and'. This really means we have three classes: one to handle the results, one to format the report and one to publish the report, and these three loosely coupled classes must collaborate to get that report delivered.
If you look back at the original requirements, points 6 and 7 said:
6. When all the files have been checked, format the report ready for publishing. 7 . Publish the report using email or some other technique.
Requirement 6 is pretty straight forward and concrete, we know that we’ve got to format the report. In a real project, you’d either have to come up with the format yourself or ask the customer what it was they wanted to see in their report.
Requirement 7 is somewhat more problematical. The first part is okay, it says "publish the report using email” and that’s no problem with Spring. The second is very badly written: which other technique? Is this required for this first release? If this was a real-world project, one that you’re doing for a living, then this is where you need to ask a few questions - very loudly if necessary. That’s because an unquantifiable requirement will have an impact on timescales, which could also make you look bad.
Questioning badly defined requirements or stories is a key skill when it comes to being a good developer. If a requirement is wrong or vague, no ones going to thank you if you just make things up and interpret it your own way. How you phrase your question is another matter… it’s usually a good idea to be ‘professional’ about it and say something like: “excuse me, have you got five minutes to explain this story to me, I don’t understand it”. There are only several answers you will get and they are usually:
- "Don’t bother me now, come back later…"
- "Oh yes, that’s a mistake in the requirements - thanks for spotting it, I’ll sort it out."
- "The end user was really vague here, I’ll get in touch with them and clarify what they meant."
- ”I’ve no idea - take a guess..”
- "This requirement means that you need to do X, Y, Y…"
In this particular case, the clarification would be that I’m going to add additional publishing methods in later blogs and that I want the code designed to be extensible, which in plain English means using interfaces…
The diagram above shows that the initial idea of a Report class has been split into three parts: Results, Formatter and Publisher. Anyone familiar with Design Patterns will notice that I’ve used the Strategy Pattern to inject a Formatter and Publisher implementations into the Results class. This allows me to tell the Results class to generate() a report without the Results class knowing anything about the report, its construction, or where it’s going to.
@Service
public class Results {
private static final Logger logger = LoggerFactory.getLogger(Results.class);
private final Map<String, List<ErrorResult>> results = new HashMap<String, List<ErrorResult>>();
/**
* Add the next file found in the folder.
*
* @param filePath
* the path + name of the file
*/
public void addFile(String filePath) {
Validate.notNull(filePath);
Validate.notBlank(filePath, "Invalid file/path");
logger.debug("Adding file {}", filePath);
List<ErrorResult> list = new ArrayList<ErrorResult>();
results.put(filePath, list);
}
/**
* Add some error details to the report.
*
* @param path
* the file that contains the error
* @param lineNumber
* The line number of the error in the file
* @param lines
* The group of lines that contain the error
*/
public void addResult(String path, int lineNumber, List<String> lines) {
Validate.notBlank(path, "Invalid file/path");
Validate.notEmpty(lines);
Validate.isTrue(lineNumber > 0, "line numbers must be positive");
List<ErrorResult> list = results.get(path);
if (isNull(list)) {
addFile(path);
list = results.get(path);
}
ErrorResult errorResult = new ErrorResult(lineNumber, lines);
list.add(errorResult);
logger.debug("Adding Result: {}", errorResult);
}
private boolean isNull(Object obj) {
return obj == null;
}
public void clear() {
results.clear();
}
Map<String, List<ErrorResult>> getRawResults() {
return Collections.unmodifiableMap(results);
}
/**
* Generate a report
*
* @return The report as a String
*/
public <T> void generate(Formatter formatter, Publisher publisher) {
T report = formatter.format(this);
if (!publisher.publish(report)) {
logger.error("Failed to publish report");
}
}
public class ErrorResult {
private final int lineNumber;
private final List<String> lines;
ErrorResult(int lineNumber, List<String> lines) {
this.lineNumber = lineNumber;
this.lines = lines;
}
public int getLineNumber() {
return lineNumber;
}
public List<String> getLines() {
return lines;
}
@Override
public String toString() {
return "LineNumber: " + lineNumber + "\nLines:\n" + lines;
}
}
}
Taking the Results code first, you can see that there are four public methods; three that are responsible for marshalling the result data and one that generates the report:
- addFile(…)
- addResults(…)
- clear(…)
- generate(…)
The first three methods above manage the Results internal Map<String, List<ErrorResult>> results hash map. The keys in this map are the names of any log files that the FileLocator class finds, whilst the values are Lists of ErrorResult beans. The ErrorResult bean is a simple inner bean class that’s used to group together the details of any errors found.
addFile() is a simple method that’s use to register a file with the Results class. It generates an entry in the results map and creates an empty list. If this remains empty, then we can say that this file is error free. Calling this method is optional.
addResult() is the method that adds a new error result to the map. After validating the input arguments using org.apache.commons.lang3.Validate it tests whether this file is already in the results map. If it isn’t, it creates a new entry before finally creating a new ErrorResult bean and adding it to appropriate List in the Map.
The clear()method is very straight forward: it will clear down the current contents of the results map.
The remaining public method, generate(…), is responsible for generating the final error report. It’s our strategy pattern implementation, taking two arguments: a Formatter implementation and a Publisher implementation. The code is is very straight forward as there are only three lines to consider. The first line calls the Formatter implementation to format the report, the second publishes the report and the third line logs any error if the report generation fails. Note that this is a Generic Method (as shown by the <T> attached to the method signature). In this case, the only “Gotcha” to watch out for is that this ’T’ has to be the same type for both the Formatter implementation and the Publisher implementation. If it isn’t the whole thing will crash.
public interface Formatter {
public <T> T format(Results report);
}
Formatter is an interface with a single method: public <T> T format(Results report). This method takes the Report class as an argument and returns the formatted report as any type you like
@Service
public class TextFormatter implements Formatter {
private static final String RULE = "\n==================================================================================================================\n";
@SuppressWarnings("unchecked")
@Override
public <T> T format(Results results) {
StringBuilder sb = new StringBuilder(dateFormat());
sb.append(RULE);
Set<Entry<String, List<ErrorResult>>> entries = results.getRawResults().entrySet();
for (Entry<String, List<ErrorResult>> entry : entries) {
appendFileName(sb, entry.getKey());
appendErrors(sb, entry.getValue());
}
return (T) sb.toString();
}
private String dateFormat() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
return df.format(Calendar.getInstance().getTime());
}
private void appendFileName(StringBuilder sb, String fileName) {
sb.append("File: ");
sb.append(fileName);
sb.append("\n");
}
private void appendErrors(StringBuilder sb, List<ErrorResult> errorResults) {
for (ErrorResult errorResult : errorResults) {
appendErrorResult(sb, errorResult);
}
}
private void appendErrorResult(StringBuilder sb, ErrorResult errorResult) {
addLineNumber(sb, errorResult.getLineNumber());
addDetails(sb, errorResult.getLines());
sb.append(RULE);
}
private void addLineNumber(StringBuilder sb, int lineNumber) {
sb.append("Error found at line: ");
sb.append(lineNumber);
sb.append("\n");
}
private void addDetails(StringBuilder sb, List<String> lines) {
for (String line : lines) {
sb.append(line);
// sb.append("\n");
}
}
}
This is really boring code. All it does is to create a report using a StringBuilder, carefully adding text until the report is complete. There’s only on point of interest and that’s in third line of code in the format(…) method:
Set<Entry<String, List<ErrorResult>>> entries = results.getRawResults().entrySet();
This is a textbook case of what Java’s rarely used package visibility is all about. The Results class and the TextFormatter class have to collaborate to generate the report. To do that, the TextFormatter code needs access of the Results class’s data; however, that data is part of the Result class’s internal workings and should not be publicly available. Therefore, it makes sense to make that data accessible via a package private method, which means that only those classes that need the data to under take their allotted responsibility can get hold of it.
The final part of generating a report is the publication of the formatted results. This is again done using the strategy pattern; The second argument of the Report class’s generate(…) method is an implementation of the Publisher interface:
public interface Publisher {
public <T> boolean publish(T report);
}
This also contains a single method: public <T> boolean publish(T report);. This Generic method takes a report argument of type ’T’, returning true if the report is published successfully.
What about the implementation(s)? of this class? The first implementation uses Spring’s email classes and will be the subject of my next blog, which will be published shortly...
The code for this blog is available on Github at: https://github.com/roghughe/captaindebug/tree/master/error-track.
If you want to look at other blogs in this series take a look here…
- Tracking Application Exceptions With Spring
- Tracking Exceptions With Spring - Part 2 - Delegate Pattern
No comments:
Post a comment