728x90

팩토리 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

  • 느슨한 결합을 이용하는 객체지향 디자인, 객체의 인스턴스를 만드는 작업이 항상 공개되어 있어야 하는 것은 아니며, 오히려 결합과 관련된 문제가 생길 수 있다. 팩토리 패턴을 이용하여 불필요한 의존성을 없애보자

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

“new”는 “구상 객체”를 뜻한다.

new를 사용하는 것은 구상 클래스의 인스턴스를 만드는 것이다. 당연히 인터페이스가 아닌 특정 구현을 사용하는 것, 앞에서 디자인 패턴을 공부하면서 구상 클래스를 바탕으로 코딩을 하게 되면 코드를 수정해야 할 가능성이 높아지고, 유연성이 떨어지는 것을 볼 수 있었다.

Duck duck = new MallardDuck();

일련의 구상 클래스들이 존재하는 경우, 다음과 같은 코드를 만들어야 하는 경우가 있다.

Duck duck;

// 컴파일시에는 어떤 것의 인스턴스를 만들어야 할지 알 수 없다.
if(picnic) {
        duck = new MallardDuck();
} else if() {
        duck = new DecoyDuck();
} else if() {
        duck = new RubberDuck();
}

이런 코드가 있다는 것은, 변경하거나 확장해야 할 때 코드를 다시 확인하고 추가 또는 제거해야 한다는 것을 뜻한다. 따라서 코드를 이런식으로 만들면 관리 및 갱신하기가 어려워지고, 오류가 생길 가능성이 높아지게 된다.

사실 “new” 자체에 문제가 있는 것은 아니다. 가장 문제가 되는 점은 “변경”이다. 뭔가 변경되는 것 때문에 new를

사용하는데 있어서 조심해야 하는 것이다.

그렇기 때문에 인터페이스에 맞춰서 코딩을 하면 다형성 덕분에 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문에 여러 변경에 대해 유연함을 가질 수 있는 것이다. 반대로 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하기 때문에 많은 문제가 생길 수 있다. 즉 OCP 원칙

바뀌는 부분을 찾아보자

피자 가게를 운영하고 있다고 생각해보자.

피자의 종류가 다양할 것이나, 새로운 피자 신메뉴를 출시하거나 메뉴가 사라질 수 있을 것이다.

따라서 oderPizza() 메소드에서 가장 문제가 되는 부분은 바로 인스턴스를 만들 구상 클래스를 선택하는 부분이다. 해당 부분 때문에 변화에 따라 코드를 변경할 수 밖에 없다. 이제 바뀌는 부분과 바뀌지 않는 부분을 파악했으니 캡슐화할 차례이다.

Pizza orderPizza(String type) {
        Pizza pizza = null; // 인터페이스

                // 바뀌는 부분
        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }

                // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

객체 생성 부분을 캡슐화해보자

이제 객체를 생성하는 부분을 메소드에서 뽑아내어 피자 객체를 만드는 일만 전담하는 다른 객체에 집어넣어 보자.

피자를 만드는 일만 처리하는 객체를 집어 넣어 다른 객체에서 피자를 만들어야 하는 일이 있으면 해당 객체에게 부탁하는 객체 추가해보자. 새로 만들 객체에는 팩토리라는 이름을 붙이기로 하자. SimplePizzaFactory를 만들고 나면 orderPizza() 메소드는 새로 만든 객체의 클라이언트가 된다. 즉 새로 만든 객체를 호출하는 것. 피자 공장에 피자 하나 만들어 달라고 부탁한다고 생각하면 쉽다.

SimplePizzaFactory를 추가하자

피자 객체 생성을 전달한 클래스를 정의한다.

// 해당 클래스에서 하는 일은 클라이언트를 위해 피자를 만들어 주는 일 뿐이다.
public class SimplePizzaFactory {

    public Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }
        return pizza;
    }

}

Q) 이렇게 하면 어떤 장점이 있는 것일까?? 얼핏 보면 아까 문제를 다른 객체로 넘겨 버린 것 처럼 보일 수 있어 보인다.

  • SimplePizzaFactory를 사용하는 클라이언트가 매우 많을 수 있는 점을 생각 하자
  • 피자 객체를 받아서 가격, 설명 등을 찾아서 활용하는 클래스 또는 피자 주문을 처리하는 클래스에서도 이 팩토리를 사용할 수 있을 것이다.
  • 따라서 피자리를 생성하는 작업을 한 클래스에 캡슐화시켜 놓으면 구현을 변경해야 하는 경우에 여기저기 다 들어가서 고칠 필요 없이 팩토리 클래스 하나만 고치면 된다.
  • 추후 클라이언트 코드에서 구상 클래스의 인스턴스를 만드는 코드를 없애는 작업 진행

Q) 비슷한 식으로 메소드를 정적 메소드를 선언한 디자인(정적 팩터리 메소드)과 차이점은 무엇일까?

  • 정적 팩토리 메서드를 사용하면 객체를 생성하기 위한 메소드를 실행시키기 위해서 객체의 인스턴스를 만들지 않아도 되기 때문에 간단한 팩토리를 정적 메소드를 정의하는 기법도 일반적으로 많이 쓰인다.
  • 하지만 서브클래스를 만들어서 객체 생성 메소드의 행동을 변경시킬 수 없다는 단점이 존재한다.

간단한 팩토리를 이용한 PizzaStore 수정

public class PizzaStore {

    SimplePizzaFactory simplePizzaFactory;

    public PizzaStore(SimplePizzaFactory simplePizzaFactory) {
        this.simplePizzaFactory = simplePizzaFactory;
    }

    Pizza orderPizza(String type) {
        Pizza pizza = simplePizzaFactory.createPizza(type);

        // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

}

PizzaStore

  • 팩토리를 사용하는 클라이언트
  • 팩토리를 통해 피자 인스턴스를 받게 된다.

SimplePizzaFactory

