조금더 복잡한 만능리모컨 설계

 -우리가 6-1에서 만들었던 간단한 만능리모컨 프로토타입이 마음에 들었는지, 이번에는 업체에서 좀더 복잡한 만능리모컨 을 설꼐해달라고 부탁했다.

- 위와같이 3쌍의 On/Off 버튼이 있는 만능리모컨을 만든다고 가정해보자,

- 우리는 먼저 테스트용도로 이 리모컨에 GarageDoor, CDplayer, Light 를 제어해보기로 했다.

 

만능 리모컨을 테스트하게 될 3가지 Receiver들

 

 

- 가장먼저 Inovoker에 해당하는 리모컨부터 객체로 디자인해보자.

public class RemoteControl {
	Command[] onCommands;
	Command[] offCommands;
	Command undoCommand;

	public RemoteControl() {
		onCommands = new Command[6];
		offCommands = new Command[6];
		
		Command noCommand = new NoCommand();
		for (int i = 0; i < 6; i++) {
			onCommands[i] = noCommand;
			offCommands[i] = noCommand;
		}
		undoCommand = noCommand;
	}
	
	public void setCommand(int slot, Command onCommand, Command offCommand) {
		onCommands[slot] = onCommand;
		offCommands[slot] = offCommand;
	}
	
	public void onButtonPushed(int slot) {
		onCommands[slot].execute();
		undoCommand = onCommands[slot];
	}
	
	public void offButtonPushed(int slot) {
		offCommands[slot].execute();
		undoCommand = onCommands[slot];
	}
	
	public void undoButtonPushed() {
		undoCommand.undo();
	}
	
	@Override
	public String toString() {
		StringBuffer st = new StringBuffer();
		st.append("\n-------- Remote Control-------------\n");
		for (int i = 0; i < onCommands.length; i++) {
			st.append("[slot"+ i + "\n"+ onCommands[i].getClass().getCanonicalName()+ " "
					+ "  "+ offCommands[i].getClass().getName()+"\n");
		}
		
		return st.toString();
	}
}

 

- 위와같이 On, Off Command를 6개씩 넣을 수 있는 배열을 내부변수로 가지고있고, 또한 undo를 할 수있는 command를 저장하였다.

- 또한 상태 확인을 위해 toString 객체 또한 Override 하여 만들어 주었다.

 

 

-이제 다음으로 Invoker에서 트리거 되는 command 객체를 만들어보자.

 

 

- 위와 같이 Receiver에 행동 하나하나가 그에 대응되는 Command 객체로 만들어진다.

- 많은 커맨드가 있지만 위 Command중 로직이 가장 복잡한 Cdplayer Command 만 만들어보자.

public class CdPlayerOnCommand  implements Command{
	CdPlayerReceiver cdPlayer;
	
	public CdPlayerOnCommand(CdPlayerReceiver cdPlayer) {
		this.cdPlayer = cdPlayer;
	}

	@Override
	public void execute() {
		cdPlayer.on();
		cdPlayer.setCd();
		cdPlayer.setVolume(11);
	}
}

 

public class CdPlayerOffCommand  implements Command{
	CdPlayerReceiver cdPlayer;
	
	public CdPlayerOffCommand(CdPlayerReceiver cdPlayer) {
		this.cdPlayer = cdPlayer;
	}

	@Override
	public void execute() {
		cdPlayer.off();
	}
}

 

- 위와같이 command내부 execute command 안에는 여러개의 Receiver에 로직이 들어갈 수 있다. (원하는 행동을 구동시키기 위하여)

- 다른 Command들은 충분히 쉬우니 생략하도록 하겠다.

 

만능리모컨 사용해보기

 

 

- 위와 같은 클래스다이어그램의 만능리모컨이 완성 되었다. 한번 사용해보도록하자

 

public class RemoteLoader {

	public static void main(String[] args) {
		RemoteControl remoteControl = new RemoteControl();
		
		LightReceiver lightReceiver = new LightReceiver();
		GarageDoorReceiver garageDoorReceiver = new GarageDoorReceiver();
		CdPlayerReceiver cdPlayerReceiver = new CdPlayerReceiver();
		
		LightonCommand lightonCommand = new LightonCommand(lightReceiver);
		LightOffCommand ligOffCommand = new LightOffCommand(lightReceiver);
		
		GrageDoorDownCommand garDoorDownCommand = new GrageDoorDownCommand(garageDoorReceiver);
		GarageDoorUpCommand garageDoorUpCommand = new GarageDoorUpCommand(garageDoorReceiver);
		
		CdPlayerOffCommand cdPlayerOffCommand = new CdPlayerOffCommand(cdPlayerReceiver);
		CdPlayerOnCommand cdPlayerOnCommand = new CdPlayerOnCommand(cdPlayerReceiver);
		
		remoteControl.setCommand(0, cdPlayerOnCommand, cdPlayerOffCommand);
		remoteControl.setCommand(1, garDoorDownCommand, garageDoorUpCommand);
		remoteControl.setCommand(2, lightonCommand, ligOffCommand);
		
		remoteControl.onButtonPushed(0);
		remoteControl.offButtonPushed(0);
		
		remoteControl.onButtonPushed(1);
		remoteControl.offButtonPushed(1);
		
		remoteControl.onButtonPushed(2);
		remoteControl.offButtonPushed(2);
		
		remoteControl.onButtonPushed(3);
		remoteControl.offButtonPushed(3);
		
	}

}

 

 

 

+ Undo 기능 만들기.

Undo 기능은 만들기 참 쉽다. Command 내의 On에 반대되는 기능만 추가해주면 된다.

 

1) 다음과같이 Command interface에 Undo method만 추가해준다.

 

2) Cdplayer command 에서 예시로 구현해 보겠다. OnCommand에 반대되게Undo를 작성해준다.

public class CdPlayerOnCommand  implements Command{
	CdPlayerReceiver cdPlayer;
	
	public CdPlayerOnCommand(CdPlayerReceiver cdPlayer) {
		this.cdPlayer = cdPlayer;
	}

	//...omit...
    
    
	@Override
	public void undo() {
		cdPlayer.off();
	}

}

 

3) Invoker 객체에서 Undo를 포함하는 객체를 다음과같이 저장할수 있게한뒤(6개나 필요없다.) 쓸수있는 Method를 정의해준다.

public class RemoteControl {
	Command[] onCommands;
	Command[] offCommands;
	Command undoCommand;

	public RemoteControl() {
		onCommands = new Command[7];
		offCommands = new Command[7];
		
		Command noCommand = new NoCommand();
		for (int i = 0; i < 7; i++) {
			onCommands[i] = noCommand;
			offCommands[i] = noCommand;
		}
		undoCommand = noCommand;
	}
	
    public void onButtonPushed(int slot) {
		onCommands[slot].execute();
		undoCommand = onCommands[slot];
	}
	
