I'm trying to convert the Java code of a design pattern called Chain of Responsibility in Kotlin idiomatically. But I'm not getting any clue on converting the default method appendNext()
of Java interface in Kotlin. I tried some already existing questions like this and this but they don't seem to be working for my use case.
I tried converting the default method appendNext()
to an extension function in Kotlin. But apparently Kotlin doesn't seem to find the method Logger.message()
and throws NoSuchMethodError
.
I have given the original Java code and the Kotlin code I tried so far in the following snippets.
I would prefer a Kotlin idiomatic solution of this code without using the @JvmDefault
annotation. The code should be as concise as Java if not more. Any help would be much appreciated.
Java code
This is the correctly working Java code for the design pattern Chain of Responsibility:
import java.util.Arrays;
import java.util.EnumSet;
import java.util.function.Consumer;
@FunctionalInterface
public interface Logger {
public enum LogLevel {
INFO, DEBUG, WARNING, ERROR, FUNCTIONAL_MESSAGE, FUNCTIONAL_ERROR;
public static LogLevel[] all() {
return values();
}
}
abstract void message(String msg, LogLevel severity);
default Logger appendNext(Logger nextLogger) {
return (msg, severity) -> {
message(msg, severity);
nextLogger.message(msg, severity);
};
}
static Logger writeLogger(LogLevel[] levels, Consumer<String> stringConsumer) {
EnumSet<LogLevel> set = EnumSet.copyOf(Arrays.asList(levels));
return (msg, severity) -> {
if (set.contains(severity)) {
stringConsumer.accept(msg);
}
};
}
static Logger consoleLogger(LogLevel... levels) {
return writeLogger(levels, msg -> System.err.println("Writing to console: " + msg));
}
static Logger emailLogger(LogLevel... levels) {
return writeLogger(levels, msg -> System.err.println("Sending via email: " + msg));
}
static Logger fileLogger(LogLevel... levels) {
return writeLogger(levels, msg -> System.err.println("Writing to Log File: " + msg));
}
public static void main(String[] args) {
// Build an immutable chain of responsibility
Logger logger = consoleLogger(LogLevel.all())
.appendNext(emailLogger(LogLevel.FUNCTIONAL_MESSAGE, LogLevel.FUNCTIONAL_ERROR))
.appendNext(fileLogger(LogLevel.WARNING, LogLevel.ERROR));
// Handled by consoleLogger since the console has a LogLevel of all
logger.message("Entering function ProcessOrder().", LogLevel.DEBUG);
logger.message("Order record retrieved.", LogLevel.INFO);
// Handled by consoleLogger and emailLogger since emailLogger implements Functional_Error & Functional_Message
logger.message("Unable to Process Order ORD1 Dated D1 For Customer C1.", LogLevel.FUNCTIONAL_ERROR);
logger.message("Order Dispatched.", LogLevel.FUNCTIONAL_MESSAGE);
// Handled by consoleLogger and fileLogger since fileLogger implements Warning & Error
logger.message("Customer Address details missing in Branch DataBase.", LogLevel.WARNING);
logger.message("Customer Address details missing in Organization DataBase.", LogLevel.ERROR);
}
}
Kotlin code
This is what I tried so far. I moved the Enum
to a separate file and kept everything at top level. Have a look at the appendNext()
method, this is what seems to be the cause of the issue.
Logger.kt
import java.util.*
import java.util.function.Consumer
interface Logger {
fun message(message: String, severity: LogLevel)
}
fun Logger.appendNext(nextLogger: Logger): Logger {
return object: Logger {
override fun message(message: String, severity: LogLevel) {
message(message, severity)
nextLogger.message(message, severity)
}
}
}
fun writeLogger(
stringConsumer: Consumer<String>,
vararg levels: LogLevel
): Logger {
val set = EnumSet.copyOf(listOf(*levels))
return object: Logger {
override fun message(message: String, severity: LogLevel) {
if (set.contains(severity)) {
stringConsumer.accept(message)
}
}
}
}
fun consoleLogger(vararg levels: LogLevel): Logger {
return writeLogger(
Consumer { msg: String -> System.err.println("Writing to console: $msg") },
*levels
)
}
fun emailLogger(vararg levels: LogLevel): Logger {
return writeLogger(
Consumer { msg: String -> System.err.println("Sending via email: $msg") },
*levels
)
}
fun fileLogger(vararg levels: LogLevel): Logger {
return writeLogger(
Consumer { msg: String -> System.err.println("Writing to Log File: $msg") },
*levels
)
}
fun main() {
// Build an immutable chain of responsibility
val logger = consoleLogger(*LogLevel.all())
.appendNext(emailLogger(LogLevel.FUNCTIONAL_MESSAGE, LogLevel.FUNCTIONAL_ERROR))
.appendNext(fileLogger(LogLevel.WARNING, LogLevel.ERROR))
// Handled by consoleLogger since the console has a LogLevel of all
logger.message("Entering function ProcessOrder().", LogLevel.DEBUG)
logger.message("Order record retrieved.", LogLevel.INFO)
// Handled by consoleLogger and emailLogger since emailLogger implements Functional_Error & Functional_Message
logger.message("Unable to Process Order ORD1 Dated D1 For Customer C1.", LogLevel.FUNCTIONAL_ERROR)
logger.message("Order Dispatched.", LogLevel.FUNCTIONAL_MESSAGE)
// Handled by consoleLogger and fileLogger since fileLogger implements Warning & Error
logger.message("Customer Address details missing in Branch DataBase.", LogLevel.WARNING)
logger.message("Customer Address details missing in Organization DataBase.", LogLevel.ERROR)
}
LogLevel.kt
enum class LogLevel {
INFO, DEBUG, WARNING, ERROR, FUNCTIONAL_MESSAGE, FUNCTIONAL_ERROR;
companion object {
public fun all(): Array<LogLevel> {
return values()
}
}
}
I don't see why you've added a
currentLogger
property that didn't exist in the original Java code.If you want the same behavior as in Java, where an implementation can override the default implementation of
appendNext()
, it would look like this:If you don't intend for this function to be overridden, it would be more suitable to move it to an extension function. Then "overriding" it would require composing another extension function with the same signature and importing that one instead to use it. This is how the standard library functions are organized. Still not foolproof, but putting the function in the interface would more strongly suggest that it is meant to be overridden.
Edit: Also, you should not need to use Consumer, since in Kotlin functions are first class types. For example, replace
Consumer<String>
with(String) -> Unit
and then call it directly withstringConsumer(message)
instead ofstringConsumer.accept(message)
.