  • 피자 객체를 만드는 팩토리
  • 애플리케이션에서 유일하게 구상 Pizza 클래스를 직접 참조한다.

팩토리 메서드

피자 가게가 큰 성공을 거두어 여러 지점을 가지게 되었고, 지역별로 조금씩 다른 차이점이 존재했고, 각각 지역의 특성을 반영하여 피자를 만들어야 했다. 이런 차이점을 어떤 식으로 적용해야 할까??

간단하게 생각하면 SimplePizzaFactory를 빼고 지역별 피자 팩토리(NYPizzaFactory, ChicagoPizzaFactory)를 만든 다음, PizzaStore에서 해당하는 팩토리를 사용하도록 하면 될 것이다.

NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");

ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.orderPizza("Veggie");

피자 가게 프레임워크 - 팩토리 메소드 선언

피자를 만드는 활동 자체를 전부 PizzaStore 클래스에 국한시키면서도 분점마다 고유의 스타일을 살릴 수 있도록 하기 위해서 createPizza() 메소드를 다시 PizzaStore에 집어 넣고 추상 메서드로 선언하고, 각 지역마다 고유의 스타일에 맞게 PizzaStore의 분점을 나타내는 서브클래스를 만들도록 할 것이다. 즉 피자의 스타일은 각 서브클래스에서 결정하는 것

public abstract class PizzaStore {

    Pizza orderPizza(String type) {

        Pizza pizza = createPizza(type); // 팩토리 객체가 아닌 메소드 호출

        // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

    // 팩토리 객체 대신 해당 "메소드" 사용
    // "팩토리 메소드"가 추상 메소드로 바뀌었다.
    abstract Pizza createPizza(String type);

}

서브클래스에서 결정되는 것

각 서브클래스에서 달라질 수 있는 것은 피자의 만들 때의 스타일 뿐이다. 이렇게 달라지는 점들을 createPizza() 메소드에 집어넣고 그 메소드에서 해당 스타일의 피자를 만드는 것을 모두 책임진다. PizzaStore의 서브클래스에서 createPizza() 메소드를 구현하도록 하면 PizzaStore 프레임워크에 충실하면서도 각각의 스타일을 제대로 구현할 수 있는 PizzaStore 서브클래스들을 구비할 수 있다.

  • PizzaStore에서 정의한 메서드를 서브클래스에서 고쳐서 쓸 수 없게 하고 싶다면 메서드를 final로 선언할 수도 있다.
  • ex) final Pizza orderPizza(String type)
public class NYPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new NYStyleCheesePizza();
        } else if (type.equals("greek")) {
            pizza = new NYStyleGreekPizza();
        }
        return pizza;
    }
}

public class ChicagoPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new ChicagoStyleCheesePizza();
        } else if (type.equals("greek")) {
            pizza = new ChicagoStyleGreekPizza();
        }
        return pizza;
    }
}
  • 슈퍼클래스에 있는 orderPizza() 메소드에서는 Pizza 객체를 가지고 여러 작업을 하긴 하지만, Pizza는 추상 클래스기 때문에 어떤 구상 클래스에서 작업이 처리되고 있는지 전혀 알 수없다.
    • 즉 PizzaStore와 Pizza는 서로 완전히 분리되어 있다.
    • 그렇다면 피자 종류를 결정하는 것은 누구일까??
  • orderPizza() 입장에서 볼 때는 PizzaStore 서브클래스에서 피자 종류를 결정한다고 할 수 있을 것이다.
    • 따라서 서브 클래스에서 실제로 뭔가를 “결정”하는 것이 아니라, 우리가 선택하는 PizzaStore의 서브클래스 종류에 따라 결정되는 것이지만, 만들어지는 피자의 종류를 해당 PizzaStore 서브클래스에서 결정한다고 할 수 있다.

팩토리 메소드

abstract Product factoryMethod(String type);

팩토리 메소드는 객체 생성을 처리하며, 팩토리 메소드를 이용하면 객체를 생성하는 작업을 서브클래스에 캡슐화시킬 수 있다. 이렇게 하면 슈퍼 클래스에 있는 클라이언트 코드와 서브클래스에 있는 객체 생성 코드를 분리시킬 수 있다.

  • abstract
    • 팩토리 메소드는 추상 메소드로 선언하여 서브클래스에서 객체 생성을 책임지도록 한다.
  • Product
    • 팩토리 메소드에서는 특정 객체를 리턴하며, 그 객체는 보통 수퍼 클래스에서 정의한 메소드 내에서 쓰이게 된다.
  • factoryMethod
    • 팩토리 메소드는 클라이언트 (ex : orderPizza method)에서 실제로 생성되는 구상 객체가 무엇인지 알 수 없게 만드는 역할도 한다.
  • type
    • 팩토리 메소드를 만들 때 매개변수를 써서 만들어낼 객체 종류를 선택할 수도 있다.

사용 code

public class Main {

    public static void main(String[] args) {
        PizzaStore nyStore = new NYPizzaStore();
        PizzaStore chicagoStore = new ChicagoPizzaStore();

        Pizza pizza = nyStore.orderPizza("cheese");
        pizza = chicagoStore.orderPizza("cheese");

    }

}

팩토리 메소드 패턴

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

  • 모든 팩토리 패턴에서는 객체 생성을 캡슐화한다.
  • 팩토리 메소드 패턴에서는 서브 클래스에서 어떤 클래스를 만들지를 결정하게 함으로써 객체 생성을 캡슐화한다.
  • 즉 팩토리 메서드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다.
  • 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

클래스 다이어그램을 살펴보자

Creator : 생산자

  • ex) PizzaStore Class
  • 제품을 가지고 원하는 일을 하기 위한 모든 메소드들이 구현되어 있다.
  • 하지만 제품을 만들어 주는 팩토리 메소드는 추상 메소드로 정의되어 있을 뿐, 구현되어 있진 않다.
  • Creator의 모든 서브 클래스에서 factoryMethod() 추상 메소드를 구현해야 한다.

ConreteCreator : 구상 생산자

  • 실제로 제품을 생산하는 factoryMethod()를 구현한다.

Product / ConreteProduct

  • 제품 클래스에서는 모두 똑같은 인터페이스 구현
  • 그래야 제품을 사용할 클래스(클라이언트)에서 구상 클래스가 아닌 인터페이스에 대한 레퍼런스를 써서 객체를 참조할 수 있기 때문

ConreteCreator → ConreteProduct

  • 구상 클래스 인스턴스를 만들어내는 일은 ConreteCreator가 책임진다.
  • 실제 제품인 ConreteProduct 객체를 만들어내는 방법을 알고 있는 클래스는 ConreteCreator 클래스 뿐

