今天,我们将讨论在设计不足和过度设计之间保持简单,愚蠢(KISS)和鲁棒性的设计价值之间的冲突。
我们正在编写一个批处理Java应用程序,需要确保在服务器上一次最多运行一个实例。 一个团队成员有一个很好的想法,那就是使用锁定文件,这确实有效并且对我们有很大帮助。 但是,最初的实现并不十分健壮,由于对该死的应用程序拒绝运行并查找锁定文件进行了故障排除,这使我们花费了宝贵的时间和昂贵的上下文切换。
正如Comoyo的ØyvindBakksjø最近解释的那样,软件工程师与纯粹的编码器的区别在于,它不仅思考和关注遍历代码的快乐路径,而且也关注不愉快的情况。 优秀的工程师会考虑可能出现的问题,并尝试适当地处理它们,以便依赖于它们和其用户的代码可以更轻松地处理有问题的情况。 健壮性包括及早发现错误,以适当的方式处理错误以及提供有用和有用的错误消息。 另一方面,简单性[TBD:Hickey]是系统的关键特征。 花太多时间来制作防弹代码总是很容易,而不是将精力集中在对业务更有价值的地方。
过于简单的实现
最初的实现非常简单:
public class SimpleSingletonBatchJob {
private static boolean getLock() {
File file = new File(LOCK_DIRECTORY+File.separatorChar+Configuration.getGroupPrefix());
try {
return file.createNewFile();
} catch (IOException e) {
return false;
}
}
private static void releaseLock() {
File file = new File(LOCK_DIRECTORY+File.separatorChar+Configuration.getGroupPrefix());
file.delete();
}
public static void exit(int nr) {
releaseLock();
System.exit(nr);
}
public static void main(String[] args) throws IOException {
...
if (! getLock()) { // #1 try to create lock
System.out.println("Already running");
return;
}
... // do the job (may throw exceptions)
releaseLock(); // #2 release lock when done
}
}
主要问题是,如果该应用程序失败或被杀死,它将留下锁定文件,而下次它将拒绝并以无用的错误消息开头。 您将需要了解/阅读代码以了解如何解决问题。
有人认为,这样的失败和故意的失败只会很少发生,以致于没有理由要求使代码更健壮。 但是,我们需要花费很少的精力来使代码更加友好和健壮,f.ex。 通过在错误消息中包括锁定文件路径并解释为什么可能存在锁定文件路径以及如何解决该问题(例如“如果应用未运行,则锁定是失败运行后的遗留物,可能会删除”)。 确保在失败时删除文件是一些琐碎的代码行,可以节省一些混乱和时间。 另外,值得一提的是使其更强大,从而不需要太多的手动干预–对您的操作人员很友好。 (我希望是你。)
更强大的实施
这是改进的版本,具有有用的错误消息,并在失败时删除锁:
public class RobustSingletonBatchJob {
// Note: We could use File.deleteOnExit() but the docs says it is not 100% reliable and recommends to
// use java.nio.channels.FileLock; however this code works well enough for us
static synchronized boolean getLock() {
File file = new File(LOCK_DIRECTORY, StaticConfiguration.getGroupPrefix());
try {
// Will try to create path to lockfile if it does not exist.
file.getParentFile().mkdirs(); // #1 Create the lock dir if it doesn't exist
if (file.createNewFile()) {
return true;
} else {
log.info("Lock file " + file.getAbsolutePath() + " already exists."); // #2 Helpful error msg w/ path
return false;
}
} catch (IOException e) {
throw new RuntimeException("Failed to create lock file " + file.getAbsolutePath()
+ " due to " + e + ". Fix the problem and retry."
, e); // #3 Helpful error message with context (file path)
}
}
private synchronized static void releaseLock() {
File file = new File(LOCK_DIRECTORY, StaticConfiguration.getGroupPrefix());
file.delete();
}
public static void main(String[] args) throws Exception {
boolean releaseLockUponCompletion = true;
try {
...
if (! getLock() {
releaseLockUponCompletion = false;
log.error("Lock file is present, exiting."); // Lock path already logged
throw new RuntimeException("Lock file is present"); // throwing is nicer than System.exit/return
}
... // do the job (may throw exceptions)
} finally {
if (releaseLockUponCompletion) {
releaseLock(); // #4 Always release the lock, even upon exceptions
}
}
}
改进之处:
- 如果不存在锁,则创建一个存储锁的目录(该锁不存在,并导致混淆的错误消息“已运行”)已经使我们痛苦不堪
- 有用的错误消息“锁定文件<文件的绝对路径>已存在。” =>易于复制和粘贴int rm 。
- 有用的错误消息,其中包含文件路径和错误信息,当我们无法创建锁时(空间不足,目录权限不足等)。
- 将整个主程序包装起来进行尝试–最后,确保始终删除锁定文件
该代码仍然不是完美的-如果您终止了该应用程序,则锁定文件仍将留下。 有多种方法可以解决该问题(例如,将应用程序的pid包含在文件中,在启动时不仅检查其是否存在,而且还检查该pid确实存在/是否为该应用程序),但是在处理时间和增加成本方面都需要解决复杂性的确高于收益。
结论
KISS和鲁棒性都是重要目标,并且经常会发生冲突。 使您的代码比必需的更健壮会使其变得过于复杂,并浪费时间,并且机会成本(丢失)。 由于故障排除,使代码过于简单会花费您或它的用户大量时间。 要实现正确的平衡,需要经验并不断地寻求平衡。 如果您的团队无法达成共识,最好从一个简单的代码开始,并根据其实际的健壮性需求收集硬数据,而不是事先对其进行过度设计。 不要像我一样成为完美主义者,但也要对您的用户和开发人员有益。 如果您可以毫不费力地使您的应用程序更强大,那就去做吧。 如果需要更多工作,请去收集数据以证明(或不需要)该工作。