How to write Java's default method of a functional interface in Kotlin idiomatically?

652 views Asked by At

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()
        }
    }
}
2

There are 2 answers

3
Tenfour04 On BEST ANSWER

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:

fun interface Logger {
    fun message(message: String, severity: LogLevel)

    fun appendNext(nextLogger: Logger): Logger {
        return Logger { message, severity ->
            message(message, severity)
            nextLogger.message(message, severity)
        }
    }
}

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.

fun interface Logger {
    fun message(message: String, severity: LogLevel)
}

fun Logger.appendNext(nextLogger: Logger): Logger {
    return Logger { message, severity ->
        message(message, severity)
        nextLogger.message(message, severity)
    }
}

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 with stringConsumer(message) instead of stringConsumer.accept(message).

1
Михаил Нафталь On

The key to the best solution here is the usage of Functional interfaces (introduced in Kotlin 1.4)

Also to make code more concise and idiomatic consider using single-expression fuctions, remove types, which could be inferred, and rewrite EnumSet.copyOf(listOf(*levels)) to auxilary function without extra objects creation:

import java.util.*
import java.util.function.Consumer

fun interface Logger {
    fun message(msg: String, severity: LogLevel)
}

fun Logger.appendNext(nextLogger: Logger) = Logger { msg, severity ->
    message(msg, severity)
    nextLogger.message(msg, severity)
}

inline fun <reified E : Enum<E>> enumSetOf(e: Array<out E>): EnumSet<E> =
    EnumSet.noneOf(E::class.java).also { result -> e.forEach { result.add(it) } }

fun writeLogger(levels: Array<out LogLevel>, stringConsumer: Consumer<String>) = Logger { msg, severity ->
    if (severity in enumSetOf(levels)) {
        stringConsumer.accept(msg)
    }
}

fun consoleLogger(vararg levels: LogLevel) =
    writeLogger(levels) { msg -> System.err.println("Writing to console: $msg") }

fun emailLogger(vararg levels: LogLevel) =
    writeLogger(levels) { msg -> System.err.println("Sending via email: $msg") }

fun fileLogger(vararg levels: LogLevel) =
    writeLogger(levels) { msg -> System.err.println("Writing to Log File: $msg") }

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)
}