병렬 클래스 계층 구조

  • Product, Creator 클래스 둘 다 추상 클래스로 시작하고, 그 클래스를 확장하는 구상 클래스들을 가지고 있다.
  • 구체적인 구현은 구상 클래스들이 책임지고 있다.
  • 구상 생산자 클래스에는 특정 구상 제품군에 대한 모든 지식이 캡슐화 되어 있으며 팩토리 메소는 이러한 지식을 캡슐화 시키는데 있어서 가장 핵심적인 역할을 맡고 있다.

Simple Factory vs Factory Method Pattern

  • 뉴욕과 시카고 분점을 만들 때 간단한 팩토리를 사용했다고 할 수 있지 않을까??
    • 비슷하긴 하지만 방법이 조금 다르다.
    • 구상 클래스를 만들 때 createPizza() 추상 메소드가 정의되어 있는 추상 클래스를 확장해서 만들었다는 점이 중요한 차이
      • createPizza() 메소드에서 어떤 일을 할지는 각 분점에서 결정한다.
    • 간단한 팩토리를 사용할 때는 팩토리가 PizzaStore 안에 포함되는 별개의 객체였다는 큰 차이점이 존재한다.
  • simple factory
    • 일회용 처방에 불과하다.
    • 객체 생성을 캡슐화하는 방법을 사용하긴 하지만 생성하는 제품을 마음대로 변경할 수 없기 때문에 강력한 유연성을 제공하진 못한다.
  • factory method pattern
    • 어떤 구현을 사용할지를 서브클래스에서 결정하는 프레임워크를 만들 수 있다.
    • 강력한 유연성을 제공한다.

의존적인 PizzaStore

  • 팩토리를 사용하지 않는 PizzaStore 클래스는 피자 객체가 의존하고 있는 구상 피자 객체의 개수만큼 의존하게 된다.
  • 왜냐하면 객체 인스턴스를 직접 만들어 구상 클래스에 의존하기 때문이다.

https://msyu1207.tistory.com/entry/4장-헤드퍼스트-디자인-패턴-팩토리-패턴

  • 적용 전

https://msyu1207.tistory.com/entry/4장-헤드퍼스트-디자인-패턴-팩토리-패턴

  • 팩토리 메소드 패턴을 적용한 다이어 그램

DIP : 의존성 역전 원칙

  • 구상 클래스에 대한 의존성을 줄이는 것이 좋다는 내용을 정리해 놓은 객체지향 디자인 원칙
  • 이 원칙은 다음과 같이 일반화 시킬 수 있다.
    • 추상화된 것에 의존하도록 만들어라
    • 구상 클래스에 의존하도록 만들지 않아야 한다.
  • 이 원칙하고 “특정 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다”는 원칙이 똑같다고 생각할 수 있다.
    • 비슷하긴 하지만 dip에서는 추상화를 더 많이 강조한다.
    • 해당 원칙에는 고수준 구성요소가 저수준 구성요소에 의존하면 안된다는 것이 내포되어 있다.
    • 항상 추상화에 의존하도록 만들어야 한다.
  • 그럼 고수준, 저수준은 어떤 의미일까??
    • 고수준 구성요소
      • 고수준 구성요소는 다른 저수준 구성요소에 의해 정의되는 행동이 들어있는 구성요소를 뜻한다.
      • ex) PizzaStore
      • PizzaStore의 행동은 피자에 의해 정의되기 때문에 고수준 구성요소라고 할 수 있다.
    • 저수준 구성요소
      • 이 때 PizzaStore에서 사용하는 피자 객체들은 저수준 구성요소라고 할 수 있다.
  • 팩토리 패턴을 사용하지 않은 기존의 PizzaStore 클래스는 구상 피자 클래스들에 의존하고 있다.
  • dip 원칙에 의하면, 구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 한다.
    • 이 원칙은 고수준 모듈과 저수준 모듈에 모두 적용될 수 있다.
  • dip 원칙을 지키는데 도움이 될만한 가이드라인
    1. 어떤 변수에도 구상 클래스에 대한 레퍼런스를 저장하지 않는다.
    2. 구상 클래스에서 유도된 클래스를 만들지 않는다.
    3. 베이스 클래스에 이미 구현되어 있던 메소드를 오버라이드 하지 않는다.
    • 해당 가이드들은 항상 지켜야 하는 규칙이 아니라 지향하는 바를 나타낸 것
    • 이 가이드라인을 완벽하게 따를 순 없다.
728x90
728x90

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

데코레이터 패턴을 이용하면 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.

개요

OO커피는 단기간에 급속도로 성장한 대형 커피 전문점이다. 빠르게 성장한 만큼, 음료들을 모두 포괄하는 주문 시스템이 이제서야 개발되려고 하는 상황이다. 처음 시스템 시작할 무렵에 만들어진 클래스는 위의 사진과 같다.

Beverage : 음료를 나타내는 추상 클래스, 모든 음료는 해당 클래스의 서브클래스

  • description : 해당 인스턴스 변수는 서브클래스에서 설정되며, 음료의 설명이 저장된다.
  • cost() : 추상 메서드이며, 서브 클래스에서 해당 메서드를 구현해야 한다.

하지만 음료에는 스팀 우유, 두유, 모카, 휘핑 크림 등 옵션을 추가할 때마다 가격이 달라진다. 이러한 경우를 모두 고려한다면??

이처럼 클래스 개수가 폭발적으로 증가하게 된다. 만약 우유나 크림 가격이 인상된다면?? 한눈에 보기에도 이렇게 수많은 클래스를 관리하기는 힘들 것이다.

그러면 슈퍼 타입인 Beverage 클래스에 인스턴스 변수로 관리하면 안될까??

  • milk, soy, mocha, ... ,whip : 각 추가 요소에 해당하는 인스턴스 변수 추가
  • cost() : 각 음료 서브 클래스의 인스턴스마다 추가 사항에 해당하는 추가 가격까지 포함할 수 있도록 기본 음료 값을 가져와서 오버라이드 하기위해 추상 메소드가 아닌 구현 메소드로 수정
  • 부울 인스턴스 변수를 위한 게터, 세터
public class Beverage {

    String description;
    boolean hasMilk, hasSoy, hasMocha;
    double milkCost, soyCost, mochaCost;

    public double cost() {
        double condimentCost = 0;
        if (getHasMilk()) {
            condimentCost += milkCost;
        }
        if (getHasSoy()) {
            condimentCost += soyCost;
        }
        if (getHasMocha()) {
            condimentCost += mochaCost;
        }
        return condimentCost;
    }

    // get, set..
    public boolean getHasMilk() {
        return hasMilk;
    }

