In my series of posts on why our team switched from Java to Kotlin, I have touched on how Kotlin more clearly communicates architectural and business decisions concerning nullability and immutability (specifically collections). Last post I described how the following signature was much clearer than the alternative in Java as it guarantees that it will not modify the list.
Since Java and Kotlin always store objects in collections by reference, even if the method can't change the list, it can possibly change the objects in the list. We probably would assume that the method above would probably not modify the User objects based on the single responsibility principle (i.e. calculate and set preferred user status on the objects as a side effect), but let's take this signature and let's assume that the User object has a "status" property that is set on the User by this method after looking at other metadata about the user:
Of course there are many benefits to making objects as immutable as possible, including clarity in cases like these. Item 15 in Effective Java describes some of them as well as how to make objects immutable in Java. The problem is that immutable objects are very unwieldy in Java, making the right road a hard road. To use immutable objects effectively, one would probably need to implement the Value Object Design Pattern. A Java User Value Object might look like this:
Although, Lombok makes immutability much easier, Kotlin allows for full language support for Value Objects rather than having your code instrumented by another library (as with Lombok). The fact that Kotlin makes immutable Value Objects much easier to implement and work with, lending to simpler code that is easier to reason about, is another reason our team moved to Kotlin.
// Kotlin
fun removePreferredUsers(List<User> list) : List<User>
There is still one thing that we cannot tell from this method signature. How can we tell whether the method will alter the User object even if it doesn't change the list I passed in? Is the returned list the same User objects or different ones?Since Java and Kotlin always store objects in collections by reference, even if the method can't change the list, it can possibly change the objects in the list. We probably would assume that the method above would probably not modify the User objects based on the single responsibility principle (i.e. calculate and set preferred user status on the objects as a side effect), but let's take this signature and let's assume that the User object has a "status" property that is set on the User by this method after looking at other metadata about the user:
// Java
public List calculateStatus(List users);
// Kotlin
fun calculateStatus(users: List) : List
To drive the point even further, consider this signature:
// Java
public User calculateStatus(User user);
// Kotlin
fun calculateStatus(user: User): User
There is really only one way to know whether the User object returned by the method is a copy (with perhaps one field overwritten) or the exact same object with a mutated field (without looking at them implementation that is). We must know whether the User object allows mutation of that field.Of course there are many benefits to making objects as immutable as possible, including clarity in cases like these. Item 15 in Effective Java describes some of them as well as how to make objects immutable in Java. The problem is that immutable objects are very unwieldy in Java, making the right road a hard road. To use immutable objects effectively, one would probably need to implement the Value Object Design Pattern. A Java User Value Object might look like this:
// Java
public class User {
private final String firstName;
private final String lastName;
private final UserStatus status;
private final String birthday;
private final String joinDate;
public User(String firstName, String lastName, UserStatus status, String birthday, String joinDate) {
this.firstName = firstName;
this.lastName = lastName;
this.status = status;
this.birthday = birthday;
this.joinDate = joinDate;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public UserStatus getStatus() {
return status;
}
public String getBirthday() {
return birthday;
}
public String getJoinDate() {
return joinDate;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(firstName, user.firstName) &&
Objects.equals(lastName, user.lastName) &&
status == user.status &&
Objects.equals(birthday, user.birthday) &&
Objects.equals(joinDate, user.joinDate);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, status, birthday, joinDate);
}
}
To change a single field on this object, you would have to copy of the fields to a new object and override the one field you wanted like so:
// Java
User newUser = new User(user.getFirstName(),
user.getLastName(),
UserStatus.PREFERRED,
user.getBirthday(),
user.getJoinDate());
If you really can't change to Kotlin, at least consider using Lombok. The following user is the equivalent of the class above except that it adds "with" methods that return copies of the object with one field changed. @Whither is an experimental feature though, so use with caution. For more information on these annotations, check out the Lombok documentation for Value Objects.
// Java
@Value
@Wither
public class User {
String firstName;
String lastName;
UserStatus status;
String birthday;
String joinDate;
}
With this class, you can set a user with a preferred status like so:
// Java
User newUser = user.withStatus(UserStatus.PREFERRED);
In contrast, a Kotlin Value Object looks like this:
// Kotlin
data class User(val firstName: String,
val lastName: String,
val status: UserStatus,
val birthday: String,
val joinDate: String)
In Kotlin, the keywords "val" and "var" signal that a property is immutable or mutable respectively. To create a new object with preferred status, you would use the following code:
// Kotlin
val newUser = user.copy(status = UserStatus.PREFERRED)
Note that you can pass as many named arguments to the "copy" method as there are fields. To modify more than one property in the Lombok Java implementation, you would have to chain calls to "with" for as many fields as you wanted to overwrite, creating intermediate objects each time, which is of course slightly less efficient.Although, Lombok makes immutability much easier, Kotlin allows for full language support for Value Objects rather than having your code instrumented by another library (as with Lombok). The fact that Kotlin makes immutable Value Objects much easier to implement and work with, lending to simpler code that is easier to reason about, is another reason our team moved to Kotlin.
No comments:
Post a Comment