본문으로 바로가기

Java 8 Lambda를 이용한 Decorate pattern

category 개발이야기/Java 2017. 12. 15. 01:21
반응형


지난 포스팅에 이어 Decorate pattern을 lambda로 바꾸는 방법에 대해서 설명합니다.

시작하기에 앞서. stream, Functional Interface, lambda expression에 대한 아주 조그만?? 이해가 필요합니다.


예제를 보다가 코드가 헷깔리신다면 위 세가지를 다시한번 리뷰하고 오시면 됩니다.

Lambda로 decorate pattern을??

decorate pattern은 말 그래로 장식하는 패턴입니다.

한가지 동작후에 나온 결과물을 다시 가공하는 형태인거죠. 이미 많이 써봤던 file IO의  BufferedStream이나, Bufferedxx로 시작하는 class들은 decorate pattern을 구현한 아주 대표적인 것들 입니다.

그래서 사용법도 아래와 같습니다.

FileWriter fw = new FileWriter("memo");
BufferedWriter bfw = new BufferedWriter(fw, 1024);
....

filterWirter를 만들어서 그냥 써도 되지만 속도 향상을 위해 BufferedWriter로 한번 감쌈니다.

BufferedWriter가 fw를 인자로 받아가고 있네요. 


여기서는 Decorate pattern을 lambda를 이용해서 구현하는 방법을 얘기합니다.

(일반적으로 java로 작성하는 부분에 대해서는 스킵 합니다. 기본 class diagram 이라든가..예제는 검색하시면 많이 나옵니다)


일단은 제가 Decorate pattern을 기억할때 항상 떠올리는 던전게임 예제를 들어 보겠습니다.


공격력과 방어력을 장식(Decorate) 합니다

게임을 하나 만든다고 칩니다.

던전을 돌면서 싸움을 하는데, 몬스터를 때려잡고, 아니면 길을 잘 헤메이다 보면 아이템을 획득할 수 있습니다.


따라서 아이템을 줍거나 착용함에 따라 공격력과 방어력이 상승한다고 가정합니다.

아이템에 따라서 방어력을 %로 올린다거나, 그냥 +point로 올라가기도 합니다.


먼저 기본적인 부분을 정해 보겠습니다.

능력(Availability)라는 class를 만들고 멤버변수로 공격력과 방어력값을 각각 갖도록 하겠습니다.

완전 무능력하면 게임 할 맛이 안날테니 기본값을 각각 100씩 줘 봅시다~

public class Availability {        
    private long mAttact = 100;
    private long mDefence = 100;
...

이제 공격력과 방어력을 아이템을 set 할 수 있는 setter를 만들려고 합니다.

item을 여러개 장착할수 있으니, 장착된 item을 ArrayList에 넣겠습니다.

ArrayList에는 각 아이템에 따라 얼마큼 능력을 올릴지를 넣고 싶습니다.

만약 ArrayList에 어떤 아이템 종류를 넣는다면, ArrayList의 값을 합산하는 과정에서 아이템에 따라서 if~ else 또는 switch문으로 분기해야 합니다. 어떤 item은 합산이고, 어떤 아이템은 %값으로 곱셈이 되기 때문입니다.


 이 분기로 인해서 각각의 동작을 넣는다면 엄청나게 피곤한 형태의 코드가 됩니다. 저 레벨 코드가 되기도 하지만, 궁극적으로 분기문이 길어지는건 이미 많은 문제를 안고 있습니다. 

(분기에 따른 가독성 저하는 물론이고, 아이템이 추가되거나 삭제될때마다 해당 부분의 코드가 변경되면서 side effect 발생 가능성이 높아집니다. 물론 테스트의 어려움도 생기겠죠.)


따라서 Availability 클래스에는 분기문을 넣지 않지 않고, 각각의 아이템이 어떤 동작을 할지는 호출하는 부분에서 함수로 넘겨주겠습니다.

일단 Enum을 하나 만들어서 item을 정의하고, 아이템별 상승하는 값을 넣어줍니다.

enum Items {
    STICK(100),
   	RED_STONE(1.05), /* 소수점값은 %를 나타냅니다. -> 5% */
   	SPECIAL_SWORD(200),
	BLUE_DRINK(100),
   	BLUE_STONE(1.05),
	GOLD_ARMOR(200);
	
	private double mVal;
	
	private Items(double val) {
	    mVal = val;
	}
	
	public double getValue() {
	    return mVal;
	}	    
}

어떤값은 +를 해야하고 어떤값은 %로 곱해야 합니다.

그 결정은 Availability 클래스를 호출하는 부분에서 만들어서 넘겨줍니다.

함수 형태로 넘겨야 하니, T->T method signature를 갖는 Function을 쓰고, item을 set할 때마다 ArrayList에 Function을 담도록 코딩해 보겠습니다.

public class Availability {
    ... 

    private ArrayList<Function<Long, Long>> mAttactDecorators;
    private ArrayList<Function<Long, Long>> mDefenceDecorators;
    
    public Availability() {
	    mAttactDecorators = new ArrayList<>();
    	mDefenceDecorators = new ArrayList<>();
    }
    
    public long totalAvaility() {
	    ...
    }
    
    public void setAttactItem(final Function<Long, Long> operator) {
	    mAttactDecorators.add(operator);
    }
    
