본문 바로가기

JAVA/Design Patterns

9.컴포지트 패턴

반응형

컴포지트 패턴

=> 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있다.

- 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합객체(composite)를 똑같은 방법으로 다룰 수 있다.


객체의 구성과 개별 객체를 노드로 가지는 트리 형태로 객체를 구축할수 있다.

이런 복잡구조(composite structure)를 사용하면 복합객체와 개별객체에 대해 똑같은 작업을 적용할 수 있다.

즉, 대부분의 경우 복합객체와 개별객체를 구분할 필요가 없어진다.


중첩되어있는 메뉴 그룹과 메뉴 항목을 똑같은 구조내에서 처리할 수 있다.

메뉴와 메뉴 항목을 같은 구조에 집어넣어서 부분-전체 계층구조를 생성할수 있다.

부분-전체 계층구조란 부분(메뉴 및 메뉴항목)들이 모여있지만, 모든 것을 하나로 묶어서 전체로 다룰 수 있는 구조를 뜻한다.


이런방식으로 만들고 나면 컴포지트 패턴을 써서 개별객체와 복합 객체들을 똑같은 식으로 다룰 수 있다.

메뉴, 서브메뉴, 서브서브메뉴 등과 함께 메뉴 항목으로 구성된 트리 구조가 있다고 하면 각각 이 모두 복합 객체가 될수 있다는 것이다. 각 메뉴안에 다른 메뉴 및 메뉴항목이 또 들어갈 수 있게 된다.

개별객체도 결국 메뉴라고 할수 있다.  다른 객체가 들어있지 않을 뿐이다.

컴포지트 패턴을 따르는 디자인을 사용하면 간단한 코드만 가지고도 똑같은 작업을 전체 메뉴 구조에 대해서 반복해서 적용할 수 있다.



구성요소 인터페이스로 메뉴와 메뉴 항목 모두 적용되는 공통 인터페이스 역할.

이 인터페이스가 있어야만 그 둘을 똑같은 방법으로 처리할 수 있다.

메뉴와 메뉴항목에 대해서 같은 메서드를 호출할 수 있게 된다.



package patterns.component;


public abstract class MenuComponent {


    public void add(MenuComponent menuComponent) {

        throw new UnsupportedOperationException();

    }


    public void remove(MenuComponent menuComponent) {

        throw new UnsupportedOperationException();

    }


    public MenuComponent getChild(int i) {

        throw new UnsupportedOperationException();

    }

    //MenuComponent를 추가하고 제거하고 가져오기 위한 메서드


    public String getName() {

        throw new UnsupportedOperationException();

    }


    public String getDescription() {

        throw new UnsupportedOperationException();

    }


    public double getPrice() {

        throw new UnsupportedOperationException();

    }


    public boolean isVegetarina() {

        throw new UnsupportedOperationException();

    }


    public void print() {

        throw new UnsupportedOperationException();

    }


    //MenuItem에서 작업을 처리하기 위해 사용하는 메서드


}



package patterns.component;


public class MenuItem extends MenuComponent {

    String name;

    String description;

    boolean vegetarian;

    double price;


    public MenuItem(String name, String description, boolean vegetarian, double price) {

        this.name = name;

        this.description = description;

        this.vegetarian = vegetarian;

        this.price = price;

    }


    public String getName() {

        return name;

    }


    public String getDescription() {

        return description;

    }


    public boolean isVegetarian() {

        return vegetarian;

    }


    public double getPrice() {

        return price;

    }


    //추가됨

    //MenuComponent클래스에 있는 print() 메서드를 오버라이드

    @Override

    public void print() {

        System.out.print(" " + getName());

        if(isVegetarian()) {

            System.out.print("(v)");

        }

        System.out.println(", " + getPrice());

        System.out.println("   --" + getDescription());

    }

}



package patterns.component;


import java.util.ArrayList;

import java.util.Iterator;


public class Menu extends MenuComponent {

    ArrayList menuComponents = new ArrayList();

    String name;

    String description;


    public Menu(String name, String description) {

        this.name = name;

        this.description = description;

    }


    @Override

    public void add(MenuComponent menuComponent) {

        menuComponents.add(menuComponent);

    }

    @Override

    public void remove(MenuComponent menuComponent) {

        menuComponents.remove(menuComponent);

    }

    @Override

    public MenuComponent getChild(int i) {

        return (MenuComponent) menuComponents.get(i);

    }


    @Override

    public String getName() {

        return name;

    }

    @Override

    public String getDescription() {

        return description;

    }

    @Override

    public void print() {

        System.out.print("\n" + getName());

        System.out.println(", " + getDescription());

        System.out.println("---------------------");


        //반복자를 사용

        //반복작업을 수행하는 중에 다른 메뉴가 나타나면 그 메뉴에서 또 다른 반복작업을 실행하게 된다.

        //서브메뉴가 여러 단계를 중첩되어 있으면 그런 과정이 여러번 반복된다.

        Iterator iterator = menuComponents.iterator();

        while (iterator.hasNext()) {

            MenuComponent menuComponent = (MenuComponent) iterator.next();

            menuComponent.print();

        }

    }

}



