TIL 4: Enums on steroid

Aliaksandr Rasolka
4 min readMay 25, 2018

--

What if you can dramatically improve you enum with functions?

Photo by Nathan Dumlao on Unsplash

Image that you write code for “Good People Coffee” company and you need to somehow describe all drinks provided by you company.

This case you just write down simple enum class with drinks list:

public class GoodPeopleCoffeeCompany {
public enum Drink {
ESPRESSO,
TOAST,
TEA
}
}

Good. Next you set price for items:

public class GoodPeopleCoffeeCompany {
public enum Drink {
ESPRESSO(1),
TOAST(2),
TEA(3);

private final double price;

Drink(final double price) {
this.price = price;
}

public double getPrice() {
return price;
}
}
}

Looks good. This case you can easily access drink price and do what ever you want. But image, your businesses grow fast and some day you offer 40% discount for users. Let’s do it:

public static void main(String[] args) {
final double discount = 0.6;
final Drink drink = Drink.valueOf(args[0]);
final double finalPrice = drink.getPrice() * discount;
}

Look good too. But… what if only tea will got discount with 40%? You code get additional verification statement for tea:

public static void main(String[] args) {
final double teaDiscount = 0.6;
final Drink drink = Drink.valueOf(args[0]);
final double initialPrice = drink.getPrice();
final double finalPrice = drink.equals(Drink.TEA)
? initialPrice * teaDiscount
: initialPrice;
}

Hm… let it be. What if next week espresso also will be on discount with, but only for 20%? Change you enum a little.

enum Drink {
ESPRESSO(1, 0.8),
TOAST(2, 1),
TEA(3, 0.6);

private final double price;
private final double discount;

Drink(final double price, final double discount) {
this.price = price;
this.discount = discount;
}

public double getPrice() {
return price;
}

public double getDiscount() {
return discount;
}
}

And get discounted price:

public static void main(String[] args) {
final Drink drink = Drink.valueOf(args[0]);
final double initialPrice = drink.getPrice();
final double finalPrice = drink.getPrice() * drink.getDiscount();
}

Well. “I thinks enough — looks good”. But!!1 Hell no, what you will do if database?! It’s easy, just tune you enum again:

enum Drink {
ESPRESSO(DataBase.getEspressoPrice(),
DataBase.getEspressoDiscout()),
TOAST(DataBase.getToastPrice(), DataBase.getToastDiscout()),
TEA(DataBase.getTeaPrice(), DataBase.getTeaDiscout());

private final double price;
private final double discount;

Drink(final double price, final double discount) {
this.price = price;
this.discount = discount;
}

public double getPrice() {
return price;
}

public double getDiscount() {
return discount;
}
}

Cool. But, what if dev, test, uat, prod environments? What if DEPENDENCY INJECTION??1

Houston, we got a problem!

Can’t properly inject to static class. Lets refactor this weird enum!

In this case — I will create service that calculate final price for me. Let’s call it PriceService . As long as service class isn’t static — I can easily inject database class.

Create price service:

class PriceService {
@Inject
private DataBase dataBase;

double computeEspressoPrice(final double discount) {
return dataBase.getEspressoPrice() * discount;
}

double computeToastPrice(final double discount) {
return dataBase.getToastPrice() * discount;
}

double computeTeaPrice(final double discount) {
return dataBase.getTeaPrice() * discount;
}

double computePrice(final Drink drink, double discount) {
return drink.getPrice().apply(this, discount);
}
}

Add bi-function to enum:

enum Drink {
ESPRESSO(PriceService::computeEspressoPrice),
TOAST(PriceService::computeToastPrice),
TEA(PriceService::computeTeaPrice);

public final BiFunction<PriceService, Double, Double> price;

Drink(BiFunction<PriceService, Double, Double> price) {
this.price = price;
}

public BiFunction<PriceService, Double, Double> getPrice() {
return price;
}
}

Use it:

class MainClass {
@Inject
private PriceService priceService;

public void main(String[] args) {
final Drink drink = Drink.valueOf(args[0]);
final double finalPrice = priceService.computePrice(drink, 0.6);
}
}

Welp! But, again — I must manually pass discount :( Don’t worry — just use function instead of bi-function. Reduce function parameter and get discount from database.

New enum:

enum Drink {
ESPRESSO(PriceService::computeEspressoPrice),
TOAST(PriceService::computeToastPrice),
TEA(PriceService::computeTeaPrice);

public final Function<PriceService, Double> price;

Drink(Function<PriceService, Double> price) {
this.price = price;
}

public Function<PriceService, Double> getPrice() {
return price;
}
}

New price service:

class PriceService {
@Inject
private DataBase dataBase;

double computeEspressoPrice() {
return dataBase.getEspressoPrice() *
dataBase.getEspressoDiscout();
}

double computeToastPrice() {
return dataBase.getToastPrice() *
dataBase.getToastDiscout();
}

double computeTeaPrice() {
return dataBase.getTeaPrice() *
dataBase.getTeaDiscout();
}

double computePrice(final Drink drink) {
return drink.getPrice().apply(this);
}
}

Use it:

class MainClass {
@Inject
private PriceService priceService;

public void main(String[] args) {
final Drink drink = Drink.valueOf(args[0]);
final double finalPrice = priceService.computePrice(drink);
}
}

Finally, you have flexible implementation that allow you to use any data base provider and any price service, so easy to switch, easy to test, easy to mock.

Easy to customize price calculation for each enum value type. Like:

class EnchancedPriceService {
@Inject
private DataBase dataBase;

double computeEspressoPrice() {
return dataBase.getEspressoPrice()
* dataBase.getToastDiscout()
* 0.1;
}

double computeToastPrice() {
return dataBase.getToastPrice()
* dataBase.getToastDiscout()
- 10;
}

double computeTeaPrice() {
return dataBase.getTeaPrice()
* dataBase.getTeaDiscout()
/ 2;
}

double computePrice(final Drink drink) {
return drink.getPrice().apply(this);
}
}

Even, if you add some new value to enum this construction require you to add method implementation for price service:

New enum value:

enum Drink {
MILK(PriceService::computeMilkPrice);
}

Create calculate method in price service:

class PriceService {
@Inject
private DataBase dataBase;

double computeMilkPrice() {
return dataBase.getMilkPrice() * dataBase.getMilkDiscout();
}
}

Perfect!

Happy codding, made you test a little functional! D_D

--

--