    public boolean getHasSoy() {
        return hasSoy;
    }

    public boolean getHasMocha() {
        return hasMocha;
    }

}
public class DarkRoast extends Beverage{

    public DarkRoast() {
        description = "다크 로스트";
    }

    @Override
    public double cost() {
        return super.cost() + 3500;
    }
}

아까와 같은 클래스 폭발을 막게되었다. 하지만 아직 확신이 서지 않는다. 어떤 문제점이 있을 수 있을까??

  • 음료에 추가되는 옵션(우유, 휘핑, 모카 등)의 가격이 바뀔때마다 코드를 수정해야 한다.
  • 음료에 추가되는 옵션의 종류가 많아지면 그 때마다 메소드를 추가하고, 슈퍼 클래스의 cost() 메소드를 수정해야 한다.
  • 새로운 음료가 추가되는 경우, 옵션이 없는 경우와 우유가 들어가지 않는 음료임에도 관련된 멤버들을 상속받게 된다.
  • 만약 샷이라는 옵션이 추가되고 샷을 두번 추가한 음료는 어떻게 해야할까??

상속은 객체지향 디자인의 강력한 요소 중 하나지만, 이처럼 상속을 사용한다고 해서 무조건 유연하고 관리하기 쉬운 디자인이 만들어지지 않는다. 그 이유는 서브 클래스를 만드는 방식으로 행동을 상속 받으면 해당 행동은 컴파일시에 완전히 결정되고 모든 서브클래스에서 슈퍼 클래스의 멤버들을 상속 받아야 하기 때문이다. 하지만 composite를 통해서 객체의 행동을 실행 중에 동적으로 설정하는 방법을 사용한다면, 즉 객체를 동적으로 구성하면, 기존 코드를 수정하는 대신 새로운 코드를 추가하는 방식으로 새로운 기능을 추가할 수 있다. 기존 코드는 수정되지 않으므로(변경에 대해서는 닫혀있으므로) 버그가 생기거나 사이드 이펙트를 방지하면서 새로운 기능을 추가(확장에 대해서는 열려있는)할 수 있는 것이다.

데코레이터 해보자!

이제 음료에서 추가되는 옵션이 있는 경우 해당 음료를 데코레이터 하는 방식으로 수정해보자. 만약 모카와 휘핑 크림을 추가한 다크 로스트 커피는 다음처럼 할 수 있을 것이다.

  1. DarkRoast 객체를 가져온다
  2. Mocha 객체로 장식한다.
  3. Whip 객체로 장식한다.
  4. cost() 메소드를 호출한다. 이때 추가 옵션의 가격을 계산하는 일은 해당 객체들에게 위임한다.

이 때 장식하고 위임하는 방법은 해당 객체를 래퍼 객체라고 생각하면 쉽다.

이렇게 가장 바깥쪽에 있는 데코레이터 객체에서 cost()를 호출하고, 해당 객체가 장식하고 있는 객체에게 가격을 위임한다. 위임한 객체에게 가격의 값을 얻으면, 자신의 가격을 더한 다음 리턴하는 것이다.

여기서 중요한 점은 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수도 있다는 저이다.

데코레이터 패턴

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

Compent

  • 각 구성요소는 직접 사용할 수도 있고 데코레이터로 감싸져서 쓰일 수도 있다.
  • ex) Beverage 클래스

ConcreteComponent

  • 해당 클래스에 새로운 행동을 동적으로 추가하게 된다.

Decorator

  • 데코레이터는 자신이 장식할 Component와 같은 인터페이스 또는 추상 클래스를 구현한다.
  • 각 데코레이터 안에는 Component 객체가 들어있다. 즉, 데코레이터에는 구성요소에 대한 레퍼런스가 들어있는 인스턴스 변수를 가진다.

ConcreteDecorator

  • ConcreteDecorator 에는 데코레이터가 감싸고 있는 Component 객체를 위한 인스턴스 변수가 있다.
  • Decorator는 Component의 상태를 확장할 수 있다.
  • Decorator에서 새로운 메소드를 추가할 수도 있다. 하지만 일반적으로 새로운 메소드를 추가하는 대신 Component의 메소드를 호출하기 전, 후에 별도의 작업을 처리하는 방식으로 새로운 기능을 추가한다.

Beverage 클래스 다이어그램

public abstract class Beverage {

    String description = "";

    public abstract double cost();

    public String getDescription() {
        return description;
    }
}

public class Espresso extends Beverage {

    public Espresso() {
        description = "에스프레소";
    }

    @Override
    public double cost() {
        return 3500;
    }
}

public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}

public class Mocha extends CondimentDecorator {

    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

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

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }
}
public class StarbuzzCoffee {

    public static void main(String[] args) {
        Beverage beverage = new Espresso();
        System.out.println(beverage.getDescription() + ": " + beverage.cost() + " won");

        Beverage beverage2 = new Espresso();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Mocha(beverage2);
        System.out.println(beverage2.getDescription() + ": " + beverage2.cost() + " won");
    }

}

데코레이터 패턴을 적용한 코드는 아까의 문제점이 사라진 오류의 코드지만 저런형태로 관리하게 될경우 마지막 Soy를 빼먹는다던가 실수로 두번넣는 경우가 생기게됩니다. 팩토리 패턴과 빌더 패턴을 이용해서 더 쉽게 객체를 만드는 방법이 존재한다.

주의해야 할 점

  • 특정 ConcreteComponent 타입을 바탕으로 작업을 처리하는 코드에 데코레이터 패턴을 적용하면 제대로 작동하지 않는다. (ConcreteDecorator로 감싸져 있기 때문)
  • 만약 여러 단계의 데코레이터를 파고 들어가서 어떤 작업을 해야 한다면, 데코레이터 패턴의 의의와 어긋나는 것이다.
  • 데코레이터 패턴에서는 특정한 추상 구성요소를 지정할 필요가 없다. 인터페이스를 사용해도 무방하다.

