Design Patterns
We will analyze how some design patterns are implemented in both languages.
1.- Optional Pattern
In Java, Optional doesn't solve the Null Pointer Exception or NPE problem. It just wraps it and "protects" our return values.
Optional<String> getCity(String user) {
var city = getOptionalCity(user);
if (city != null)
return Optional.of(city);
else
return Optional.empty();
}
Optional.ofNullable(null)
.ifPresentOrElse(
email -> System.out.println("Sending email to " + email),
() -> System.out.println("Cannot send email"));
Optional is useful for returning types, but it should not be used on parameters or properties.
getPermissions(user, null);
getPermissions(user, Optional.empty()); // Not recommended
KOTLIN
Solution: Nullability is built into the type system. Kotlin embraces null.
String? and String are different types. T is a subtype of T?.
val myString: String = "hello"
val nullableString: String? = null // correct!!
In Kotlin, all regular types are non-nullable by default unless you explicitly mark them as nullable. If you don't expect a function argument to be null, declare the function as follows:
fun stringLength(a: String) = a.length
The parameter a has the String type, which in Kotlin means it must always contain a String instance and it cannot contain null.
An attempt to pass a null value to the stringLength(a: String) function will result in a compile-time error.
This works for parameters, return types, properties and generics.
val list: List<String>
list.add(null) // Compiler error
2.- Overloading Methods
void log(String msg) { ......... };
void log(String msg, String level) { ......... };
void log(String msg, String level, String ctx) { ......... };
KOTLIN
In kotlin we declare only one function, because we have default arguments and named arguments.
fun log(
msg: String,
level: String = "INFO",
ctx: String = "main"
) {
.........
}
log(level="DEBUG", msg="trace B")
3.- Utility static methods
final class NumberUtils {
public static boolean isEven(final int i) {
return i % 2 == 0;
}
}
In some projects we may end up declaring the same utility function more than once.
KOTLIN
fun Int.isEven() = this % 2 == 0 // Extension function
2.isEven()
4.- Factory
public class NotificationFactory {
public static Notification createNotification(
final NotificationType type
) {
return switch(type) {
case Email -> new EmailNotification();
case SMS -> new SmsNotification();
};
}
}
KOTLIN
In Kotlin a function is used instead of an interface.
// This would be a code smell in Java
fun Notification(type: NotificationType) = when(type) {
NotificationType.Email -> EmailNotification()
NotificationType.SMS -> SmsNotification()
}
}
val notification = Notification(NotificationType.Email)
5.- Singleton
// Much code, it's not even thread-safe
public final class MySingleton {
private static final MySingleton INSTANCE;
private MySingleton() {}
public static MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
}
KOTLIN
This pattern is built into the Kotlin language. It's lazy and thread-safe.
object Singleton {
val myProperty......
fun myInstanceMethod() {
...............
}
}
6.- Iterator
This can be applied only to collections, not to user defined classes.
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
var iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element); // A, B, C
}
KOTLIN
val list = listOf("A", "B", "C")
for (elem in list) {
println(elem)
}
This can be applied to any class that has the iterator operator function defined.
class School(
val students: List<Student> = listOf(),
val teachers: List<Teacher> = listOf()
)
operator fun School.iterator() = iterator<Person> { // Extension function
yieldAll(teachers)
yieldAll(students)
}
val mySchool = School()
for (person in mySchool) {
println(person)
}
Likewise, the operator function compareTo must be used to compare objects.
7.- Comparable
class School(val students: List<Student>, val teachers: List<Teacher>)
override fun School.compareTo(other: School) =
students.size.compareTo(other.students.size)
fun main() {
val school1 = School(listOf(Student("John"), Student("Alice")), listOf(Teacher("Mr. Smith")))
val school2 = School(listOf(Student("Bob"), Student("Eve"), Student("Carol")), listOf(Teacher("Mrs. Johnson")))
if (school1 > school2) {
println("$school1 has more students than $school1")
}
}
8.- Strategy pattern
Implementation with interfaces
This is the classical approach, shown in Kotlin.
fun interface PaymentStrategy {
fun charge(amount: BigDecimal) : PaymentState
}
Next, we implement the interface for all the different payment methods we want to support:
class CreditCardPaymentStrategy : PaymentStrategy {
override fun charge(amount: BigDecimal) : PaymentState = PaymentState.PAID
}
class PayPalPaymentStrategy : PaymentStrategy {
override fun charge(amount: BigDecimal) = PaymentState.PAID
}
This is the resulting class:
class ShoppingCart2(private val paymentStrategy: PaymentStrategy) {
fun process(totalPrice: BigDecimal) = paymentStrategy.charge(totalPrice)
}
Implementation with Function Types
This implementation is easier to read than the previous one, but it's less reusable and less maintainable.
class ShoppingCart(private val paymentProcessor: (BigDecimal) -> PaymentState) {
fun process(totalPrice: BigDecimal) = paymentProcessor(totalPrice)
}
typealias PaymentStrategy = (BigDecimal) -> PaymentState
class ShoppingCart(private val paymentProcessor: PaymentStrategy) {
fun process(totalPrice: BigDecimal) = paymentProcessor(totalPrice)
}
This is how it's used:
val creditCardPaymentProcessor = { amount: BigDecimal -> ... }
val payPalPaymentProcessor = { amount: BigDecimal -> ... }
**JAVA
In Java, function types have a strange syntax.
interface PaymentProcessor {
public Function<BigDecimal, PaymentState> process;
};
This is how it's used:
class creditCardPaymentProcessor implements PaymentProcessor {
@Override
public Function<BigDecimal, PaymentState> process = .....;
};
It's quite annoying having to create a class per strategy.
Top comments (1)
Kotlin "double" type system is not 100% null safe and, actually, suffers from several negative consequences. It encourages use of optional chaining, which, in turn, often is used to look deep into object structure, causing deep coupling of the code. Optional chaining is not composable, unlike monad transformations.
Extension methods actually suffer from the duplication much more than utility methods. Utility methods are grouped inside dedicated classes, but extension methods usually scattered across the whole code base and often remain invisible, and this encourages repeating them. Usually, there are rules for placement of extension methods to prevent uncontrolled scattering. Lack of tooling makes following these rules an additional task for code reviews, harming productivity.
I see no point in creating a dedicated class/interface for factory. Making it a method in the Notification makes much more sense. Full implementation would look like so:
Full code in Kotlin will be barely more concise.
Correct implementation of singleton in Java usually uses enum and is lazily loaded and thread safe.
Iterator is an interface and nothing prevents you from implementing it for your classes too.
Why not implement Comparable interface for your class directly, instead of using extension method? Actually, extension methods available in Java too with Manifold compiler plugin.
Functions have strange, inconsistent syntax in Kotlin. In Java, they follow general language design. And you don't have to implement class per strategy, nothing prevents you from passing lambda or, even better, method reference:
Overall, I think the comparison of Kotlin with Java makes not so much sense. In fact, it would be better for Kotlin if its proponents stop comparing it to Java, as Kotlin gradually loses points as Java evolves. The use of modern Java language features, combined with functional style and some non-traditional techniques, makes Java quite concise and expressive. Inherently more clean syntax, lacking zero-value noise of "fun" and ":", makes Java also more readable. You can take a look at this example, to get a glimpse of what modern Java can look like.