DEV Community

duylv
duylv

Posted on • Edited on

Effective Enum

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);
}
Enter fullscreen mode Exit fullscreen mode

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;

}
Enter fullscreen mode Exit fullscreen mode

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");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Solution Recap

The code works fine. Let's analyze the above solution a bit.

  1. More logic: We need a logic to check the bank type and then use it to determine the start and end working hours.

  2. 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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;

}
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

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);
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Solution Recap

  1. Entity Encapsulation: All components related to a bank are encapsulated within the Bank Enum, including bank properties and cut-off time logic.

  2. 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.

  3. 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;
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)