Market btc = new Market("KRW-BTC", 10, CoinEnum.upbit);
Market eth = new Market("KRW-ETH", 20, CoinEnum.upbit);
Market matic = new Market("KRW-MATIC", 50, CoinEnum.upbit);
Market bidr = new Market("BIDRUSDT", 100, CoinEnum.binance);
Market inch = new Market("1INCHUSDT", 5000, CoinEnum.binance);
List<Market> markets = Arrays.asList(btc, eth, matic, bidr, inch);
5개의 마켓 정보를 가지고 있는 list가 있다고 작성하겠다
public static List<Market> filterUpibts(List<Market> marketList) {
List<Market> markets = new ArrayList<>();
for (Market market : marketList) {
if (market.getCoinEnum().equals(CoinEnum.upbit)) {
markets.add(market);
}
}
return markets;
}
이번에는 바이낸스 list를 가져와야 하는 경우가 생겨서 요구사항을 더 추가 했다.
public static List<Market> filterBinance(List<Market> marketList) {
List<Market> markets = new ArrayList<>();
for (Market market : marketList) {
if (market.getCoinEnum().equals(CoinEnum.binance)) {
markets.add(market);
}
}
return markets;
}
나는 당연하게 함수를 하나 더 만들어서 equals 부분을 변경하기만 했다.
그래서 마켓에 관련된 파라메타를 하나 추가하여 위 두개의 함수를 하나로 합쳤다.
public static List<Market> filterByMarket(List<Market> marketList, CoinEnum coinEnum) {
List<Market> markets = new ArrayList<>();
for (Market market : marketList) {
if (market.getCoinEnum().equals(coinEnum)) {
markets.add(market);
}
}
return markets;
}
이렇게 파라메타에서 마켓에 대한 정보를 받아 if문을 통해 작업이 마켓이 몇개가 추가되든
상관이 없어졌다.
몇일이 지나고 마켓이 가지고 있는 코인의 개수를 알고 싶어졌다. 그래서 특정 개수 이상의 코인을 가지고 있는 마켓 list를 출력하는 요구사항이 추가 되었다.
public static List<Market> filterCoinQuantity(List<Market> marketList, long coinQuantity) {
List<Market> markets = new ArrayList<>();
for (Market market : marketList) {
if (market.getCoinQuantity() > coinQuantity) {
markets.add(market);
}
}
return markets;
}
작성을 하다보니 coinEnum으로 비교해서 가져오는 함수와 코인 개수를 비교해서 가져오는 함수가 if문을 제외한 부분이 동일하다.
그래서 이것도 하나로 만들어야 한다는 생각으로 flag를 생각한다.
flag가 true 이면 coinEnum으로 조회, false이면 quantity로 조회!
filterMarkets(markets, CoinEnum.upbit, 0, true)
filterMarkets(markets, CoinEnum.binance, 30, false)
public static List<Market> filterMarkets(List<Market> marketList,
CoinEnum coinEnum,long coinQuantity, boolean flag) {
List<Market> markets = new ArrayList<>();
for (Market market : marketList) {
if (flag && market.getCoinEnum().equals(coinEnum) ||
!flag && market.getCoinQuantity() > coinQuantity) {
markets.add(market);
}
}
return markets;
}
나중에 마켓에 한글이름이 추가되어 한글이름만 출력하는 list가 생겼을 때 대응하기 힘들다.
이전의 요구사항을 보면
1. coinEnum이 upbit인가?
2. coinEnum이 binance인가?
3. coin 개수가 coinQuantity 개수 이상인가?
참이면 add 하였고 false이면 add를 하지 않았다.
참 또는 거짓을 반환하는 함수를 프리디케이트 라고 한다.
프리디케이트를 정의 해보자
public interface MarketPredicate {
boolean list(Market market);
}
public class MakretUpbitPredicate implements MarketPredicate{
@Override
public boolean list(Market market) {
return market.getCoinEnum().equals(CoinEnum.upbit);
}
}
public class MarketQuantityPredicate implements MarketPredicate {
@Override
public boolean list(Market market) {
return market.getCoinQuantity() > 30;
}
}
public static List<Market> filterMarkets(List<Market> marketList, MarketPredicate marketPredicate) {
List<Market> markets = new ArrayList<>();
for (Market market : marketList) {
if (marketPredicate.list(market)) {
markets.add(market);
}
}
return markets;
}
filterMarkets(markets, new MakretUpbitPredicate());
filterMarkets(markets, new MarketQuantityPredicate());
MakretUpbitPredicate를 넘기면 market이 upbit인지 확인하는 작업을 하게 될 것이고
MarketQuantityPredicate를 넘기면 coinQuantity가 30이상인지 확인하는 작업을 하게 될 것이다.
이런식으로 런타임에 필요한 알고리즘을 선택해서 대응하는 기법을 전략 패턴이라고 한다.
이제 Market에 관련된 어떤 요구사항이든 Predicate를 하나 만들어서 클래스를 하나 만들어 구현하면 된다.
더 개선 가능한 방법이 없는가?
일단 클래스를 줄이는 방법부터 생각해보자
자바에는 익명클래스가 존재한다. 익명 클래스는 선언과 인스턴스화를 동시에 할 수 있다.
즉 따로 클래스를 만들 필요가 없다.
filterMarkets(markets, new MarketPredicate() {
@Override
public boolean list(Market market) {
return market.getCoinEnum().equals(CoinEnum.binance);
}
});
filterMarkets(markets, new MarketPredicate() {
@Override
public boolean list(Market market) {
return market.getCoinQuantity() > 20;
}
});
이런식으로 익명 클래스를 사용하게 된다면 별도의 클래스를 만들지 않아도 구현이 가능하다.
하지만 아직도 return 부분을 제외한 나머지 부분은 중복이다.
자바8의 람다를 이용하면 가능하다 라고 말할 수 있다.
filterMarkets(markets, market -> market.getCoinEnum().equals(CoinEnum.binance));
filterMarkets(markets, market -> market.getCoinQuantity() > 20);
1. filterMarkets(markets, new MarketPredicate() {
@Override
public boolean list(Market market) {
return market.getCoinEnum().equals(CoinEnum.binance);
}
});
2. filter(markets, (Market m) -> CoinEnum.upbit.equals(m.getCoinEnum()));
1번을 2번처럼 변경 했다.
(Market m) -> CoinEnum.upbit.equals(m.getCoinEnum())
이것을 보기 편하게 변경하게 되면 아래와 같이 된다.
public boolean list(Market m){
return CoinEnum.upbit.equals(m.getCoinEnum());
}
이제 filterMarkets와 함께 보자
public boolean list(Market m){
return CoinEnum.upbit.equals(m.getCoinEnum());
}
public static List<Market> filterMarkets(List<Market> marketList, MarketPredicate marketPredicate) {
List<Market> markets = new ArrayList<>();
for (Market market : marketList) {
if (marketPredicate.list(market)) {
markets.add(market);
}
}
return markets;
}
이렇게 두개를 놓고 보자면
MarketPredicate marketPredicate에 (Market m) -> CoinEnum.upbit.equals(m.getCoinEnum())이 넘어 온다면
marketPredicate.list(market)에서 .list(market)가 public boolean list(Market m) 함수를 호출하는 것과 같다.
(Market m) -> CoinEnum.upbit.equals(m.getCoinEnum())이 의미하는 것은 파라메타로는 Market m 하나가 있고 m.getCoinEnum()이 CoinEnum.upbit이면 true를 반환하고 아니면 false를 반환한다.
true이면 add를 하고 false이면 넘어간다.
최대한 코드 중복을 해결 하였다고 본다.
마켓에 대하여 파라메타 비교에 해당하는 list를 반환하는 기능들을 이제 중복 코드 없이 관리 할 수 있게 되었다.
public static List<User> filterUsers(List<User> users, MarketPredicate marketPredicate)
위와 같은 함수를 하나 더 만들어서 사용해야 한다.
반환값과 파라메타 List<>타입 값만 다르지 filterMarkets이랑 다를게 없다.
자바에는 제너릭이라는 것이 있다.
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t : list) {
if (predicate.list(t)) {
result.add(t);
}
}
return result;
}
이런식으로 만들게 되면 T 로 선언되어 있으니 어떤 타입이든 사용할 수 있다. Market이 사용되든 User가 사용되는 모두 사용이 가능하다.
List<Market> markets_predicate = filter(markets, (Market m) -> CoinEnum.upbit.equals(m.getCoinEnum()));
List<Market> markets_stream = markets.stream().filter(market -> market.getCoinEnum().equals(CoinEnum.upbit)).collect(Collectors.toList());
첫번째꺼는 predicate를 이용해서 필터링을 한다.
두번째꺼는 stream의 filter()를 이용해서 필터링을 한다.
stream의 filter()에 대한 설명이다.
filter()안에 Predicate가 사용되고 있다.
어...그러면 custom predicate interface를 쓸게 아니라 그냥 list에서 stream을 통해 filter()를 가지고 뽑아내면 되는거 아닌가? 라는 생각을 가지게 되었다.
해당 케이스에서만 쓰는게 아니기에 확정할 수 있는건 없지만
둘다 predicate를 쓰게되니 filter()의 복잡도에 따라 케바케로 쓰게 되는 것 같다고 생각이 든다.