마무리

  • 상속을 통한 확장은 디자인의 유연성 면에서 좋지 않을 수 있다.
  • 기존 코드를 수정하지 않고도 행동을 확장하는 방법이 필요하다 (OCP)
  • 합성과 위임을 통해서 실행중에 새로운 행동을 추가할 수 있다.
  • 상속 대신 데코레이터 패턴을 통해 행동을 확장할 수 있다.
  • 데코레이터 패턴에서는 구상 구성요소를 감싸주는 데코레이터를 사용한다.
  • 데코레이터 클래스의 형식은 그 클래스가 감싸고 있는 클래스의 형식을 반영한다.
  • 데코레이터에서는 자기가 감싸고 있는 구성요소의 메소드를 호출한 결과에 새로운 기능을 더함으로써 확장한다.
  • 구성요소를 감싸는 데코레이터의 개수에는 제한이 없다.
  • 구성요소의 클라이언트 입장에서는 데코레이터의 존재를 알 수 없다.
    • 따라서 클라이언트에서 구성 요소의 구체적인 타입에 의존하게 되는 경우는 다시 생각해봐야한다.
  • 데코레이터 패턴을 사용하면 객체들이 많이 추가될 수 있고, 코드가 복잡해질 수 있다.
728x90
728x90

옵저버 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

개요

실제 기상 정보를 수집하는 장비인 기상 스테이션과 기상 스테이션으로부터 오는 데이터를 추적하는 객체인 WeatherData, 그리고 사용자에게 현재 기상 조건을 보여주는 디스플레이, 세 요소로 이루어진다.

WeatherData 객체에서는 기상 스테이션으로부터 데이터를 가져올 수 있다. 데이터를 가져온 후에는 디스플레이 장비에 세 가지 항목을 표시할 수 있다.

  • 현조 조건(온도, 습도, 압력)
  • 기상 통계
  • 기상 예보

WeatherData 객체를 사용하여 디스플레이 장비에서 위의 3가지 요소를 갱신해 가면서 보여주는 애플리케이션을 만들어보자

주어진 WeatherData 클래스와 상황

  • 세가지의 게터 메소드는 각각 가장 최근에 측정된 온도, 습도, 기압 값을 리턴하는 메소드
  • measurementsChanged 메소드를 현재 조건, 기상 통꼐, 기상 예측 3가지 디스플레이를 갱신할 수 있도록 구현해야 한다.
  • 시스템은 확장 가능해야 한다. 추후 디스플레이 항목들은 추가/제거될 수 있다.

초기 구현

public class WeatherData {

    // 인스턴스 변수 선언

    public void measurementsChanged() {
        float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();

        currentConditionsDisplay.update(temp, humidity, pressure);
        statisticsDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);

    }

}

위의 코드의 문제는 무엇일까??

  • 인터페이스가 아닌 구체적인 구현을 바탕으로 코딩하고 있다.
  • 새로운 디스플레이 항목이 추가될 때마다 코드를 변경해야 한다.
  • 실행중에 디스플레이 항목을 추가/제거할 수 없다.
  • 바뀌는 부분을 캡슐화하지 않았다.

구체적인 구현에 맞춰서 코딩되어 있기 때문에 코드를 고치지 않고는 다른 디스플레이 항목을 추가하거나 제거할 수 없고 디스플레이에 항목들을 갱신하여 업데이트하는 부분들은 바뀔 수 있는 부분이므로 캡슐화해야 한다.

이제 옵저버 패턴에 대해서 알아보자

옵저버 패턴

쉽게 생각해서 신문 구독 메커니즘과 비슷한다. 즉 출판사 + 구독자 = 옵저버 패턴 인 것

출판사를 주제 or 주체(subject), 구독자를 옵저버(observer)라고 부른다.

  • subject 객체에서 일부 데이터를 관리
  • subejct의 데이터가 달라지면 옵저버한테 소식과 데이터가 전달된다.
  • observer 객체들은 subject 객체를 구독(등록)하고 있으며 subject의 데이터가 바뀌면 갱신 내용을 전달받는다.

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

  • 일대다 관계는 subject와 observer에 의해 정의되고 observer는 subject에 의존한다.
  • 옵저버 패턴을 구현하는 방법에는 여러가지가 있지만 대부분 subject 인터페이스와 observer 인터페이스를 사용한 클래스 디자인을 바탕으로 한다.

Subject 인터페이스

  • 객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때 이 인터페이스에 있는 메소드를 사용한다.

ConcreteSubject

  • Subject 역할을 하는 Concrete 클래스에서는 항상 subject 인터페이스를 구현해야 한다.
  • subject 클래스에서는 등록 및 해지를 위한 메소드 외에 상태가 바뀔때마다 모든 옵저버들에게 연락을 하기 위한 notifyObservers() 메소드도 구현해야 한다.
  • subject 클래스에는 상태를 설정하고 알아내기 위한 세터/게터 메소드가 있을 수도 있다.

Observer 인터페이스

  • observer가 될 가능성이 있는 객체에서는 반드시 observer 인터페이스를 구현해야 한다.
  • observer 인터페이스에는 subject의 상태가 바뀌었을 때 호출되는 update() 메소드 밖에 없다.

ConcreteObserver

  • Observer 인터페이스만 구현한다면 어떤 클래스든 옵저버 클래스가 될 수 있다.
  • 각 옵저버는 특정 주제 객체에 등록을 해서 연락을 받을 수 있다.

옵저버 패턴에서 상태를 저장하고 지배하는 것은 subject 객체이다. 따라서 상태가 들어있는 객체는 하나만 존재하고, 옵저버는 반드시 상태를 가지고 있어야 하는 것은 아니기 때문에 옵저버는 여러 개가 있을 수 있으며 subject 객체에서 상태가 바뀌었다는 알려주기를 기다리는, subject에 의존적인 성질을 가진다.

따라서 하나의 subject와 여러개의 observer가 연관된 일대다 관계가 성립하고 해당 의존을 통해 여러 객체에서 동일한 데이터를 제어하도록 할 수 있다.

느슨한 결합

추가로 옵저버 패턴의 장점은 subject와 observer가 느슨하게 결합되어 있는 디자인을 제공하는 것이다. 두 객체가 느슨하게 결합되어 있다는 것은, 해당 객체들이 상호작용을 하지만 서로에 대해 잘 모른다는 것을 의미한다.

subject가 observer에 대해 아는 것은 해당 옵저버가 observer 인터페이스를 구현한다는 것 뿐이다

  • 옵저버의 구상 클래스, 어떤 행동을 하는지 등, 나머지는 알 필요가 없다.

observer는 언제든지 추가 가능하다

  • subject는 observer 인터페이스를 구현하는 객체 목록에먼 의존하기 때문에 실행중에 한 옵저버를 다른 옵저버로 바꿔도 되고, 언제든지 새로운 옵저버를 추가하거나 삭제할 수 있다.