	public void offButtonPushed(int slot) {
		offCommands[slot].execute();
		undoCommand = onCommands[slot];
	}

	// ... omit ...
    
    
	public void undoButtonPushed() {
		undoCommand.undo();
	}
}

 

- 차이가 보이는가? 커맨드가 실행될때마다 슬롯을 UndoCommand에 저장시켜서 언제나 Undo method를 실행할 수 있게준비한다.

 

4) 메인에서 사용방법이다.

public class RemoteLoader {

	public static void main(String[] args) {
		RemoteControl remoteControl = new RemoteControl();
		
		GarageDoorReceiver garageDoorReceiver = new GarageDoorReceiver();

		CdPlayerOffCommand cdPlayerOffCommand = new CdPlayerOffCommand(cdPlayerReceiver);
		CdPlayerOnCommand cdPlayerOnCommand = new CdPlayerOnCommand(cdPlayerReceiver);
		
		
		remoteControl.onButtonPushed(0);
		remoteControl.undoButtonPushed();
		remoteControl.offButtonPushed(0);
		
		// ... omit ....
		
	}

}

 

- 위와같이 사용할 수 있다.

'To be Developer > DesignPatterns' 카테고리의 다른 글

6-1.Command Pattern (Simple)  (0) 2020.01.10
5.Singleton Pattern  (0) 2020.01.07
4-2 Factory Pattern (Abstract Factory Pattern)  (0) 2020.01.05
4-1.Factory Pattern (Factory Method Pattern)  (0) 2020.01.04
3.Decorator Pattern  (0) 2020.01.02

문제 제기

만능 리모컨 회사에서 여러 물품에 블루투스 리모컨 시스템을 사용할 수 있는 환경을 만들어달라고 의뢰를 해왔다.
의뢰를 들어온 물품 Object 들의 시그니쳐는 다음과 같이 제공되었다.

물론 더 많겠지만 일단 4개만 해보자.

 

- 공통된 Method가 존재하기도 하는데 대부분 겹치지않는다. 공통된 행위가 없어서 Starategy 패턴으로 뺄수도 없을것 같고 이럴땐 어떻게 해야할까?

 

커맨드 패턴 살펴보기

- 가장 먼저 커맨드 패턴의 클래스 다이어그램부터 살펴보자

 

- 클래스 다이어그램에는 가장 핵심적인 요소 4가지가 있다. Client, Invoker, Command, Receiver 이다.

- 대소 관계로 비교하기 좀그렇지만 Client는 {Invoker, Command, Receiver}를 다 가지고 있다.
그리고 클라이언트의 요청에따라 Invoker > Command > Receiver 순서로 트리거 된다. 그러나 그전에 클라이언트에서 해주어야 할 작업이 있다. 이 작업을 순서대로 풀어 써 보자면

1) Client는 가장 먼저 Receiver를 생성 후 Command에 넘겨 Command를 생성해준다.
2) 다음으로 클라이언트는 CommandInvoker에게 Set 해주고 Invoker를 생성해준다
3) client는 필요할때 Invoker에게 invoke()를 요청한다.
4) 이렇게 되면 Invoker.Invoke() -> Command.execute() -> receiver.action() 순으로 실행된다.

 


이를 그림으로 표현 한 것이다.

 

간단한 리모컨에 커맨드 패턴 끼얹기

- 자 위와 같이 간단한 On/Off 버튼이 있다고 먼저 가정해보자.

 

- 웜업으로 아까 받은 여러개의 클래스 다이어그램에서 Light의 On()과 Garage의 Up()만 구현해보자.

1) 각각 Receiver를 구현해보자.

public class GarageDoorReceiver {
	
	public void up() {
		System.out.println("garage up");
	}
	
	public void down() {
		
	}
	
	public void stop() {
		// TODO Auto-generated method stub

	}	
	
	public void lightOn() {
		
	}
	
	public void lightOff() {
		
	}
}
public class GarageDoorReceiver {
	
	public void up() {
		System.out.println("garage up");
	}
	
	public void down() {
		
	}
	
	public void stop() {
		// TODO Auto-generated method stub

	}	
	
	public void lightOn() {
		
	}
	
	public void lightOff() {
		
	}
}

 

 

2) Command Interface를 만들고, Command 구현체를 각각 만들어주자

public interface Command {
	public void execudte();
}
public class LightonCommand implements Command{
	LightReceiver light;
	
	public LightonCommand(LightReceiver light) {
		this.light = light;
	}
	
	@Override
	public void execudte() {
		light.on();
	}

}
public class GarageCommand implements Command{
	GarageDoorReceiver gar;
	
	public GarageCommand(GarageDoorReceiver gar) {
		this.gar = gar;
	}
	
	@Override
	public void execudte() {
		gar.up();
	}

}

 

 

3) 위 두가지 Command를 실행해줄 SimpleRemotecontrol Invoker 를 만들어보자

public class SimpleRemotecontrol {
	Command com;
	
	public SimpleRemotecontrol() {
		// TODO Auto-generated constructor stub
	}
	
	public void setCom(Command com) {
		this.com = com;
	}
	
	public void btnPressed() {
		com.execudte();
	}
	
}

 

4) 한번 Client (메인함수) 에서 실행시키고 결과를 보자.

public class RemoteControlTest {

	public static void main(String[] args) {
		SimpleRemotecontrol remote = new SimpleRemotecontrol();
		LightReceiver light = new LightReceiver();
		LightonCommand lightOnCommand = new LightonCommand(light);
		
		remote.setCom(lightOnCommand);
		remote.btnPressed();
		
		GarageDoorReceiver ga = new GarageDoorReceiver();
		GarageCommand garageCommand = new GarageCommand(ga);
		
		remote.setCom(garageCommand);
		remote.btnPressed();
	}

}

 

하나의 인보커로 두가지 커맨드가 잘 실행 되었다.!

 

- 간단하게 커맨드 패턴을 활용하여 두가지 Light, Garage Receiver를 구현해보았다.

 

- 위 소스의 클래스 다이어그램은 아래와 같이 그릴 수 있다.

 

커맨드 패턴의 정의

- 커맨트 패턴의 공식적인 정의는 다음과 같다.

커맨드 패턴
커맨드 패턴을 이용하면 요구사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러가지 다른 요구사항을 집어넣을수도 있다. 또한 요청내역을 큐에저장하거나, 로그로 기록할 수도 있으며, 작업취소 기능도 지원합니다.

 

-간단한 커맨드 패턴에 작업취소등 추가적인 작업은 다음시간에 해보겠다.

- 결국 커맨드 패턴이란, 일련의 행동을 특정 리시버로 연결시켜 요구사항을 커맨드로 캡슐화 한것이다. 외부에서 볼때는 어떤 리시버가 실행되는지, 어떤 객체가 리시버역할을하는지, 그 리시버에서 실제로 어떤 수행을 하는지 알 수 없다. 그냥 execute()가 실행되면 리시버가 실행된다는 것만 알 수 있을뿐이다. 또한 우리는 Receiver를 Command객체로 구현하므로써, 매개변수로써 여러가지 다른 리시버들을 실행 시켰다.

