As developers, we are familiar with Enums, a data type capable of representing constants, entities, entity states, and more.
We frequently utilize Enums to solve various problems in our codes. In this post, we'll delve into effective strategies for utilizing Enums in our codebase. Let's take some notes to leverage them effectively.
State Management
One awesome thing that Enums bring us is help us to manage states of specific object by. By defining Enums to represent different states or statuses we can streamline state transitions and ensure code correctness.
For instance, let's say we got Bank Enum, contains TechBank, ACBank, ITBank. We need to implement simple cut-off time for them to ensure smooth processing and compliance within working hours, so each bank will need services working hours.
Let's say TechBank is 8:30 - 18:00, ACBank is 9:00 - 18:30, IT Bank is 9:30 - 18:00.
Without state management enums
We have 3 banks, each bank need to have start and end working hour, so we need 6 places to store. Let's declare 6 variables as constants.
BankConstants.java
import java.time.LocalTime;
public final class BankConstants {
public final static LocalTime TECH_BANK_START_WORKING_HOUR = LocalTime.of(8, 30, 0);
public final static LocalTime TECH_BANK_END_WORKING_HOUR = LocalTime.of(18, 0, 0);
public final static LocalTime AC_BANK_START_WORKING_HOUR = LocalTime.of(9, 0, 0);
public final static LocalTime AC_BANK_END_WORKING_HOUR = LocalTime.of(18, 30, 0);
public final static LocalTime IT_BANK_START_WORKING_HOUR = LocalTime.of(9, 30, 0);
public final static LocalTime IT_BANK_END_WORKING_HOUR = LocalTime.of(18, 0, 0);
}
Next, we need to write some code to handle cut-off time for bank. Let's say BankService.java will do that for us.
TransactionDTO.java
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
@Builder
@Data
public class TransactionDTO {
private String bankType;
private String accountNumber;
private BigDecimal amount;
}
BankService.java
import lombok.extern.log4j.Log4j2;
import java.time.LocalTime;
import org.springframework.stereotype.Service;
@Log4j2
@Service
public class BankService {
public void processTransaction(TransactionDTO txn) throws Exception {
LocalTime currTime = LocalTime.now();
LocalTime startWorkingHour;
LocalTime endWorkingHour = switch (txn.getBankType()) {
case "TechBank" -> {
startWorkingHour = BankConstants.TECH_BANK_START_WORKING_HOUR;
yield BankConstants.TECH_BANK_END_WORKING_HOUR;
}
case "ACBank" -> {
startWorkingHour = BankConstants.AC_BANK_START_WORKING_HOUR;
yield BankConstants.AC_BANK_END_WORKING_HOUR;
}
case "ITBank" -> {
startWorkingHour = BankConstants.IT_BANK_START_WORKING_HOUR;
yield BankConstants.IT_BANK_END_WORKING_HOUR;
}
default -> throw new Exception("Invalid Bank Type");
};
boolean isAvailable = currTime.isAfter(startWorkingHour)
&& currTime.isBefore(endWorkingHour);
log.info(
"{}: Current time {}, start working hour {}, end working hour {}",
txn.getBankType(),
currTime,
startWorkingHour,
endWorkingHour
);
if (isAvailable) {
log.info("Process Transaction successfully for Bank Type: {}", txn.getBankType());
} else {
throw new Exception("Bank is out of service now");
}
}
}
DemoApplication.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.math.BigDecimal;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Autowired
BankService bankService;
@Bean
ApplicationRunner applicationRunner() {
return args -> {
TransactionDTO txn = TransactionDTO.builder()
.accountNumber("001-001-001")
.bankType("ITBank")
.amount(BigDecimal.TEN)
.build();
bankService.processTransaction(txn);
};
}
}
Result:
2024-04-02T13:29:59.729+07:00 INFO 16540 --- [ main] com.example.demo.Bank : ITBank: Current time 13:29:59.726404500, start working hour 09:00, end working hour 18:30
2024-04-02T13:29:59.730+07:00 INFO 16540 --- [ main] com.example.demo.BankService : Process Transaction successfully for Bank Type: ITBank
Process finished with exit code 0
Solution Recap
The code works fine. Let's analyze the above solution a bit.
More logic: We need a logic to check the bank type and then use it to determine the start and end working hours.
Code complexity: We can easily see that the Bank and its properties (start and end working hours) are disparate. Consider a scenario where we need to incorporate additional banks or introduce more properties for each bank to handle other business processes during cutoff times. In such a situation, we would need to (1) define more constants in BankConstants and (2) update the logic within BankService.processTransaction(txn) (Let's say this method is stable in production, now you need to update its logic).
The more banks and properties we have, the more code is added.
With state management enums
First, we will define an enums to represent for bank, each record will contains start and end working hour for bank.
Bank.java
package org.example;
import java.time.LocalTime;
public enum Bank {
TechBank(
LocalTime.of(8, 30, 0),
LocalTime.of(18, 0, 0)
),
ACBank(
LocalTime.of(9, 0, 0),
LocalTime.of(18, 30, 0)
),
ITBank(
LocalTime.of(9, 30, 0),
LocalTime.of(18, 0, 0)
);
private final LocalTime startWorkingHrs;
private final LocalTime endWorkingHrs;
Bank(final LocalTime startWorkingHrs, final LocalTime endWorkingHrs) {
this.startWorkingHrs = startWorkingHrs;
this.endWorkingHrs = endWorkingHrs;
}
public LocalTime getStartWorkingHrs() {
return startWorkingHrs;
}
public LocalTime getEndWorkingHrs() {
return endWorkingHrs;
}
}
Now we need some logic code to check if now the bank is available or not. We will have method isAvailable() to do that.
import lombok.extern.log4j.Log4j2;
import java.time.LocalTime;
@Log4j2
public enum Bank {
TechBank(
LocalTime.of(8, 30, 0),
LocalTime.of(18, 0, 0)
),
ACBank(
LocalTime.of(9, 0, 0),
LocalTime.of(18, 30, 0)
),
ITBank(
LocalTime.of(9, 30, 0),
LocalTime.of(18, 0, 0)
);
private final LocalTime startWorkingHrs;
private final LocalTime endWorkingHrs;
Bank(final LocalTime startWorkingHrs, final LocalTime endWorkingHrs) {
this.startWorkingHrs = startWorkingHrs;
this.endWorkingHrs = endWorkingHrs;
}
public LocalTime getStartWorkingHrs() {
return startWorkingHrs;
}
public LocalTime getEndWorkingHrs() {
return endWorkingHrs;
}
public boolean isAvailable() {
LocalTime currTime = LocalTime.now();
log.info(
"{}: Current time {}, start working hour {}, end working hour {}",
this,
currTime,
this.startWorkingHrs,
this.endWorkingHrs
);
return currTime.isAfter(this.startWorkingHrs) &&
currTime.isBefore(this.endWorkingHrs);
}
}
Next, we need to update TransactionDTO.bankType datatypes to enum, then update logic code in BankService.java as below.
TransactionDTO.java
import lombok.Builder;
import lombok.Data;
import org.example.enumerations.Bank;
import java.math.BigDecimal;
@Builder
@Data
public class TransactionDTO {
private Bank bankType;
private String accountNumber;
private BigDecimal amount;
}
BankService.java
import lombok.extern.log4j.Log4j2;
import org.example.dto.TransactionDTO;
import org.springframework.stereotype.Service;
@Log4j2
@Service
public class BankService {
public void processTransaction(TransactionDTO txn) throws Exception {
if (txn.getBankType().isAvailable()) {
log.info("Process Transaction successfully for Bank Type: {}", txn.getBankType());
} else {
throw new Exception("Bank is out of service now");
}
}
}
DemoApplication.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.math.BigDecimal;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Autowired
BankService bankService;
@Bean
ApplicationRunner applicationRunner() {
return args -> {
TransactionDTO txn = TransactionDTO.builder()
.accountNumber("001-001-001")
.bankType(Bank.ITBank)
.amount(BigDecimal.TEN)
.build();
bankService.processTransaction(txn);
};
}
}
Code looks good now, let's see how does it works
Result
2024-04-02T13:36:38.072+07:00 INFO 8424 --- [ main] com.example.demo.Bank : ITBank: Current time 13:36:38.069208300, start working hour 09:00, end working hour 18:30
2024-04-02T13:36:38.073+07:00 INFO 8424 --- [ main] com.example.demo.BankService : Process Transaction successfully for Bank Type: ITBank
Process finished with exit code 0
Solution Recap
Entity Encapsulation: All components related to a bank are encapsulated within the Bank Enum, including bank properties and cut-off time logic.
Reduced Code Complexity: Elimination of code logic for checking bank type to retrieve bank properties. Any additions of new banks or properties only require updating the Bank Enum, with no need for changes elsewhere.
Documentation: With all relevant details centralized within the Bank Enum, it serves as a comprehensive document for reference when needed.
Enum & Functional Programming
One drawback of using enums in many programming languages is that you can't modify their properties without modifying the source code and recompiling the application. Enums are typically used to represent a fixed set of constants, and their values are usually determined at compile-time.
How do we update enum properties without modifying the source code? Using enums along with functional programming will help to solve that.
We will use the above example to do that. The idea is that we have an object to obtain bank properties, which can be a configuration file, an entity object (queried from a database), and more. For this example, we will use a configuration object called BankConfig.java.
BankConfig.java
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import java.time.LocalTime;
@Getter
@Configuration
public class BankConfig {
@Value("${bank.cutoff-time.techbank.start-working-hrs}")
private LocalTime techBankStartWorkingHrs;
@Value("${bank.cutoff-time.techbank.end-working-hrs}")
private LocalTime techBankEndWorkingHrs;
@Value("${bank.cutoff-time.itbank.start-working-hrs}")
private LocalTime ithBankStartWorkingHrs;
@Value("${bank.cutoff-time.itbank.end-working-hrs}")
private LocalTime itBankEndWorkingHrs;
@Value("${bank.cutoff-time.acbank.start-working-hrs}")
private LocalTime acBankStartWorkingHrs;
@Value("${bank.cutoff-time.acbank.end-working-hrs}")
private LocalTime acBankEndWorkingHrs;
}
application.yml
bank:
cutoff-time:
techbank:
start-working-hrs: "08:30"
end-working-hrs: "18:00"
itbank:
start-working-hrs: "09:00"
end-working-hrs: "18:30"
acbank:
start-working-hrs: "09:30"
end-working-hrs: "18:00"
Next, we need to modify Bank.java Enum. Let's replace start working hours and end working hours properties by using functional programming to get values from BankConfig.java (properties file).
Bank.java
import lombok.extern.log4j.Log4j2;
import java.time.LocalTime;
import java.util.function.Function;
@Log4j2
public enum Bank {
TechBank(
BankConfig::getTechBankStartWorkingHrs,
BankConfig::getTechBankEndWorkingHrs
),
ACBank(
BankConfig::getAcBankStartWorkingHrs,
BankConfig::getAcBankEndWorkingHrs
),
ITBank(
BankConfig::getIthBankStartWorkingHrs,
BankConfig::getItBankEndWorkingHrs
);
private final Function<BankConfig, LocalTime> startWorkingHrsConsumer;
private final Function<BankConfig, LocalTime> endWorkingHrsConsumer;
Bank(Function<BankConfig, LocalTime> startWorkingHrsConsumer, Function<BankConfig, LocalTime> endWorkingHrsConsumer) {
this.startWorkingHrsConsumer = startWorkingHrsConsumer;
this.endWorkingHrsConsumer = endWorkingHrsConsumer;
}
public boolean isAvailable(BankConfig bankConfig) {
LocalTime currTime = LocalTime.now();
LocalTime startWorkingHrs = this.startWorkingHrsConsumer.apply(bankConfig);
LocalTime endWorkingHrs = this.endWorkingHrsConsumer.apply(bankConfig);
log.info(
"{}: Current time {}, start working hour {}, end working hour {}",
this,
currTime,
startWorkingHrs,
endWorkingHrs
);
return currTime.isAfter(startWorkingHrs) &&
currTime.isBefore(endWorkingHrs);
}
}
BankService.java must provide BankConfig when using method isAvailable
BankService.java
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Log4j2
@Service
public class BankService {
@Autowired
private BankConfig bankConfig;
public void processTransaction(TransactionDTO txn) throws Exception {
if (txn.getBankType().isAvailable(bankConfig)) {
log.info("Process Transaction successfully for Bank Type: {}", txn.getBankType());
} else {
throw new Exception("Bank is out of service now");
}
}
}
Now when we want to change Bank properties, we just need to update properties file. I will try to update application.yml to make ITBank unavailable then run application to view how it works.
application.yml
bank:
cutoff-time:
techbank:
start-working-hrs: "08:30"
end-working-hrs: "18:00"
itbank:
start-working-hrs: "09:00"
end-working-hrs: "10:30"
acbank:
start-working-hrs: "09:30"
end-working-hrs: "18:00"
Result
Caused by: java.lang.Exception: Bank is out of service now
at com.example.demo.BankService.processTransaction(BankService.java:18) ~[classes/:na]
at com.example.demo.DemoApplication.lambda$applicationRunner$0(DemoApplication.java:29) ~[classes/:na]
at org.springframework.boot.SpringApplication.lambda$callRunner$4(SpringApplication.java:786) ~[spring-boot-3.2.4.jar:3.2.4]
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:83) ~[spring-core-6.1.5.jar:6.1.5]
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60) ~[spring-core-6.1.5.jar:6.1.5]
... 17 common frames omitted
Process finished with exit code 1
Solution Recap
In short, it increases flexibility and maintainabilit, using enums with functional programming gives me a good option to modify enum properties. Now, whenever we want to update them, we don't need to modify the source code, we can simply modify the .properties file or database records (in case our reference object is queried from the database).
Top comments (0)