새로운 형식의 observer를 추가하려고 할 때도 subject를 변경할 필요가 없다.

  • observer 인터페이스에 의조하기 때문에 새로운 옵저버 구상 클래스가 생겨도 문제 없다.

subject와 observer는 서로 독립적으로 재사용할 수 있다.

  • 다른 용도로 활용할 일이 있어도 문제 없다. 느슨하게 결합되어 있기 때문

subject와 observer에 변경이 생겨도 서로에게 영향이 미치지 않는다.

  • 마찬가지로 느슨한 결합 덕분

옵저버 패턴 적용

  • Subject, Observer 인터페이스 생성
  • WeatherData는 Subject 인터페이스를 구현하도록 수정
  • 모든 기상 구성요소에서 Observer 인터페이스를 구현하도록 수정
    • subject 객체에서 갱신된 데이터를 전달할 수 있도록 메소드 제공
  • 모든 디스플레이 항목에서 구현하는 DisplayElement 인터페이스를 하나 더 추가
    • 측정값을 바탕으로 각각 다른 내용을 표시하는 메소드인 display() 메소드 추가
public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObserver();
}

public interface Observer {
    void update(float temperature, float humidity, float pressure);
}

public interface DisplayElement {
    void display();
}
import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {

    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        this.observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if(i >= 0) {
            observers.remove(i);
        }
    }

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

    public void measurementsChanged() {
        notifyObserver();
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}
public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        this.weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}
public class WeatherStation {

    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay conditionsDisplay = new CurrentConditionsDisplay(weatherData);
        weatherData.setMeasurements(70, 60, 30.4f);
        weatherData.setMeasurements(55, 50, 15.4f);
        weatherData.setMeasurements(90, 70, 40.4f);
    }

}

데이터 전달 시 두가지 방식

현재는 subject의 상태가 변경될 때마다 observer에게 알려주고(push) 있다. 옵저버 입장에서는 필요한 상황에서만 주체의 상태를 가져오는 방식(pull)이 더 편할 수도 있지 않을까??

이처럼 옵저버 패턴은 PUSH 방식과 PULL 방식으로 구분할 수 있다.

PUSH 방식 : 주제의 내용이 변경될 때마다 구독자에게 알려주는 방식

PULL 방식 : 구독자가 필요할 때마다 주제에게 정보를 요청하는 방식

또한 자바에서 몇 가지 API를 통해 자체적으로 옵저버 패턴을 지원한다. 일반적으로 java.util 패키지에 들어있는 Observer 인터페이스와 Observable 클래스이다. 해당 내장 클래스들은 푸시 방식과 풀 방식 모두 가능하다.

그렇다면 자바 내장 클래스를 이용하여 풀 방식으로 수정해보자

Pull 방식으로 수정(with 자바 내장 옵저버 패턴)

  • Observable은 인터페이가 아니라 클래스이므로 WeatherData 클래스에서 해당 클래스를 상속하면서 메소드들을 상속받는다.
  • setChange() 제공
    • 해당 메소드는 상태가 바뀌었다는 것을 밝히기 위한 용도로 사용된다. setChange() 메소드가 호출되지 않은 상태에서 notifiyObservers()가 호출되면 옵저버들에게 연락이 가지 않는다. 해당 메소드를 조건에 따라서 적절히 호출하여 옵저버들에게 연락이 가는 것을 제어할 수 있다.
  • Observer 인터페이스는 앞에서 만든 클래스와 거의 똑같다.

바뀐 WeatherData

import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import observer.after.Observer;
import observer.after.Subject;

public class WeatherDataObservable extends Observable {

    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherDataObservable() {
    }

    public void measurementsChanged() {
        setChanged();
        notifyObservers(); // pull 방식, push 방식인 경우 notifyObservers(Object arg);
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    // pull 방식이므로 옵저버가 주체 객체의 상태를 알아야하므로 필요하다.
    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}
import java.util.Observable;
import java.util.Observer;
import observer.after.DisplayElement;
import observer.after2.WeatherDataObservable;

public class CurrentConditionsDisplayObserver implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    Observable observable;