고전적인 싱글턴 패턴

- 객체가 하나만 필요한 경우는 굉장히 많다, 스레드풀, 레지스트리 설정 객체 아니면 그래픽카드객체 등등... 이런경우에는 객체가 한가지만 필요하다.

- 분명히 static 전역 객체를 쓰면, 구지 싱글턴을 안써도 될텐데....? 라는 말을 할 수 있을것이다. 그렇지만 메모리 효율이라는 명목아래 우리는 싱글턴 패턴을 쓴다.

public class Singleton {
	private static Singleton unique;
    
    //접근 제한자를 써서 외부에서 객체를 생성못하게함.
    private Singleton() {}
    
    public static Singleton getInstance () {
    	if (unique == null) {
        	uniqueInstance = new Singleton();
        }
        return unique;
    }

}

 

- 위와같은 코드는 누구나 한번쯤 봤을것이다. 그러나, 이런 식으로 싱글턴을 쓰지않도록 주의해야 한다.

 

문제 제기

- 우리는 초콜릿바 공장을 운영하고 있고, 초콜릿을 녹이는 가마솥이 있다고 가정하자, 이 솥은 컴퓨터로 제어된다.

- 이 가마솥에서는 초콜릿과 우유를 받아서 끓이고 다음공정으로 넘기는 역할을 한다. 빈 가마솥을 끓이다거나 가득찬 가마솥에 끓인다거나 하는 실수는 절대 하지말아야한다.

public class ChocolateGamasot {
	private boolean empty;
	private boolean heat;
	
	public ChocolateGamasot() {
		empty = true;
		heat = false;
	}
	
	//가마솟이 비어있을때만 집어넣음.
	public void fill() {
		if(isEmpty()) {
			empty = false;
			heat = false;
			// 원유를 집어넣음
		}
	}
	
	public void drain() {
		if(!isEmpty() && isHeat()) {
			//끓인 재료를 다음 단계로 넘김
			empty = true;
		}
	}
	
	public void boil() {
		if(isEmpty() && isHeat()) {
			// 끓임
			heat = true;
		}
	}
	
	private boolean isHeat() {
		return heat;
	}

	public boolean isEmpty() {
		return empty;
	}
}

 

- 이런 가마솥 객체를 만들었다고 가정하자, 가마솥 객체가 두개가 있으면 어떻게될까?

- 대개가 있으면 안되므로 이를 싱글턴으로 바꿔보자.

public class ChocolateGamasot {
	
	private static ChocolateGamasot unique;
	private boolean empty;
	private boolean heat;
	
	private ChocolateGamasot() {
		empty = true;
		heat = false;
	}
	
	public static ChocolateGamasot getInstance() {
		if(unique == null) {
			unique = new ChocolateGamasot();
		}
		return unique;
	}
	
	//가마솟이 비어있을때만 집어넣음.
	public void fill() {
		if(isEmpty()) {
			empty = false;
			heat = false;
			// 원유를 집어넣음
		}
	}
	
	public void drain() {
		if(!isEmpty() && isHeat()) {
			//끓인 재료를 다음 단계로 넘김
			empty = true;
		}
	}
	
	public void boil() {
		if(isEmpty() && isHeat()) {
			// 끓임
			heat = true;
		}
	}
	
	private boolean isHeat() {
		return heat;
	}

	public boolean isEmpty() {
		return empty;
	}
}

 

- 위와같이 싱글턴으로 바꾼뒤 이 소스를 실행시켰더니 문제가생겼다.

//Main 함수
ChocolateGamasot gamasot = ChocolateGamasot.getInstance();

gamasot.fill();
gamasot.boil();
gamasot.drain();

- JVM 내에 두개의 스래드가 위의 코드로 접근했는데 2개의 객체가 생긴것이다!!! (이럴수가!!!)
1번스레드가 if === null 을 확인하고, 그 다음구문을 실행하던중, 다음 스레드가 똑같이 이프문 안으로 분기된것이다.

 

	public static synchronized ChocolateGamasot getInstance() {
		if(unique == null) {
			unique = new ChocolateGamasot();
		}
		return unique;
	}

- 위와같이 동기화 하여 문제를 해결하긴 했는데, 왠지 오버헤드가 너무아까운 생각이든다.. 일단 unique 변수에 ChocolateGamasot를 한번 초기화하기만하면 구지 getInstance() 전체 함수에 동기화를 거는 오버헤드는 필요 없을듯하다.

 

더 효율적으로 getInstance의 동기성을 제어하자

1. getInstance() 의 속도가 중요하지않다면 그냥 둔다.
메소드를 동기화하면 성능이 100배정도 저하된다고한다. 그럼에도 불구하고 getInstance의 속도가 그닥 중요하지않다면 놔두도록하자.

2. 인스턴스를 필요할때 생성하지않고, 바로 생성하여둔다.

	private static ChocolateGamasot unique = new ChocolateGamasot();
	private boolean empty;
	private boolean heat;
	
	public static ChocolateGamasot getInstance() {
		return unique;
	}

- 위 같은방법도 그닥 나빠보이지 않는다 (근데 static 변수만 쓸때와 차이점은 뭐지??)

3."DCL(Dobule Checking Locking)"을 써서 getInstance()에서 동기화 되는 부분을 줄여보자.

private static volatile ChocolateGamasot unique;
private boolean empty;
private boolean heat;

public static ChocolateGamasot getInstance() {
	
	//인스턴스가 있는지 확인하고 synchronized 블럭으로 접근
	if(unique == null) {
		// 이렇게하면 처음에만 동기화 진행.
		synchronized (ChocolateGamasot.class) {
			if(unique == null) {
				unique = new ChocolateGamasot();
			}
		}
	}
	return unique;
}

 

- 잠깐! volatile 이란 무엇일까?
“… the volatile modifier guarantees that any thread that reads a field will see the most recently written value.” - Josh Bloch

이를 해석해보자면, volatile 이란 어떤 쓰레드가 read 하더라도 항상 최근값만 불러온다는 뜻이다.

Volatile을 사용하게 된다면, RAM에서 직접 변수를 읽고 쓰도록 Thread에게 강제한다. 이렇게 되면 스레드는 캐시에 존재하는 복사본이 아닌, RAM에 있는 마지막버전을 확인한다. 스레드가 동기화된 블록으로 들어가게 되면 ( 코드상 sychronizied 부분) 다른 모든 스레드는 첫번째 스레드가 동기화된 블록에서 나갈때 까지 기다린다. (캐시에 접근하지않고 직접 RAM에 직접 읽고 기록)