    public void setDefenceItem(final Function<Long, Long> operator) {
	    mDefenceDecorators.add(operator);
    }
}


이제 호출부에서는 setAttactItem이나 setDefenceItem을 이용하여 아이템을 ArrayList에 담습니다.

그리고 호출부에서 Function 을 구현해서 넘겨줍니다.

-

-


Function안에 어떤 동작이 있을지 Availability class는 알 이유가 없습니다.

다만 totalAvaility() 함수에서는 list에 담긴 공격력과 방어력을 합산하여 능력치를 반환하도록 합니다.

public long totalAvaility() {
    Function<Long, Long> attactFun = mAttactDecorators.stream()
    	.reduce((item0, item1) -> item0.andThen(item1))
		.orElseGet(Function::identity);
	
	Function<Long, Long> defenceFunc = mDefenceDecorators.stream()
		.reduce((item0, item1) -> item0.andThen(item1))
		.orElseGet(Function::identity);
	
	    
	mAttact = attactFunc.apply(mAttact);	
	mDefence = defenceFunc.apply(mDefence);
	
	return mAttact + mDefence;
}	

list를 stream()으로 만들고 reduce를 이용하여 한개로 합칩니다.

이때 합치는 작업은 andThen() 함수를 이용합니다.


andThen()과 compose()는 Function에 정의된 default method로 여러개의 function을 연결하는 역할을 합니다.

간단하게 예를 들면 아래와 같습니다.

Function<String, String> first = input -> input + " 1 ";
Function<String, String> second = input -> input + " 2 ";

String temp1 = first.appy("짜란");
String temp2 = seconde.apply(temp1);

/* temp2에는 "짜란 1 2"가 들어간다 */

Function<String, String> third = first.andThen(second);
String temp3 = third.apply("짜란");

/* temp3에도 "짜란 1 2"가 들어간다 */

Function<String, String> forth = first.compose(second);
String temp4 = third.apply("짜란");

/*temp4에는 "짜란 2 1"이 들어간다 */

andThen으로 Function을 연결하면 앞 Function의 결과값이 뒤 Function의 인자값으로 넘어갑니다.


그럼 완성된 Availability class를 보시죠.

public class Availability {
        
    private long mAttact = 100;
    private long mDefence = 100;
    
    private ArrayList<Function<Long, Long>> mAttactDecorators;
    private ArrayList<Function<Long, Long>> mDefenceDecorators;
    
    public Availability() {
	    mAttactDecorators = new ArrayList<>();
	    mDefenceDecorators = new ArrayList<>();
    }
    
    public long totalAvaility() {
	    Function<Long, Long> attactFunc = mAttactDecorators.stream()
		    .reduce((item0, item1) -> item0.andThen(item1))
		    .orElseGet(Function::identity);
	
    	Function<Long, Long> defenceFunc = mDefenceDecorators.stream()
		    .reduce((item0, item1) -> item0.andThen(item1))
		    .orElseGet(Function::identity);	
	    
	    mAttact = attactFunc.apply(mAttact);	
	    mDefence = defenceFunc.apply(mDefence);
	
	    return mAttact + mDefence;
    }
    
    public void setAttactItem(final Function<Long, Long> operator) {
	    mAttactDecorators.add(operator);
    }
    
    public void setDefenceItem(final Function<Long, Long> operator) {
	    mDefenceDecorators.add(operator);
    }
}

Availability class에는 item의 종류도, 어떤 분기도, 어떤 작업에 대한 명시도 없습니다.

어떤 작업을 해야하는지의 구현은 호출부에서 정의됩니다.


아래는 호출부인 main 함수 입니다.

public static void main(String[] args) {
	Availability avail = new Availability();	
	
	System.out.println("막대기를 주웠습니다. 공격력 + 100");
	avail.setAttactItem(attactPoint ->
                        attactPoint + (long)Items.STICK.getValue());
	System.out.println("빨간 보석을 착용합니다. 공격력 5% 증가");
	avail.setAttactItem(attactPoint ->
                        Math.round(attactPoint * Items.RED_STONE.getValue()));
	System.out.println("희귀한 검을 받았습니다. 공격력 +200 증가");
	avail.setAttactItem(attactPoint ->
                        attactPoint + (long)Items.SPECIAL_SWORD.getValue());

	System.out.println("파란 물약을 먹습니다. 방어력 + 100");
	avail.setDefenceItem(defencePoint ->
                         defencePoint + (long)Items.BLUE_DRINK.getValue());
	System.out.println("파란 보석을 착용합니다. 방어력 5% 증가");
	avail.setDefenceItem(defencePoint ->
                         Math.round(defencePoint * Items.BLUE_STONE.getValue()));
	System.out.println("황금갑옷을 주웠습니다. 방어력 +200 증가");
	avail.setDefenceItem(defencePoint ->
                         defencePoint + (long)Items.GOLD_ARMOR.getValue());
	
	System.out.println("전체 능력: " + avail.totalAvaility());

    }

막대기를 주웠습니다. 공격력 + 100 

빨간 보석을 착용합니다. 공격력 5% 증가 

희귀한 검을 받았습니다. 공격력 +200 증가 

파란 물약을 먹습니다. 방어력 + 100 

파란 보석을 착용합니다. 방어력 5% 증가 

황금갑옷을 주웠습니다. 방어력 +200 증가 

전체 능력: 820

반응형