    public CurrentConditionsDisplayObserver(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        if(o instanceof WeatherDataObservable) {
            WeatherDataObservable weatherData = (WeatherDataObservable) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}

java.util.Observable의 단점

Observer와 Observable은 Java SE 9 버전부터 Deprecated 되었다. 그 이유는 무엇일까?

  • Observer와 Observable이 제공하는 이벤트 모델이 제한적이다.
  • Observable의 notify는 순서를 보장할 수 없으며, 상태 변경은 1:1로 일치하지 않는다.
  • 더 풍부한 이벤트 모델은 java.beans 패키지가 제공하고 있다.
  • 멀티 스레드에서의 신뢰할 수 있고 순서가 보장된 메시징은 java.util.concurrent 패키지의 concurrent 자료 구조들 중 하나를 골라 쓰는 편이 낫다.
  • reactive stream 스타일 프로그래밍은 Flow API를 쓰기를 권한다.

Observable의 문제는 헤드 퍼스트 디자인 패턴에서도 지적하고 있다

  • Observable이 interface가 아니라 class이다.
    • 다른 클래스를 상속하는 클래스가 Observable을 상속할 수 없다. 따라서 재사용성에 제약이 생긴다.
    • 내장된 Observer API하고 잘 맞는 클래스를 직접 구현하는 것이 불가능하다.
  • Obserable 클래스의 핵심 메소드를 외부에서 호출할 수 없다.
    • setChanged() 메소드가 protected으로 선언되어 있기 때문이다.
    • 상속보다 구성을 사용한다는 디자인 원칙을 위배한다.

마무리 핵심 정리

  • 옵저버 패턴에서는 객체들 사이에 일대다 관계를 정의한다.
  • 주체 객체는 동일한 인터페이스를 써서 옵저버에 연락한다.
  • 주체와 옵저버는 서로 느슨한 결합
  • 주체에서 데이터를 보내는 푸시방식과 옵저버가 데이터를 가져오는 풀방식이 있다.
    • 풀 방식 추천
  • 옵저버들한테 연락을 돌리는 순서에 절대로 의존하면 안 된다.
    • 만약 의존하도록 했다면 잘못된 것, 느슨한 결합이라고 볼 수 없다.
728x90
728x90

전략 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

전략 패턴 은 알고리즘군을 정의하고 각각의 알고리즘을 캡슐화하며 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

개요

오리 연못 시뮬레이션 게임에서 오리는 헤엄도 치고 꽥꽥거리는 소리도 내는 매우 다양한 오리 종류를 보여줄 수 있다. 가장 처음에는 표준적인 객체지향 기법을 사용하여 Duck이라는 수퍼클래스를 만든 다음, 그 클래스를 확장하여 다른 모든 종류의 오리를 생성

예제 코드

public abstract class Duck {

    void quack() {
        System.out.println("duck quack");
    }

    void swim() {
        System.out.println("duck swim");
    }

    abstract void display();

}

class MallardDuck extends Duck {

    @Override
    void display() {
        System.out.println("MallardD Duck");
    }
}

class RedheadDuck extends Duck {

    @Override
    void display() {
        System.out.println("Redhead Duck");
    }
}

이제 오리들이 날아다닐 수 있도록 해야한다고 가정했을 경우 어떻게 해야할까??

public abstract class Duck {

    void quack() {
        System.out.println("duck quack");
    }

    void swim() {
        System.out.println("duck swim");
    }

    abstract void display();

    void fly() {
        System.out.println("duck fly");
    }

}

슈퍼클래스에 fly()메소드를 추가함으로써 모든 서브 클래스에서 fly()를 상속받도록 수정

class MallardDuck extends Duck {

    @Override
    void display() {
        System.out.println("Mallard Duck");
    }
}

class RedheadDuck extends Duck {

    @Override
    void display() {
        System.out.println("Redhead Duck");
    }
}

class RubberDuck extends Duck {

    @Override
    void quack() {
        System.out.println("rubber duck quack");
    }

    @Override
    void display() {
        System.out.println("Rubber Duck");
    }
}

하지만 해당 변경사항은 모든 오리가 날 수 있지 않다는 것을 간과했다. 고무로 된 오리 인형 클래스에도 비행 기능하게 된 것이다. 결론적으로 Duck 슈퍼클래스에 fly() 메소드가 추가되면서 일부 서브클래스에는 적합하지 않은 행동이 추가될 수 있는 사이드 이펙트가 발생하게 된 것이다.

RubberDuck의 quack() 메소드처럼 fly() 오버라이드를 해서 아무것도 하지 않게 한다면 가능은 하지만 어떤 부작용이 있을까??

상속의 문제점

이처럼 Duck 행동을 제공하는데 있어서 상속을 사용할 때의 단점은 많다.

  • 서브 클래스에서 코드가 중복
  • 모든 서브클래스의 행동을 알기가 어려움
  • 코드를 변경했을 때 원치 않는 서브클래스들에게 영향을 끼칠 수 있음
  • 실행시에 특징 변경 어려움
  • 모든 오리의 행동을 알기 힘듬

인터페이스?

그렇다면 상속 대신 Flyable, Quackable 인터페이스를 사용하는 방법은 어떨까??

서브클래스에서 Flyable, Quackable 인터페이스를 구현함으로써 고무오리의 비행기능 같은 문제점을 해결할 수 있지만, 코드 재사용성과 관리측면에서 더욱 큰 문제점이 발생하게 된다. (Java 8이하라고 가정)

문제를 명확하게 파악하기

디자인원칙1 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리킨다.

결국 가장 중요한 것은 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화함으로써 나머지 부분에 영향을 미치지 않도록 한다면 시스템의 유연성을 향상시키는 것이다.

즉 확장에는 열려있고 수정에 대해서는 닫혀있도록 하는 것!!

모든 패턴은 시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는 방법을 제공하기 위한 것이다.

*스트레티지 패턴 (Straetegy 패턴) *

Duck 예제에서 달라지는 부분 뽑아보기

Duck 클래스에서 나는 행동과 꽥꽥거리는 행동을 추출하여 행동을 나타낼 클래스 집합을 새로 추가

  • 최대한 유연하게 만들어야 한다.
  • 오리의 행동을 동적으로 바꾸고싶다
    • 세터 메소드를 포함시켜서 동적으로 바꿀 수 있도록 한다면 좋을 것이다.

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

각 행동들을 인터페이스 (FlyBehavior, QuackBehavior)로 표현하고 행동을 구현할 때 Duck 클래스에서 구현하느 것이 아닌 구체적인 행동 클래스에서 구현함으로써 특정 행동은 Duck 국한되지 않는다.

public interface FlyBehavior {
    void fly();
}

public class FlyNoWay implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("날 수 없다");
    }
}

public class FlyWithWings implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("나는 방법을 구현");
    }
}
public interface QuackBehavior {

    void quack();
}

public class Quack implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("꽥");
    }
}

public class MuteQuack implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("소리 낼 수 없어요");
    }
}

public class Squeak implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("삑삑");
    }
}

이로써 비행과 꽥꽥거리는 행동들은 Duck 클래스 안에 숨겨져 있지 않으므로 다른 객체에서도 이러한 행동들을 재사용할 수 있다. 그리고 기존의 Duck 클래스들을 전혀 수정하지 않고도 새로운 행동을 추가할 수 있다.

상속의 단점을 해결하는 것 뿐만 아니라 재사용성을 가질 수 있다.

결과

이제 Duck 클래스는 행동을 다른 클래스에 위임함으로써 특정 행동을 할 수 있게 되었다.

public abstract class Duck {

    private FlyBehavior flyBehavior;
    private QuackBehavior quackBehavior;

    public Duck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        this.flyBehavior = flyBehavior;
        this.quackBehavior = quackBehavior;
    }

    public void performFly() {
        flyBehavior.fly();
    }

    public void performQuack() {
        quackBehavior.quack();
    }

    public abstract void display();

    public void swim() {
        System.out.println("모든 오리는 수영한다.");
    }
}

class RubberDuck extends Duck {

    public RubberDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        super(flyBehavior, quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Rubber Duck");
    }
}

class MallardDuck extends Duck {

    public MallardDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        super(flyBehavior, quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Mallard Duck");
    }
}

class RedheadDuck extends Duck {

    public RedheadDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        super(flyBehavior, quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Redhead Duck");
    }
}

결론

여태까지 사용했던 행동들을 일련의 행동이 아닌 알고리즘군으로 생각하고 똑같은 테크닉을 적용가능 하다.

ex) 지역별 세금 계산 방식 등등