출처:https://stackoverflow.com/questions/106591/what-is-the-volatile-keyword-useful-for

 

What is the volatile keyword useful for

At work today, I came across the volatile keyword in Java. Not being very familiar with it, I found this explanation: Java theory and practice: Managing volatility Given the detail in which that

stackoverflow.com

c언어의 예시로 쉽게 volatile 설명 :https://dojang.io/mod/page/view.php?id=749

 

- 참고로 DCL은 자바 1.4 버전 이후부터만 쓸 수 있다. (1.4 이전 JVM 중에는 volatille 을 사용하더라도 동기화가 안되는 것이 많았다)

문제 제기

- 지난시간(링크). 우리는 피자가게 스토리와 엮어서 Factory Method Pattern에 대해 알아보았다.

- KorPizzaStore, IndiaPizzaStore를 만들어 Pizza 객체를 생성하는 createPizza에 대한 메소드는 Localize 된 피자스토어에 맡겼다. (India에서는 피자를 손으로 찢어먹기때문에 cutting 하지 않는다는 상황가정때문)

- IndiaPizzaStore에서는 cheese를 파니르(채식) 치즈와 채식 도우와 닭고기 불고기 토핑을 쓴다고 한다.

- 이런 차이를 극복하기위해 Abstaract Factorty Pattern을 써서 KorPizzaStore와 다르게 재료생산해보자.

 

- 위와같이 피자마다 같은구성요소를 가지긴 하지만, 지역마다 다른 방식으로 구성요소를 구현한다. 또한 위의 3재료뿐만아니라 피자마다 추가로 새로운 토핑 및  향신료도 추가될 수 있을것이다.

 

Abstract Factory Pattern을 스토리에 적용시켜보자.

public interface PizzaIngredientFactory {
	public Dough createDough();
	public Cheese createCheese();
	public Boolgogi createBoolgogi();
	//이건 지역별로 함께 쓸 수 있다.
	public Veggies[] createVeggies();
}

- 먼저 추상화된 재료 Pizza 재료 공장을 만들고, 지역별로 공장을 만들어보자.

- 도우, 불고기, 치즈는 다르지만, 베지는 공통적으로 함께 쓸 수있다.

 

public class IndiaPizzaIngredientFactory implements PizzaIngredientFactory{

	@Override
	public Dough createDough() {
		return new VeggieDough();
	}

	@Override
	public Cheese createCheese() {
		return new VeggieCheese();
	}

	@Override
	public Boolgogi createBoolgogi() {
		return new ChickenBoolgogi();
	}

	@Override
	public Veggies[] createVeggies() {
		Veggies veggies[] = {new Garlic(), new Onion(), new Mushroom()};
		return veggies;
	}
}

 

public class KorIngredientPizzaFactory implements PizzaIngredientFactory{

	@Override
	public Dough createDough() {
		return new OriginalDough();
	}

	@Override
	public Cheese createCheese() {
		return new OriginalCheese();
	}

	@Override
	public Boolgogi createBoolgogi() {
		// TODO Auto-generated method stub
		return new BeefBoolgogi();
	}

	@Override
	public Veggies[] createVeggies() {
		Veggies veggies[] = {new Garlic(), new Onion(), new Mushroom()};
		return veggies;
	}

}

 

- 위와같은식으로 나라에따라 피자 ingredient Factory를 만들었다.

- 이제 재료를 생산할 준비가 끝났다. 피자에 따라 재료가 달라지니 한번 Pizza Class를 바꿔보도록 하자.

 

public abstract class Pizza {
	String name;
	Dough dough;
	Cheese cheese;
	Boolgogi boolgogi;
	Veggies veggies[];
	
	//원재료 생산공장을 달리하면서 추상화시켯다.
	public abstract void preare();

	public void bake() {
		System.out.println("굽는중.....");
	}
	
	public void cut() {
		System.out.println("자르는중.....");
	}
	
