문제 제기

 - "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

+ Recent posts