각 오리에는 나는 행동과 꽥꽥하는 행동을 위임하기 위한 클래스들이 있다. 이처럼 두 클래스를 합치는 것을 구성(Composition)을 이용하는 것이라 한다. 오리 클래스에서는 행동을 상속받는 대신, 행동 객체로 구성됨으로써 행동을 부여받게 된다. 구성을 이용하면 유연성을 크게 향상시킬 수 있다. 캡슐화할 수 있도록 만들어주는 것 뿐만 아니라 실행시에 행동을 바꿀 수도 있게 해주기 때문이다.

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

참고 출처

728x90
728x90

디자인 패턴이란


  • 디자인 패턴 은 소프트웨어 디자인에서 일반적으로 발생하는 문제에 대한 일반적인 솔루션이다.
  • 이는 코드에서 반복되는 디자인 문제를 해결하기 위해 사용자 지정할 수 있는 미리 만들어진 청사진과 같다고 생각하면 좋다고 한다.
  • 패턴은 특정 문제를 해결하기 위한 일반적인 개념이다.
  • 패턴과 알고리즘 두 개념 모두 알려진 문제에 대한 일반적인 솔루션을 설명하기 때문에 비슷해 보이지만 다르다.
    • 알고리즘은 항상 어떤 목표를 달성할 수 있는 명확한 일련의 작업을 정의하고 요리법에 비유될 수 있다.
    • 패턴은 솔루션에 대한 보다 높은 수준의 설명이다. 프로그램 개발 시에 자주 부닥치는 애로 상황에 대한 일반적이고 재사용 가능한 추상화된 해결책이다.
      • 결과와 그 기능이 무엇인지 볼 수 있지만 정확한 구현 순서는 사용자에게 달려 있다.

디자인 패턴 장점


  1. 개발자(설계자) 간의 원활한 의사소통
    • 여러 디자인 패턴의 특성을 잘 알고 있어 문제해결 시 어떤 디자인 패턴을 사용하면 좋을지 해결책을 논의할 수 있다.
  2. 소프트웨어 구조 파악 용이
    • 디자인 패턴의 특성을 잘 알고 있기에 어떤 디자인 패턴이 설계할 때 사용되었는지 알면 소프트웨어 전체구조를 쉽게 파악 가능하다.
  3. 재사용을 통한 개발 시간 단축
    • 이미 만들어 놓은 디자인 패턴을 사용하므로 개발시간을 단축시킬 수 있다.
  4. 설계 변경 요청에 대한 유연한 대처
    • 사용자의 지속적인 추가 요청, 환경 변화 등의 설계 변경 요청에 쉽고 빠르게 대처 가능하다

디자인 패턴 단점


잘못된 대상을 문제로 할 수 있다.

  • 보통 패턴의 필요성은 추상화 능력이 부족한 컴퓨터 언어나 기술을 사용할 때 일어난다.
  • 이럴 때 패턴은 언어에 반드시 필요한 조잡한 인터페이스가 된다.

비효율적인 솔루션

  • 디자인 패턴의 아이디어는 이미 승인된 모범 사례를 표준화하려는 시도이다.
  • 원칙적으로 이것은 유익하게 보일 수 있지만, 실제로는 종종 불필요한 코드 중복을 초래한다.
  • "거의 만족스럽지 못한" 설계 패턴보다는 잘 구성된 구현을 사용하는 것이 더 효율적인 솔루션일 수 있다.

잘못되고 무분별한 사용

  • 패턴에 대해 배운 후 더 간단한 코드가 잘 작동하는 상황에서도 패턴을 어디에나 적용하려고 하면 비효율적일 수 있다.

패턴의 종류


패턴의 분류 기준

  • 목적(purpose)
    • 패턴이 무엇을 하는지에 따라 분류
    • 생성 패턴 : 객체의 생성 과정에 관여하는 패턴
    • 구조 패턴 : 클래스나 객체의 합성에 관한 패턴
    • 행동 패턴 : 클래스나 객체들이 상호작용하는 방법과 책임을 분산하는 방법을 정의하는 패턴
  • 범위(scope)
    • 패턴을 적용하는 범위에 따라 분류
    • 클래스 패턴 : 클래스와 서브클래스 간의 관련성을 다루는 패턴
      • 관련성은 주로 상속이며, 컴파일 타임에 정적으로 결정된다.
    • 객체 패턴 : 객체 관려성을 다루는 패턴으로서, 런타임에 변경할 수 있으며 더 동적인 성격을 가진다.
      • 대부분의 패턴들은 어느정도 상속을 이용한다.
      • 23개의 패턴중 대부분 객체 패턴이다.

)

생성 패턴(Creational Patterns)

인스턴스를 만드는 절차를 추상화하는 패턴, 다양한 객체 생성 메커니즘을 제공하여 기존 코드의 유연성과 재사용성을 향상시킨다.

  • 싱글톤 패턴(Singleton)
  • 추상팩토리 패턴(Abstract Factory)
  • 빌더 패턴(Builder)
  • 팩토리 메서드 패턴(Factory Method)
  • 원형 패턴(Prototype)

구조 패턴(Structural Patterns)

객체 및 클래스를 보다 큰 구조로 조립하는 동시에 이러한 구조를 유연하고 효율적으로 유지하는 패턴

  • 적응자 패턴(Adapter or Wrapper)
  • 브리지 패턴(Bridge)
  • 데코레이터 패턴(Decorator)
  • 퍼사드 패턴(Facade)
  • 프록시 패턴(Proxy)

행위 패턴(Behavioral Patterns)

어떤 처리의 책임을 어느 객체에 할당하는 것이 좋은지, 알고리즘을 어느 객체에 정의하는 것이 좋은지 다루는 패턴

  • 옵저버 패턴(Observer)
  • 상태 패턴(State)
  • 전략 패턴(Strategy)
  • 템플릿 메서드 패턴(Template method)
  • 비지터 패턴(Visitor)
  • 역할 사슬 패턴(Chain of Responsibility)
  • 커맨드 패턴(Command)
  • 인터프리터 패턴(Interpreter)
  • 이터레이터 패턴(Iterator)
  • 미디에이터 패턴(Mediator)

참고 문헌 및 출처


  • GoF의 디자인 패턴(개정판) / 에릭 감마, 리처드 헬름, 랄프 존슨, 존 블라시디스 공저 / 김정아 역
  • https://refactoring.guru/
728x90

+ Recent posts