	public void box() {
		System.out.println("박싱중.....");
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

- prepare와 원재료들을 변수 이외에는 달라진 로직은 없다. 그러면 Boolgogipizza 클래스를 한번 만들어보자.

public class BoolGogiPizza extends Pizza {
	PizzaIngredientFactory pizzaIngredientFactory;
	
	public BoolGogiPizza(PizzaIngredientFactory pizzaIngredientFactory) {
		setName( " Korea Style Cutting Pizza");
		this.pizzaIngredientFactory = pizzaIngredientFactory;
	}

	@Override
	public void prepare() {
		System.out.println("재료준비중... " +name);
		dough = pizzaIngredientFactory.createDough();
		cheese = pizzaIngredientFactory.createCheese();
		boolgogi = pizzaIngredientFactory.createBoolgogi();
		
		System.out.println(dough.getClass().getSimpleName());
		System.out.println(cheese.getClass().getSimpleName());
		System.out.println(boolgogi.getClass().getSimpleName());
	}
}

- 전 포스팅에서는 KorKorBoolGoggiPizza, IndiaBoolGoggiPizza 등, 구체적으로 로컬별 피자객체를 만들었으나, 이제는 ingredient Factory만 달리 생성하면 되므로 이런식으로 BoolGogiPizza 객체를 만들었다.

-다만 이제 KorPizzaStore에서 피자객체를 생성할때 코드가 바뀌어야한다..

//바뀌기전
public class KorPizzaStore extends PizzaStore {

	@Override
	protected Pizza createPizza(String type) {
		Pizza pizza= null;
		
		if(type.equals("boolgogi")) {
	        pizza = new KorBoolGoggiPizza();
	    } 
		
		return pizza;
	}

}

 

//바뀐후
public class KorPizzaStore extends PizzaStore {

	@Override
	protected Pizza createPizza(String type) {
		Pizza pizza= null;
		PizzaIngredientFactory fac = new KorIngredientPizzaFactory();
		
		if(type.equals("boolgogi")) {
			pizza = new BoolGogiPizza(fac);
			pizza.setName("Korea Style Pizza From KOR ingredient Factory");
	    } 
		
		return pizza;
	}

}

 

- 바뀌기전에는 피자 자체를 store에서 생성해주었지만, 이제는 원재료를 로컬별 IngredientPizzaFactory에서 받아쓰기때문에, IngredientPizzaFactory 객체를 BoolGogiPizza 생성자에 넘겨주어 피자 재료를 달리생성하게된다.

- 전 포스팅한 메인함수를 그대로 실행시켜보니 내가 의도한대로 잘 나온다.

 

Abstract Factory Pattern 내용 정리

- 지역별 PizzaStore에 피자라는 Product가 종속되어 있지않고, Abstract된 Ingredient Pizza Factory에 따라 제품을 받아쓰기 때문에, 프로그램이 실행중에도, 같은 불고기피자라도 다른 재료를 적용 시킬 수 있다.

 

 

 

 

Abstarct Factory Pattern 정의

추상 팩토리 패턴
추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 서로 의존하는 객체를 구상 클래스를 지정하지 않고도 생성 할 수있다.

- 추상 팩토리 패턴을 이용하면, 인터페이스를 통해 Product 들을 생성 할 수있다. 
(pizza = new BoolgogiPizza(korPizzaIngredientFactory); 처럼 말이다.)

- 따라서 클라이언트와 Product를 분리시킬수가 있다.

 

- 위와같은 클래스 다이어그램으로 Abstract Factorty Pattern을 표현할 수 있다.

 

- 실제 우리가 만든 피자스토어의 클래스다이어그램은 이렇게 만들 수 있다.

 

Abstract Factory Pattern Vs Factory Method Pattern

 

- Abstract Factory Pattern은 Factory Method Pattern에 비해 다소 복잡하다.

- 새로운 Product를 추가하려면 Factory Method Pattern는 Interface를 바꿔야 한다. (India boolgogi Pizza에서 Original Dough를 쓰고싶다면?? 코드자체가 바뀌어야함)

- Abstract Factory Pattern에서는 소스코드 자체를 추가하지않고, 구현체만 갈아끼우면 해결 할 수있다.

'To be Developer > DesignPatterns' 카테고리의 다른 글

6-1.Command Pattern (Simple)  (0) 2020.01.10
5.Singleton Pattern  (0) 2020.01.07
4-1.Factory Pattern (Factory Method Pattern)  (0) 2020.01.04
3.Decorator Pattern  (0) 2020.01.02
2.Observer Pattern  (0) 2020.01.01

문제 제기

 - "new"는 "구상객체" 를 뜻한다. 즉 new를 사용해서 인스턴스를 만든다는 것은, 특정 구현된 클래스의 인스턴스를 만든다는 것이다. new를 바탕으로 코딩을 하면 나중에 코드를 수정해야할 가능성이 높아진다. 예를들어보자

Beverage beverage = new DarkRoast()

 

- 전에 데코레이트 패턴에서 카페시나리오에서 만들었던, 음료에 관한 코드이다. 만약 카페인이 싫은 사람은 무조건 디카페인을 먹는다면?, DarkRoast의 진한맛을 싫어하는 사람이 있다면?

Beverage beverage;

  if(isNoCaf) {
  	beverage = new Decaf();
  } else if(isStrong) {
  	beverage = new DarkRoast();
  } else {
  	beverage = new HouseBlend();
  }

- 이런식으로 조건에따라. 몇 가지의 구상 클래스의 인스턴스가 만들어지는 코드를 써야할것이다.
  위와 같은 코드는 변경, 또는 확장이 필요할때 코드를 다시 지우거나 추가해야한다는 뜻이다. 휴먼오류를 일으킬 가능성과 OCP의 원칙을 지키기 어렵다.

- 사실 "new" 자체에는 문제가 없다. 그러나 변화를 원하는 고객과 요구사항이 항상 문제인것이다..... 그러면 우리는 어떻게 이러한 "new" 키워드의 OCP 문제를 해결할 수 있을까?

 

- 자, 우리가 피자가게를 차리는 시나리오를 생각해보자.

public Pizza orderPizza(String type) {
	Pizza pizza;
    
    if(type.equals("cheese")) {
    	pizza = new CheesePizz();
    } else if(type.equlas("pepperoni") {
    	pizza = new PepperoniPizz();
    } else if(type.equlas("boolgogi") {
    	pizza = new BoolgogiPizz();
    }

	pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    
    return pizza;
}

 

- 일단은 우리가 아까 고민했던, 위와같은 코드를 사용하여 보자.
치즈피자에 대한 단가가 안맞아, 치즈피자는 제외하고 야채피자(Veggie)를 새로 만든다고 해보자.

public Pizza orderPizza(String type) {
	Pizza pizza;
    
    //분기문에 피자 타입 코드변경에 관한 부분은 닫혀있지않다.
    //즉 메뉴를 변경하려면 직접 코드를 수정해야한다.
    if(type.equals("cheese")) {
    	pizza = new CheesePizz();
//    } else if(type.equals("pepperoni") {
//    	pizza = new PepperoniPizza();
    } else if(type.equals("boolgogi") {
    	pizza = new BoolgogiPizza();
    } else if(type.equals("veggi") {
    	pizza = new VeggiPizza();
    }
    
    
    //이부분은 바뀌지 않는다.
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    
    return pizza;
}

- 위와같이 변경되지 않는부분과, 변경되는 부분이 명확하게 나뉘었다. 지금 orderPizza 메소드에서 가장 문제가 되는부분은, 인스턴스를 만들 피자종류 클래스를 선택하는 부분인것이다(OCP에 위배).

- 객체지향 디자인원칙에 따라, 바뀌는 부분에 대하여 캡슐화를 진행해보자,

 

팩토리 패턴에 단계적 적용

1)Simple Pizza Facotory

- 진짜 팩토리 패턴에 들어가기전에 워밍업으로 Simple Factory 을 만들어봅시다

 

public class SimplePizzaFactory {
  public Pizza createPizaa(String type) {
    Pizza pizza= null;
    
    if(type.equals("cheese")) {
        pizza = new CheesePizz();
    } else if(type.equals("boolgogi")) {
        pizza = new BoolgogiPizza();
    } else if(type.equals("veggi")) {
        pizza = new VeggiPizza();
    }

    return pizza;
  }
}

 

public class PizzaStore {
  SimplePizzaFactory factory;

  public Pizza orderPizza(String type) {
    Pizza pizza;
	
    pizza = factory.createPizza(type);
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();

    return pizza;
  }
}

- 위와같은 형태의 SimpleFactory는 널리 쓰이기는하나, 진짜 팩토리패턴이라고 말할수 없다. 왜냐하면, 실행시에 동적으로 SimplePizzaFactory에 관한 객체구성을 바꿀 수 없기때문이다. 이는 위 코드를 "팩토리 패턴" 이라고 부르는 사람이 있으면, 귓속말로 그건 팩토리 패턴이 아니라고 알려주자.

 

2)Factory Method Pattern

- 위의 심플피자 팩토리가 문제가 되는 경우에 대해서 이야기해보자. 우리의 피자가게가 번성하여, 우리의 피자가게가 해외로 진출을 하게되었다. 근데 인도에서는 피자를 Cutting하지 않고 손으로 찢어먹는다고한다고 가정해보자. 이 경우에 PizzaStore와 SimpleFactory 객체와의 강력한 결합떄문에 이를 해결 할 수 없다. (근데 만약 팩토리 자체를 인터페이스로 다중화시킨다면??)

-결론적으로 우리의 사업의 확장으로 Pizza Instance를 만들때 한 객체에서 만들기 보다는, 팩토리 메소드를 추상화하여 일련의 서브클래스에서 처리하는 방식으로 바꿔보자

public abstract class PizzaStore {

  public Pizza orderPizza(String type) {
    Pizza pizza;
	
    //바뀐부분
    pizza = createPizza(type);
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();

    return pizza;
  }
  
  //
  protected abstract Pizza createPizza(String type);
}

 

- 위의 추상 PizzaStore 클래스로 여러가지 로컬 피자스토어 클래스를 만들 수 있게 되었다.

-이제 공장에서 Creator는 완성 되었으니, Product(Pizza)에 대해서 이야기 해보자,

public abstract class Pizza {
	String name;
	ArrayList<String> toppings;
	
	public void prepare() {
		System.out.println("준비중 " + name);
		for (int i = 0; i < toppings.size(); i++) {
			System.out.println("토핑추가 : "+ toppings.get(i));
		}
	}

	public void bake() {
		System.out.println("굽는중.....");
	}
	
	public void cut() {
		System.out.println("자르는중.....");
	}
	
	public void box() {
		System.out.println("박싱중.....");
	}
}

 

- nation 별로 다른 Product를 적용해보자.

public class KorBoolGoggiPizza extends Pizza {
	public KorBoolGoggiPizza() {
		this.name = "Korea Style Cutting Pizza";
	}
}

 

public class IndiaBoolGoggiPizza extends Pizza {
	
	public IndiaBoolGoggiPizza() {
		this.name = "India Style No cutting Pizza";
	}
	
	@Override
	public void cut() {
		System.out.println("인도에서는 피자를 컷팅하지 않아요");
	}
}

 

- 위와같은 abstract Pizza class로 인해, Localize한 피자를 생산할 수 있게 되었다.

 

- 실제로 다른 nation 별 피자를 생성해보자.

public class PizzaStoreMain {

	public static void main(String[] args) {
		PizzaStore korStore = new KorPizzaStore();
		PizzaStore indiaStore = new IndiaPizzaStore();
		
		Pizza pizza = korStore.orderPizza("boolgogi");
		System.out.println("완성" + pizza.getName() + "\n");
		
		pizza = indiaStore.orderPizza("boolgogi");
		System.out.println("완성" + pizza.getName() + "\n");
		
	}
}

 

결과

 

 

 

- 전체적인 다이어그램은 이런식으로 그려진다고 보면된다.

 

 

팩토리 메소드 패턴
 객체를 생성하기 위한 interface(또는 abstract method)를 정의하게 되는데, 어떤클래스의 인스턴스를 만들지는 서브클래스에 의해서 결정되게 된다.

- 위 정의에서, "결정한다" 라고 표현한 이유는, 이 패턴을 사용할 때 서브클래스에서 실행중에 어떤 클래스의 인스턴스를 만들지 결정하기 때문이 아니라, 생산자 클래스 자체가 실제 생산될 제품에대한 사전지식이 전혀 없이 만들어지기 때문이다.

- 저금더 풀어서 표현하자면, "사용하는 서브클래스에 따라 생산되는 개체 인스턴스가 결정된다" 이다.

 

 

팩토리 패턴을 몰랐을때로 돌아가볼까?

 

public class DependentPizzaStore {
	public Pizza createPizza(String nation, String type) {
		
		Pizza pizza = null;
		if (nation.equals("kor")) {
			if(type.equals("bollgogi")) {
				pizza = new KorBoolGoggiPizza();
			} else if (type.equals("cheese")) {
				pizza = new KorCheesePizza();
			} else if (type.equals("veggie")) {
				pizza = new KorVeggiePizza();
			}
		} else if (nation.equals("india")) {
			if(type.equals("bollgogi")) {
				pizza = new IndiaBoolGoggiPizza();
			} else if (type.equals("cheese")) {
				pizza = new IndiaCheesePizza();
			} else if (type.equals("veggie")) {
				pizza = new IndiaVeggiePizza();
			}
		}
		
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();

		return pizza
	}
}

 

- 나중에 local이 많아지면 많아질수록 더욱 힘들어질것이다. 왜냐하면, PizzaStore 코드또한 변경되기 때문이다. 의존성에 대하여 다이어그램으로 표현해보자.

- 이제는 구상 클래스에 의존성을 줄이는것이 좋다는 것을 확실히 알 수 있다. 이런내용의 디자인 원칙을 표현하는 말이 있다..
"의존성 뒤집기 원칙(Dependency Inversion Principle)" 이라고 한다.

디자인 원칙
추상화된 것에 의존하도록 만들어라, 구상클래스에 의존하도록 만들지 않도록 한다.

- 이 원칙에서는 고수준 구성요소저수준 고성요소에 의존하면 안된다는 의미가 내포되어 있다. 이게 무슨말일까?
- PizzaStore는 고수준 구성요소, Pizza class들은 저수준 구성요소 라고 할 수 있다.
- 고수준 구성요소는 다른 저수준 구성요소에 의해 정의되는 행동이 들어있는 구성요소를 뜻한다. 예를들어 PizzaStore의 메소드는 Pizza에 의해 정의되어진다. PizzaStore는 피자를 만들고 또한 피자를 준비,굽기,자르고,포장한다.

 

 

- 디자인 원칙을 적용해 우리는 이렇게 멋진 디자인 다이어그램을 만들어냈다.!!

 

- 피자를 Localize하다보니 또하나 문제가 생겼다. 같은 boolgogi 피자인데, 인도에서는 소고기를 먹지않아 치킨불고기 재료를 써야하고, 또한 피자치즈는 염소치즈를 쓴다고한다. 

- 위 문제를 추상 팩토리 패턴을 써서 해결하는법을 한번 알아보자.

'To be Developer > DesignPatterns' 카테고리의 다른 글

5.Singleton Pattern  (0) 2020.01.07
4-2 Factory Pattern (Abstract Factory Pattern)  (0) 2020.01.05
3.Decorator Pattern  (0) 2020.01.02
2.Observer Pattern  (0) 2020.01.01
1.Strategy Pattern  (0) 2019.12.31

문제 제기

- 우리가 스타** 커피집을 차렸다고 생각해보자.

 

-장사 초기에는 POS에서 구현된  음료객체는 많지않았고, 위와같이 아주 심플했다.

 

-그러나 스팀 우유나, 두유, 초콜릿을 추가하고 그위에 휘핑크림을 얹기도 하면서 굉장히 많은 메뉴들이 생겨나기 시작했다.

-주문시스템에서도 이러한 메뉴들을 반영하기 시작했고 초기에는 무조건 상속받아 구현하였다.

- 앞으로 녹차가 추가될수도있고 많은 토핑들이 추가될수도있다. 그러면 이렇게 클래스가 늘어나는 것을 지켜보고만 있어야할까?

- 이 스토리에서는 앞서 배운 두가지 디자인패턴 원칙을 따르지 않고있다.

 

- 그러면 폭팔적으로 늘어나는 음료 class를 막기위해 다음과같이 super 클래스를 정의해보면 어떨까?

-각 cost() 에서는 일단 음료의 가격을 구한다음, 첨가된 항목에따라 추가 가격을 더하면 된다.

-그러나 여기서 야기될 수 있는 문제점이 몇가지 보인다.
 1) 첨가물 가격이 바뀔때마다 기존코드 수정.
 2) 첨가물의 종류가 추가되면 setter메소드 및 has 메소드 추가
 3) 새로운 음료가 출시됐는데 거기에 기존 첨가물이 안들어가도 오버라이드 되어야함. (녹차에 whip 등)
 4) 손님이 더블 휩을 주문한다면? boolean에서 int로 설계해야 할까?

