
本文探讨了在groovy中如何通过闭包(closure)来优雅地合并具有相似逻辑但条件判断不同的轮询方法,以减少代码冗余并提高可维护性。通过引入一个通用的`waituntil`方法,它接受一个返回布尔值的闭包作为条件检查器,并支持自定义重试间隔和最大重试次数,从而实现灵活且高效的条件等待机制,同时优化了潜在的垃圾回收开销。
优化相似轮询逻辑的挑战
在软件开发中,我们经常会遇到需要等待某个条件满足才能继续执行的场景。这些场景往往表现为一段轮询代码,其核心逻辑是在一个循环中反复检查某个状态,直到满足特定条件后退出。当存在多个这样的轮询逻辑,它们之间除了条件判断不同外,其余部分(如循环结构、等待机制、错误处理)高度相似时,代码冗余就会成为一个问题。
例如,考虑以下两个Groovy方法:
def someCondition = false
def method1() {
while(!someCondition) {
def connectionStatus = getConnectionStatus() // return 200, 404, etc.
if (connectionStatus == 200) {
someCondition = true
} else {
println "repeat until connection is 200"
sleep 15
}
}
}
def method2(){
while(!someCondition) {
def result = getResult() // A, B, C, etc.
if (result in ["A", "B", "C"]) {
someCondition = true
} else {
println "waiting until result is either A, B, or C"
sleep 15
result = getResult() // call again to check if result changed
}
}
}这两个方法都包含了一个while循环、一个内部的条件检查以及一个sleep调用。它们的区别仅在于if语句中的具体条件。直接复制粘贴并修改条件虽然可行,但会导致代码重复,降低可维护性。当需要修改等待逻辑(例如,改变等待时间或增加最大重试次数)时,必须修改所有相似的方法,这增加了出错的风险。
利用闭包实现通用等待机制
Groovy的闭包(Closure)提供了一种优雅的方式来抽象行为,使其作为参数传递给方法。我们可以利用这一特性,将上述两个方法中不同的条件判断逻辑封装成闭包,然后传递给一个通用的等待方法。
下面是一个名为waitUntil的通用方法,它接受一个闭包作为参数,该闭包负责执行实际的条件检查并返回一个布尔值(true表示条件满足,false表示条件不满足)。
/** * 等待直到指定条件满足。 * @param sleep 每次重试之间的等待时间(毫秒)。 * @param maxRetries 最大重试次数。 * @param test 一个闭包,执行条件检查并返回布尔值。 */ def waitUntil(int sleep = 1, int maxRetries = 10, Closuretest) { int retries = 0 while (retries++ < maxRetries && !test()) { println "Failed at attempt $retries/$maxRetries, sleeping for $sleep ms before retrying" Thread.sleep(sleep) } // 可以在此处添加逻辑来判断是否成功,例如返回一个布尔值 // return test() // 返回最终条件是否满足 }
方法解析:
- sleep:每次重试之间的等待时间,默认为1毫秒。
- maxRetries:最大重试次数,默认为10次。
- test:这是一个Closure
类型的闭包,它不接受参数,并期望返回一个布尔值。当test()返回true时,表示条件已满足,循环终止。 - retries++
- !test():如果条件未满足(闭包返回false),则继续循环。
- Thread.sleep(sleep):暂停当前线程以避免忙等。
如何使用:
有了waitUntil方法,我们可以将之前的两个示例方法重构为简洁的调用:
// 假设 getConnectionStatus() 和 getResult() 是已定义的方法
// 对应 method1 的场景
waitUntil {
getConnectionStatus() == 200
}
// 对应 method2 的场景
waitUntil {
getResult() in ["A", "B", "C"]
}此外,waitUntil方法还支持自定义等待参数:
// 等待时间更长(100毫秒)
waitUntil(100) {
getResult() in ["A", "B", "C"]
}
// 等待时间更长(100毫秒),且只重试5次
waitUntil(100, 5) {
getResult() in ["A", "B", "C"]
}通过这种方式,我们成功地将重复的轮询逻辑抽象到一个通用方法中,极大地减少了代码冗余,并提高了灵活性。
优化闭包中的对象创建
在使用闭包进行条件检查时,需要注意一个潜在的性能问题:如果闭包内部每次执行都会创建新的对象(例如,每次都创建一个新的列表),在长时间运行或高速循环的任务中,这可能导致大量的临时对象被创建,从而增加垃圾回收的负担。
以上述 getResult() in ["A", "B", "C"] 为例,每次闭包执行时都会创建一个新的 ["A", "B", "C"] 列表。为了优化这一点,我们可以修改waitUntil方法,使其能够接受期望值作为参数,并将这些值传递给闭包。这样,列表或其他期望对象只需创建一次。
/** * 等待直到指定条件满足,支持传递期望值到闭包。 * @param expectedResult 期望的结果值,可以是一个列表、单个值或其他任何闭包所需的参数。 * @param sleep 每次重试之间的等待时间(毫秒)。 * @param maxRetries 最大重试次数。 * @param test 一个闭包,执行条件检查并返回布尔值。它接受 expectedResult 作为参数。 */ def waitUntil(Object expectedResult, int sleep = 1, int maxRetries = 10, Closuretest) { int retries = 0 while (retries++ < maxRetries && !test(expectedResult)) { // 将 expectedResult 传递给闭包 println "Failed at attempt $retries/$maxRetries, sleeping for $sleep ms before retrying" Thread.sleep(sleep) } // return test(expectedResult) // 返回最终条件是否满足 }
使用优化后的方法:
// 期望结果列表只创建一次
waitUntil(["A", "B", "C"]) { expected -> // 闭包现在接受一个参数
getResult() in expected // 使用传入的 expected 参数
}
// 也可以传递单个值
waitUntil(200) { expectedStatus ->
getConnectionStatus() == expectedStatus
}在这个优化版本中,["A", "B", "C"] 列表只在 waitUntil 方法被调用时创建一次,然后作为参数传递给闭包。闭包在每次执行时,都使用这个已经存在的列表进行比较,避免了重复创建对象的开销。
总结与注意事项
通过利用Groovy的闭包特性,我们可以将相似的轮询等待逻辑进行高度抽象和合并,从而实现:
- 代码去重(DRY原则):消除了重复的轮询结构。
- 提高可维护性:所有等待逻辑的修改都集中在一个地方。
- 增强灵活性:通过参数和闭包,可以轻松定制等待时间、重试次数和具体条件。
- 优化性能:通过将期望值作为参数传递,减少了闭包内部的重复对象创建。
注意事项:
- 异常处理:在test闭包内部执行的操作(如getConnectionStatus()或getResult())可能会抛出异常。在实际应用中,你可能需要在闭包内部或waitUntil方法中添加适当的异常捕获和处理逻辑。
- 日志记录:waitUntil方法中简单的println语句可以替换为更专业的日志框架(如Log4j或SLF4J),以便更好地控制日志输出级别和目标。
- 超时机制:虽然maxRetries提供了一种超时机制,但有时直接设置一个总的等待时间会更直观。可以考虑在waitUntil方法中增加一个timeoutMillis参数,与maxRetries结合使用或作为替代。
- 异步操作:对于涉及异步操作的等待场景,闭包同样强大。可以修改test闭包来检查Future、Promise或其他异步结果的状态。
通过上述方法,Groovy开发者可以编写出更简洁、更健壮、更易于维护的轮询等待代码。