package patterns.component;


public class Waitress {

    MenuComponent menuComponent;


    public Waitress(MenuComponent menuComponent) {

        this.menuComponent = menuComponent;

    }


    public void printMenu() {

        menuComponent.print();

    }

}



package patterns.component;


import patterns.iterator.PancakeHouseMenu;


public class MenuTestDriven {

    public static void main(String[] args) {

        MenuComponent pancakeHouseMenu = new Menu("팬케이크 하우스 메뉴", "아침메뉴");

        MenuComponent dinerMenu = new Menu("식당메뉴", "점심메뉴");

        MenuComponent cafeMenu = new Menu("카페메뉴", "저녁메뉴");

        MenuComponent dessertMenu = new Menu("디저트 메뉴", "디저트를 즐겨보세요");


        MenuComponent allMenus = new Menu("전체메뉴", "전체메뉴");

        allMenus.add(pancakeHouseMenu);

        allMenus.add(dinerMenu);

        allMenus.add(cafeMenu);


        dinerMenu.add(new MenuItem(

                "파스타",

                "마리나라 소스 스파게티, 효모빵",

                true,

                3.89));

        dinerMenu.add(dessertMenu);


        dessertMenu.add(new MenuItem(

                "애플파이",

                "크러스트에 바닐라 아이스크림 애플파이",

                true,

                1.59));


        Waitress waitress = new Waitress(allMenus);

        waitress.printMenu();

    }

}



컴포지트 패턴에서는 단일 역할 원칙을 깨면서 대신에 투명성을 확보하기 위한 패턴이라고 할수 있다.


투명성(transparency)이란

Component 인터페이스에 자식들을 관리하기 위한 기능과 앞으로써의 기능을 전부 집어넣음으로써 클라이언트에서 복합객체와 잎 노드를 똑같은 방식으로 처리할 수 있도록 할 수 있다.

어떤 원소가 복잡객체인지 잎노드인지가 클라이언트 입장에서는 투명하게 느껴지게 된다.


Component 클래스에는 두 종류의 기능이 모두 들어있다 보니깐 안정성은 약간 떨어지게 된다.

클라이언트에서 어떤 원소에 대해 무의미한 또는 부적절한 작업(메뉴 항목에 메뉴를 추가)을 처리하려고 할 수 있을 수있기 때문이다.

이런 문제는 디자인상의 결정 사항에 속한다.

다른 방향으로 디자인해서 여러 역할을 서로 다른 인터페이스로 분리시킬수도 있다.

이렇게 하면 어떤 원소에 대해 부적절한 메서드를 호출하는 일이 일어나지 않을 테고, 컴파일 중에, 또는 실행 중에 문제가 생기는 일도 없을 것이다.

하지만 대신에 투명성이 떨어지게 되고, 코드에서 조건문이라든가 instanceof 연산자 같은 걸 써야만 할 것이다.


상황에 따라 원칙을 적절하게 사용해야 한다는 것.

디자인 원칙에서 제시하는 가이드라인을 따르는 것이 좋긴 하지만, 항상 그 원칙이 우리가 생각하고 있는 디자인에 어떤 영향을 끼칠지를 생각해봐야 한다.

일부러 원칙에 위배되는 방식으로 디자인을 하는 경우도 있다.

물론 상황에 따라서 원칙을 바라보는 과넞ㅁ에 따라 해석 방법이 크게 달라질수도 있다.

잎 노드에 자식을 관리하기 위한 기능 add(), remove(), getChild() 집어넣는 것이 올바르지 못한 디자인이라고 생각할수 있긴 하지만, 조금만 시각을 바꿔보면 잎 노드를 자식이 0개인 노드라고 생각할 수도 있기 때문이다.



컴포지트 패턴내에서 이터레이터 패턴을 활용


package patterns.composite;


import java.util.Iterator;


public abstract class MenuComponent {


    public void add(MenuComponent menuComponent) {

        throw new UnsupportedOperationException();

    }


    public void remove(MenuComponent menuComponent) {

        throw new UnsupportedOperationException();

    }


    public MenuComponent getChild(int i) {

        throw new UnsupportedOperationException();

    }

    //MenuComponent를 추가하고 제거하고 가져오기 위한 메서드


    public String getName() {

        throw new UnsupportedOperationException();

    }


    public String getDescription() {

        throw new UnsupportedOperationException();

    }


    public double getPrice() {

        throw new UnsupportedOperationException();

    }


    public boolean isVegetarian() {

        throw new UnsupportedOperationException();

    }


    public void print() {

        throw new UnsupportedOperationException();

    }


    //MenuItem에서 작업을 처리하기 위해 사용하는 메서드


    public abstract Iterator createIterator ();

}




package patterns.composite;


import java.util.ArrayList;

import java.util.Iterator;


public class Menu extends MenuComponent {

    ArrayList menuComponents = new ArrayList();

    String name;

    String description;


    public Menu(String name, String description) {

        this.name = name;

        this.description = description;

    }


    @Override

    public void add(MenuComponent menuComponent) {

        menuComponents.add(menuComponent);

    }