- 서브클래스를 만드는 방식으로 행동을 상속받게되면, 그 행동은 컴파일시에 완전히 결정되어진다.
-> 반면 구성을 통해서 객체의 행동을 확장하면 실행중에 동적으로 행동을 설정할 수 있다.
 또한, 기존의 코드는 건드리지않기 때문에, 기존코드에서 버그가 생기거나 의도하지 않은 부작용이 발생하는 것을 원천 봉쇄할 수 있다. 

디자인 원칙
클래스는 확장에 대해서는 열려 있어야 하지만, 코드변경에는 닫혀있어야 한다.

- 이 원칙을 준수하기 위해서는, 바뀔 확률이 높은 부분을 중점적으로 살펴보고, 원칙을 적용하는 방법이 가장 현명하다.
(무조건 OCP(Open Closed Principale)를 적용하는 것은 시간낭비가 될 수도있고, 쓸데없이 일을 크게 벌일 수도 있으니 유의하자)

 

데코레이터 패턴의 개념 추상화

-상속을 써서 음료의 가격과 첨가물(휘핑,우유,모카)의 총 가격을 계산하는 것은 그닥 좋지 않았다. 음료를 첨가물로 Decroeate 해보면 어떨까?
 1) DarkRoast 객체를 가져온다.
 2) Mocha 객체로 장식한다.
 3) Whip 객체로 장식한다.
 4) cost() 메소드를 호출한다. 이때 첨가물의 가겨을 계산하는 일은 해당 객체들에 위임된다.

 

