Using two languages causes you try to apply some concepts from one language to the other. Scala developers often say they started to commonly use final
word with class field in Java. Similarly, one language often evolves inspired by other languages. For example, from Java 8 developers can use deafult
interface method (similar to Scala trait
), from Java 10 there is var
keyword for declaring local variables, pattern matching implemented with switch
statements gone through a rapid evolution since Java 7.
I would like to share with you how Scala improves my Java code. In this post I will describe why I started commonly use inner static classes, enums and interfaces.
It is common practice to defining every Java class in separated file. You often meet every class, enum and interface placed in dedicated file. Let's start with example. Consider service to customer payments history verification (CustomerVerificationService
). It can result with positive or negative loan decision. Verification consists of few steps: first system checks if customer is blacklisted, next if customer has internal debts (in relation to our company), at the end if external services reveals customer debts.
CustomerVerificationService#verifyPaymentHistory
can return:
-
BlackListedCustomer(String reason)
- customer is blacklisted, credit decision is negative; result contains reason of blacklisting. -
InternalDebt(String agreementNumber, BigInteger debtAmount)
- customer isn't blacklisted, but is our company debtor, credit decision is negative; result contains debt agreement number and debt value. -
ExternalDebt(String debtSource, BigInteger debtAmount)
- customer isn't blacklisted, has no debt in our company but external services reveal debt, credit decision is negative; result contains debt value and its source (external service name). -
PositiveDecision(BigInteger maxAmount)
- customer is not blacklisted and has no debts (internal or external), credit decision is positive; result contains maximum allowed loan value.
Above service interface can be implemented with Java 11 (LTS) as following:
public class CustomerVerificationService {
public VerificationResult verifyPaymentHistory(Customer customer) {
//omitted code
return new PositiveDecision(BigInteger.valueOf(6000L));
}
}
public class Customer {
private final String pesel;
private final String firstName;
private final String lastName;
private final String idNumber;
public Customer(String pesel, String firstName, String lastName, String idNumber) {
this.pesel = pesel;
this.firstName = firstName;
this.lastName = lastName;
this.idNumber = idNumber;
}
public String getPesel() {
return pesel;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getIdNumber() {
return idNumber;
}
}
public interface VerificationResult {
}
public class BlackListedCustomer implements VerificationResult {
private final String reason;
public BlackListedCustomer(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
}
public class InternalDebt implements VerificationResult {
private final String agreementNumber;
private final BigInteger debtAmount;
public InternalDebt(String agreementNumber, BigInteger debtAmount) {
this.agreementNumber = agreementNumber;
this.debtAmount = debtAmount;
}
public String getAgreementNumber() {
return agreementNumber;
}
public BigInteger getDebtAmount() {
return debtAmount;
}
}
public class ExternalDebt implements VerificationResult {
private final String debtSource;
private final BigInteger debtAmount;
public ExternalDebt(String debtSource, BigInteger debtAmount) {
this.debtSource = debtSource;
this.debtAmount = debtAmount;
}
public String getDebtSource() {
return debtSource;
}
public BigInteger getDebtAmount() {
return debtAmount;
}
}
public class PositiveDecision implements VerificationResult {
private final BigInteger maxAmount;
public PositiveDecision(BigInteger maxAmount) {
this.maxAmount = maxAmount;
}
public BigInteger getMaxAmount() {
return maxAmount;
}
}
We have here as many as 7 files. Situation is quite different in Scala, single file often contains definition of several classes
, objects
, traits
,case classes
. For example:
class CustomerVerificationService {
def verifyPaymentHistory(customer: Customer): Result = {
//omitted code
PositiveDecision(6000)
}
}
object CustomerVerificationService {
case class Customer(pesel: String, firstName: String, lastName: String, idNumber: String)
sealed trait Result
object Results {
case class InternalDebt(agreementNumber: String, debtAmount: BigInt) extends Result
case class ExternalDebt(debtSource: String, debtAmount: BigInt) extends Result
case class BlackListedCustomer(reason: String) extends Result
case class PositiveDecision(maxAmount: BigInt) extends Result
}
}
Apart from the huge difference in code volume, pay attention how Scala example nicely encapsulates service interface. Service CustomerVerificationService
has single public method verifyPaymentHistory
. In the same file you will find definition of method input: Customer
class. Every possible result of this operation is instance of Result
trait. Moreover, all Result
descendant are grouped in Results
object. Thanks to it, if you will start typing CustomerVerificationService.Results.
IDE will prompt with all possible Results. Once again, pay attention how comfortably you can get all information about CustomerVerificationService#verifyPaymentHistory
. You need to look only into single file. But good news! You can write
in a similar manner in Java! All you need to do is use inner static classes and inner interfaces.
public class CustomerVerificationService {
public Result verifyPaymentHistory(Customer customer) {
//omitted code
return new Results.PositiveDecision(BigInteger.valueOf(6000L));
}
public static class Customer {
private final String pesel;
private final String firstName;
private final String lastName;
private final String idNumber;
public Customer(String pesel, String firstName, String lastName, String idNumber) {
this.pesel = pesel;
this.firstName = firstName;
this.lastName = lastName;
this.idNumber = idNumber;
}
public String getPesel() {
return pesel;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getIdNumber() {
return idNumber;
}
}
public interface Result { }
public interface Results {
class BlackListedCustomer implements Result {
private final String reason;
public BlackListedCustomer(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
}
class InternalDebt implements Result {
private final String agreementNumber;
private final BigInteger debtAmount;
public InternalDebt(String agreementNumber, BigInteger debtAmount) {
this.agreementNumber = agreementNumber;
this.debtAmount = debtAmount;
}
public String getAgreementNumber() {
return agreementNumber;
}
public BigInteger getDebtAmount() {
return debtAmount;
}
}
class ExternalDebt implements Result {
private final String debtSource;
private final BigInteger debtAmount;
public ExternalDebt(String debtSource, BigInteger debtAmount) {
this.debtSource = debtSource;
this.debtAmount = debtAmount;
}
public String getDebtSource() {
return debtSource;
}
public BigInteger getDebtAmount() {
return debtAmount;
}
}
class PositiveDecision implements Result {
private final BigInteger maxAmount;
public PositiveDecision(BigInteger maxAmount) {
this.maxAmount = maxAmount;
}
public BigInteger getMaxAmount() {
return maxAmount;
}
}
}
}
Comparing to Scala example there still is much more code lines. But the main advantage of this approach is better encapsulation. You don't create separated file for every interface and class. Especially since they don't make sense outside the context of CustomerVerificationService
class. Pay also attention I changed name of VerificationResult
interface to Result
. Due to the fact Result
is part of CustomerVerificationService
class, its name may be less precise.
But still, if code volume bother you, Java14 comes to the rescue. More specifically Java 14 introduces record
keyword which is closer to Scala class
even than Java class
. With Java record
you can reduce earlier implementation of BlackListedCustomer
to one line:
record BlackListedCustomer(String reason) implements Result { }
The Java compiler auto generates getter methods, toString()
, hashcode()
and equals()
methods, so you don't have to write that boilerplate code yourself. Since a Java record
is immutable, no setter methods are generated. Final version (in Java 14) may look like:
public class CustomerVerificationService {
public Result verifyPaymentHistory(Customer customer) {
//omitted code
return new Results.PositiveDecision(BigInteger.valueOf(6000L));
}
public static record Customer(String pesel, String firstName, String lastName, String idNumber) { }
public interface Result { }
public interface Results {
record BlackListedCustomer(String reason) implements Result { }
record InternalDebt(String agreementNumber, BigInteger debtAmount) implements Result { }
record ExternalDebt(String debtSource, BigInteger debtAmount) implements Result { }
record PositiveDecision(BigInteger maxAmount) implements Result { }
}
}
Look how above implementation is similar to the Scala example.
In this post I wanted to show how nicely and easy you can encapsulate your code using inner static classes, enums and interfaces. Pay also attention how Java evolution made it simpler. Java 14 record
keyword hiding boilerplate code and lets you focus on implementation.
Originally published at https://stepniewski.tech.
All Java examples from this post you will find at my github.
Top comments (0)