npm共有包和私有包
这是本系列中的第三个博客,它宽松地着眼于跟踪应用程序错误。 在本系列中,我正在编写一个轻量级但具有工业实力的应用程序,该应用程序定期扫描应用程序日志文件,查找错误,如果发现错误,则生成并发布报告。
如果您阅读了该系列的第一个博客 ,您可能会记得我最初说过我需要一个Report
类,并且“如果您查看代码,您将找不到名为Report的类,它将被重命名为Results并进行了重构。创建Formatter接口,TextFormatter和HtmlFormatter类以及Publisher接口和EmailPublisher类”。 该博客介绍了设计过程,重点介绍了重构背后的原因以及如何实现最终实现。
如果继续阅读,您可能会认为下面给出的设计逻辑有些人为。 那是因为。 从Report
类到Results
类, Formatter
和Publisher
接口以及其实现的实际过程可能只花了几秒钟的时间才能完成。 但是,将其全部写下来需要一些时间。 设计故事就是这样……
如果您有一个名为Report
的类,那么您如何定义其职责? 您可以这样说:“ Report
类负责生成错误报告。 这似乎符合“ 单一职责负责人”的原则 ,所以我们应该没事……还是? 说一个报告负责生成一个报告是重言式的。 这就像说一个表负责成为一个表一样,它什么也没有告诉我们。 我们需要进一步分解。 “生成报告”是什么意思? 涉及哪些步骤? 考虑一下,要生成报告,我们需要:
- 整理错误数据。
- 将错误数据格式化为可读文档。
- 将报告发布到已知的目的地。
如果将所有这些都包含在Report
类的职责定义中,则会得到以下内容:
“
Report
类负责整理错误数据并将数据格式化为可读文档, 然后将报告发布到已知目标。”
显然,这违反了“单一责任主体”,因为Report
类具有三项职责,而不是一项; 您可以通过使用“和”一词来分辨。 这实际上意味着我们有三个类:一个用于处理结果,一个用于格式化报告,一个用于发布报告,这三个松散耦合的类必须协作才能交付该报告。
如果回头看原始要求,第6点和第7点表示:
6. When all the files have been checked, format the report ready for publishing.
7 . Publish the report using email or some other technique.
要求6非常直截了当且具体,我们知道我们必须格式化报告。 在真实的项目中,您要么必须自己提出格式,要么询问客户他们希望在报表中看到什么。
要求7有点问题。 第一部分是可以的,它说“使用电子邮件发布报告”,Spring没问题。 第二种写得很糟:还有哪一种技术? 这是第一个发行版本所必需的吗? 如果这是一个现实世界的项目,是您正在谋生的项目,那么这里就是您需要问几个问题的地方-必要时大声地问。 那是因为无法量化的要求会影响时间表,这也可能使您看起来很糟糕。
当要成为一名优秀的开发人员时,质疑定义不明确的需求或故事是一项关键技能。 如果需求是错误或含糊的,只要您自己整理并以自己的方式解释,就没有人会感谢您。 如何表达您的问题是另一回事……对这个问题“专业化”并说出类似的话通常是个好主意:“不好意思,您有五分钟的时间向我解释这个故事吗,我听不懂”。 您只会得到几个答案,通常是:
- “现在不要打扰我,以后再来……”
- “哦,是的,这是要求中的错误–感谢您发现它,我将对其进行整理。”
- “最终用户在这里确实含糊不清,我将与他们联系并阐明它们的含义。”
- “我不知道-猜一猜。”
- “此要求意味着您需要执行X,Y,Y…”
…并记住记下您的未解决需求问题并加以追究:其他人的不活动可能会威胁您的截止日期。
在这种特殊情况下,需要澄清的是,我将在以后的博客中添加其他发布方法,并且我希望代码被设计为可扩展的,这简而言之意味着使用接口。
上图显示了Report
类的最初思想已分为三个部分: Results
, Formatter
和Publisher
。 任何熟悉设计模式的人都会注意到,我已经使用了策略模式将Formatter
和Publisher
实现注入到Results
类中。 这使我可以告诉 Results
类要generate()
报告,而Results
类却不知道有关该报告,其构造或去向的任何信息。
@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;
}
}
}
首先使用“ Results
代码,您可以看到有四个公共方法。 三个负责编组结果数据,另一个负责生成报告:
-
addFile(…)
-
addResults(…)
-
clear(…)
-
generate(…)
上面的前三个方法管理Result
的内部Map<String, List<ErrorResult>> results
哈希映射。 此映射中的键是FileLocator
类找到的任何日志文件的名称,而值是ErrorResult
bean的List
。 ErrorResult
bean是一个简单的内部bean类,用于将找到的任何错误的详细信息分组在一起。
addFile()
是一个简单的方法,用于向Results
类注册文件。 它在results
图中生成一个条目,并创建一个空列表。 如果它保持为空,那么我们可以说该文件没有错误。 调用此方法是可选的。
addResult()
是将新的错误结果添加到地图的方法。 使用org.apache.commons.lang3.Validate
验证输入参数后,它将测试此文件是否已在结果映射中。 如果不是,它将在最终创建新的ErrorResult
bean并将其添加到Map
适当List
之前创建一个新条目。
clear()
方法非常简单:它将清除results
图的当前内容。
其余的公共方法generate(…)
负责生成最终的错误报告。 这是我们的策略模式实现,带有两个参数: Formatter
实现和Publisher实现。 该代码非常简单,因为只需考虑三行。 第一行调用Formatter
实现以格式化报告,第二行发布报告,如果报告生成失败,第三行记录任何错误。 请注意,这是一个通用方法(如方法签名所附的<T>所示)。 在这种情况下,唯一需要注意的“ Gotcha”是,对于两个
Formatter
实现和Publisher
实现。 如果不是这样,整个事情将会崩溃。
public interface Formatter {
public <T> T format(Results report);
}
Formatter
是具有单个方法的接口: public <T> T format(Results report)
。 此方法将Report
类作为参数,并以您喜欢的任何类型返回格式化的报告
@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");
}
}
}
这真是无聊的代码。 它所做的就是使用StringBuilder
创建报告,仔细添加文本,直到报告完成。 只有兴趣点在format(…)
方法的第三行代码中:
Set<Entry<String, List<ErrorResult>>> entries = results.getRawResults().entrySet();
这是一本教科书,讲述了Java很少使用的包可见性的全部内容。 Results
类和TextFormatter
类必须协作才能生成报告。 为此, TextFormatter
代码需要访问Results
类的数据。 但是,该数据是Result
类内部工作的一部分,并且不应公开获得。 因此,使数据通过package private
方法可访问是有意义的,这意味着只有那些需要数据承担其分配职责的类才能获得它。
生成报告的最后一部分是格式化结果的发布。 再次使用策略模式来完成; Report
类的generate(…)
方法的第二个参数是Publisher
接口的实现:
public interface Publisher {
public <T> boolean publish(T report);
}
它还包含一个方法: public <T> boolean publish(T report);
。 此泛型方法采用类型为'T'的report
参数,如果成功发布了报告,则返回true。
实施情况如何? 这个班的? 第一个实现使用Spring的电子邮件类,并将成为我的下一个博客的主题,该博客将很快发布...
- 该博客的代码可在Github上找到: https : //github.com/roghughe/captaindebug/tree/master/error-track 。
如果您想查看本系列中的其他博客,请在这里看……
npm共有包和私有包