-위 로직을 그림으로 표현해보자.

1) DarkRoast 객체를 가져온다.

 

 

 2) Mocha 객체로 장식한다.

 


 3) Whip 객체로 장식한다.

 

 


 4) cost() 메소드를 호출한다. 이때 첨가물의 가겨을 계산하는 일은 해당 객체들에 위임된다.

 

데코레이터 패턴의 정의

데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있다.

- 데코레이터 패턴의 추상화된 클래스 다이어그램부터 살펴보자.

- 조금은 복잡하지만, 원래 Super 클래스를 상속받아, 구체화 시킨 class를 Decorator의 내부 인스턴스로 가지고있다는 것 밖에 없다.

 

드디어 우리 카페에 Decorator 패턴을 적용시켜보자.

 

 

- 자 이제 코드로 구현해보자, 추상 클래스들(음료, 데코레이터)를 구현해보자

public abstract class Beverage {
	String description = "제목 없음";
	
	public String getDescription() {
		return description;
	}
	
	public abstract int cost();

}

 

//Beverage 객체가 들어갈 자리에 있어야하므로, 베버리지 상속
public abstract class ToppingDecorator extends Beverage {
	//모든 토핑은 description을 구현하도록 만들예정임
	public abstract String getDescription();
}

 

- 다음은 구현체들을 만들어보자.

public class DarkRoast extends Beverage{

	public DarkRoast() {
		this.description = "다크로스트";
	}
	
	@Override
	public int cost() {
		return 3000;
	}
}

 

- 아래부터는 토핑 데코레이터의 구현체이다.

public class Mocha extends ToppingDecorator{
	
	Beverage beverage;
	
	public Mocha(Beverage beverage) {
		this.beverage = beverage;
	}
	
	@Override
	public String getDescription() {
		return beverage.getDescription() + ", 추가 모카";
	}

	@Override
	public int cost() {
		return 500 + this.beverage.cost();
	}

}

 

public class Whip extends ToppingDecorator{

	Beverage beverage;
	
	public Whip(Beverage beverage) {
		this.beverage = beverage;
	}
	
	@Override
	public String getDescription() {
		return beverage.getDescription()  + ", 추가 휘핑";
	}

	@Override
	public int cost() {
		return 500 + beverage.cost();
	}

}

 

 

- 이제 데코레이터들을 통해 음료를 wrapping 하는법을 알아보자.

public class CafeMain {

	public static void main(String[] args) {
		Beverage beverage = new DarkRoast();
		
		System.out.println(beverage.getDescription() + " 가격은: " + beverage.cost());
		
		//모카 추가
		beverage = new Mocha(beverage);
		System.out.println(beverage.getDescription() + " 가격은: " + beverage.cost());
		
		//휘핑 추가
		beverage = new Whip(beverage);
		System.out.println(beverage.getDescription()  + " 가격은: " + beverage.cost());
		
	}

}

 

-결과는 우리가 바라던 대로 잘 나온다.

'To be Developer > DesignPatterns' 카테고리의 다른 글

5.Singleton Pattern  (0) 2020.01.07
4-2 Factory Pattern (Abstract Factory Pattern)  (0) 2020.01.05
4-1.Factory Pattern (Factory Method Pattern)  (0) 2020.01.04
2.Observer Pattern  (0) 2020.01.01
1.Strategy Pattern  (0) 2019.12.31

문제제기

 

- 위처럼 기상 모니터링 애플리케이션을 만든다고 가정해보자.
- WeatherData 자체는 기상측정소에서 가져온다.
- 우리는 어플리케이션에서 총 3가지 어플리케이션을 구현해야한다
 1)번 디스플레이에는 기상통계를 보여준다.(평균, 최고 최저기온)
 2)번 디스플레이에는 현재 조건을 보여준다. (기온, 습도, 기압)
 3)번 디스플레이에는 기상 예보를 보여준다(기온)
- 우리는 위에 조건을 가진 디스플레이를 어떻게 확장가능하게 보여줄 수 있을까?

- 다음과 같이 프로그래밍을 한다고 가정해보자

public class WeatherData {
	public void measurementsChange() {
    	float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();
        
        display1.update(temp, humidity, pressure);
        display2.update(temp, humidity, pressure);
        display3.update(temp, humidity, pressure);
    }
	//....
}

 

- 위처럼 만들수는 있겠지만, 무엇인가 유연하지 않다. 그리고 전에 배웠던 디자인패턴의 원칙과는 동떨어져보인다

 

- 코드에서 전에 배웠던 디자인 패턴 원칙에 따라 구분지어보니, 바뀔수 있는 부분과, 바뀌지 않는 부분을 나눠보았고. 공통된 update라는 기능을 추출해보았다.

 

 

Observer 패턴에 대한 개요