    @Override

    public void remove(MenuComponent menuComponent) {

        menuComponents.remove(menuComponent);

    }

    @Override

    public MenuComponent getChild(int i) {

        return (MenuComponent) menuComponents.get(i);

    }


    @Override

    public String getName() {

        return name;

    }

    @Override

    public String getDescription() {

        return description;

    }

    @Override

    public void print() {

        System.out.print("\n" + getName());

        System.out.println(", " + getDescription());

        System.out.println("---------------------");


        //반복자를 사용

        //반복작업을 수행하는 중에 다른 메뉴가 나타나면 그 메뉴에서 또 다른 반복작업을 실행하게 된다.

        //서브메뉴가 여러 단계를 중첩되어 있으면 그런 과정이 여러번 반복된다.

        Iterator iterator = menuComponents.iterator();

        while (iterator.hasNext()) {

            MenuComponent menuComponent = (MenuComponent) iterator.next();

            menuComponent.print();

        }

    }


    @Override

    public Iterator createIterator() {

        return new CompositeIterator(menuComponents.iterator());

    }

}



package patterns.composite;


import java.util.Iterator;


public class MenuItem extends MenuComponent {

    String name;

    String description;

    boolean vegetarian;

    double price;


    public MenuItem(String name, String description, boolean vegetarian, double price) {

        this.name = name;

        this.description = description;

        this.vegetarian = vegetarian;

        this.price = price;

    }


    public String getName() {

        return name;

    }


    public String getDescription() {

        return description;

    }


    @Override

    public boolean isVegetarian() {

        return vegetarian;

    }


    public double getPrice() {

        return price;

    }


    //추가됨

    //MenuComponent클래스에 있는 print() 메서드를 오버라이드

    @Override

    public void print() {

        System.out.print(" " + getName());

        if(isVegetarian()) {

            System.out.print("(v)");

        }

        System.out.println(", " + getPrice());

        System.out.println("   --" + getDescription());

    }


    @Override

    public Iterator createIterator() {

        return new NullIterator();

    }

}


package patterns.composite;


import java.util.Iterator;

import java.util.Stack;


public class CompositeIterator implements Iterator {

    Stack stack = new Stack();


    public CompositeIterator(Iterator iterator) {

        stack.push(iterator);

    }


    @Override

    public boolean hasNext() {

        if (stack.empty()) {

            return false;

        } else {

            Iterator iterator = (Iterator) stack.peek();

            if (!iterator.hasNext()) {

                stack.pop();

                return hasNext();

            } else {

                return true;

            }

        }

    }


    @Override

    public Object next() {

        if (hasNext()) {

            Iterator iterator = (Iterator) stack.peek();

            MenuComponent component = (MenuComponent) iterator.next();

            if (component instanceof Menu) {

                stack.push(component.createIterator());

            }

            return component;

        } else {

            return null;

        }

    }


    @Override

    public void remove() {

        throw new UnsupportedOperationException();

    }

}



package patterns.composite;


import java.util.Iterator;


public class NullIterator implements Iterator {


    @Override

    public boolean hasNext() {

        return false;

    }


    @Override

    public Object next() {

        return null;

    }


    @Override

    public void remove() {

        throw new UnsupportedOperationException();

    }    

}



package patterns.composite;


import java.util.Iterator;


public class Waitress {

    MenuComponent allMenus;


    public Waitress(MenuComponent allMenus) {

        this.allMenus = allMenus;

    }


    public void printMenu() {

        allMenus.print();

    }



    public void printVegetarianMenu() {

        Iterator iterator = allMenus.createIterator();

        System.out.println("\n Vegetarian menu\n-----");

        while (iterator.hasNext()) {

            MenuComponent menuComponent = (MenuComponent) iterator.next();

            try {

                if (menuComponent.isVegetarian()) {

                    menuComponent.print();

                }

            } catch (UnsupportedOperationException e) {


            }

        }

    }

}


}



try/catch는 프로그램 로직을 처리하기 위해 만들어진게 아니라 오류를 처리하기 위해 만들어진것이다.

이런 방법말고 다른 방법으로는 instanceof를 써서 실행중에 메뉴 구성요소의 형식을 확인하고 나서 isVegetarian()을 호출할 수 도 있을 것이다. 하지만 그렇게 하면 Menu 와 MenuItem을 똑같이 다루지 않게 되는 셈이므로 투명성을 잃어버리게 된다.


Menu의 isVegetarian()메서드에서 무조건 false를 리턴하도록 하는 방법도 있을 것이다.

이렇게 하면 코드도 간단해지고 투명성도 계속 유지할수 있다.


출처 - Head First Design Patterns 저자- 에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 버트 베이츠


반응형

'JAVA > Design Patterns' 카테고리의 다른 글

11. 프록시 패턴  (0) 2019.01.02
10.스테이트 패턴  (0) 2018.12.17
9.이터레이터 패턴  (0) 2018.12.13
8.템플릿 메서드 패턴  (0) 2018.12.10
7.어댑터 패턴 / 퍼사드 패턴  (0) 2018.12.10