- 위 기상모니터링 애플리케이션 코드를 리팩토링하기전에 Observer 패턴에 대해서 먼저 알아보자,

- 위 그림처럼, Subject 객체가 데이터를 전달하는 주체이고, Observer 객체들은 그 주제들을 전달받는 객체인것만 알면 Observer 패턴에대해서 모두 이해한 것이다. 그리고 Observer들은 언제든지 구독과 구독취소를 실행할 수 있다.

- 또한 옵저버 패턴은 Subject와 Observer사이에 일대다(one-to-many) 의존성을 갖는다.

- 위 그림에서 Subject 객체와 Observer 객체는 Loose Coupling 하다고 불수있다.
 1) Subject 객체가 옵저버에 대해 아는 것은 옵저버가 특정 인터페이스만 구현한 것만 알 수있다.
 2) 옵저버는 언제든지 추가, 삭제 될수있다.
 3) 새로운 형식의 옵저버를 추가하려고 할때, Subject를 전혀 변경할 필요가 없다.
 4) Subject와 Observer는 독립적으로 재사용 될 수있다.
 5) 주제와 옵저버의 코드가 변경되더라도 Code에 대한 Side effect가 없다.

디자인 원칙
서로 상호작용을 하는 객체 사이에서는 가능하면 느슨한 결합을 하는 디자인을 사용해야한다.

 

 

Observer 패턴을 기상 어플리케이션에 적용

-위와 같은 형태로 클래스 다이어그램을 만들 수 있을것이다.

 

public class WeatherData implements Subject{
	
	private ArrayList<Observer> observers;
	private float humidity;
	private float temperature;
	private float pressure;
	private Random random;
	
	
	public WeatherData() {
		observers = new ArrayList<Observer>();
	}

	@Override
	public void notifyObserver() {
		observers.stream().forEach(o -> o.update(temperature, humidity, pressure));
	}
    
    // ... omit
}

-WeatherData 객체에서 notify 하는 코드에 대해서만 보자면 위와같이 프로그래밍 할 수 있을것이다.

'To be Developer > DesignPatterns' 카테고리의 다른 글

5.Singleton Pattern  (0) 2020.01.07
4-2 Factory Pattern (Abstract Factory Pattern)  (0) 2020.01.05
4-1.Factory Pattern (Factory Method Pattern)  (0) 2020.01.04
3.Decorator Pattern  (0) 2020.01.02
1.Strategy Pattern  (0) 2019.12.31

문제제기

- 우리가 기존의 객체지향 프로그래밍을 활용하여 게임을 만든다고 가정해보자.
Person 이라는 Super Class를 사용하여 다음과같이 여러 직업군의 캐릭터를 정의할 수 있을 것이다.

 

- 만약 우리가 attack 이라는 method를 구현하고자 한다. 그러나 여기서 야기될 수 있는 문제점이 여러개가 보인다.
 1) 만약 Person을 상속받은 허수아비 가있다면 attack을 할 수 없어야한다. (그렇다면 그냥 override 해버리면 되잖아?)
 2) attack은 override를 해서 해결했지만, 만약에 추가적으로 sound 같은 행위를 구현해야한다면? interface가 계속늘어가고, 계속해서 override를 통해 재정의 해야할 것이다.

 

- 그렇다면 차라리 Interface를 implements 하면 어떻게 될까?
 이런식으로 만든다면, 물론 일부만 attack을 할 수 있고, 일부는 안하게 선택적으로 행위를 정의 할 수 있지만, Attack 행동에 대한 코드재사용성은 전혀 기대할 수 없다(클래스별로 공격 행위에 대한 코드를 작성해야 하며, 새로운 클래스가 생겼을때나, 클래스의 공격을 패치를 통해 변동시키고 싶다면??). 그리고 별개로 새로운 행동이 추가되면 될수록 재사용성은 전혀 기대할 수 없다.

 

문제를 명확하게 구체화

- attackAble 인터페이스 사용은 처음에는 괜찮아 보이지만, 자바의 인터페이스는 코드가 전혀 들어가있지 않기 때문에, 재사용성은 꽝이다.
- 즉 attack 이외의 한 행동이 추가될때마다, 그 interface를 구현한 obejct를 찾아가 일일히 코딩해야 한다는 것이다.
- 여기서 찾아낼 수 있는 디자인원칙이 있다.

디자인 원칙1
어플리케이션에서 달라지는 부분을 찾아 달라지지 않는 부분으로부터 분리시킨다

 

디자인 패턴 원리 적용


 - 위 다이어그램에서는 attack, run, drink 등의 행동이, 바뀌는 부분 일 것이다. 그러나 글쓴이의 편의를 위해, attack의 예시만 한번 확인 해보겠다.

- attack에 대한 class 집합은 어떻게 디자인 할 수 있을까?
 우선 최대한 유연하게 만들어야한다. 또한 Person의 인스턴스에 attack을 할당할 수 있어야한다. 즉, Person의 클래스에 Setter를 만들어서 프로그램의 실행중에도 동적으로 할당할수 있는게 가장 좋다.
여기서 디자인원칙 2번째를 알 수있다.

디자인 원칙2
구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

 

 

- 위와 같이 attackBehavior 라는 interface를 구체화하여 공격을 나타내는 클래스를 만들었다.
 (물론 객체지향 패턴에서 클래스는 변수와 메소드를 둘다가지고 있어야한다. 행동만 나타내는 클래스는 이상할 수 있다. 그러나 더 구체화 시키다보면, 클래스별 axe 시너지등을 계산할때 getter setter등을 통해 내부 멤버변수를 다룰 수도 있을것이다.)

 

 

- 위에서 만든 캡슐화된 공격 행동을 Person과 합쳤다.
 기존 객체지향 프로그래밍의 "A는 B이다" 라는 관계가 아니라 "A에는 B가있다" 와 같이 합치는 것을 구성(composition) 이라고 한다. 이 테크닉은 매우 중요하며 디자인의 3번째 원칙이다

디자인 원칙3
상속보다는 구성을 활용한다.

 

 위 패턴이 바로 Starategy Pattern 이다. 즉 각각을 캡슐화 하여, 교환해서 사용할 수 있도록 만든다. Starategy Pattern을 적용한다면, Object를 사용하는 클라이언트와는 독립적으로 Obejct의 캡슐화된 행위등을 변경할 수 있다.

 

 

위 글은 Head first design Patterns책의 내용을 기초로 작성되었습니다.

'To be Developer > DesignPatterns' 카테고리의 다른 글

5.Singleton Pattern  (0) 2020.01.07
4-2 Factory Pattern (Abstract Factory Pattern)  (0) 2020.01.05
4-1.Factory Pattern (Factory Method Pattern)  (0) 2020.01.04
3.Decorator Pattern  (0) 2020.01.02
2.Observer Pattern  (0) 2020.01.01

+ Recent posts