본문 바로가기

디자인 패턴/디자인 패턴

[디자인패턴] 컴포지트 패턴

책을 공부하며 새로운 문제를 만들고 구현한것을 기록한 게시글입니다.

(이해한 내용을 바탕으로 작성했기 때문에, 내용이 정확하지않고 틀린부분이 있을수있으며, 예시가 패턴을 사용하기에 알맞지 않을수도있습니다)


컴포지트 패턴

- 클라이언트에서 객체컬렉션과 객체를 똑같이 처리한다.

쉽게말해, 객체를 담고있는 객체컬렉션과 객체컬렉션에 담겨있는 객체에대해서 똑같은 처리방법으로 접근할수있다는것이다.

 

트리 자료구조를 생각하면 이해가 쉬운데, 

 

노드 D의 자식노드 E,F,G가 있다고하자. 이때, E, F, G는 D에 담겨있는 객체이고, D는 E,F,G를 담고있는 객체 컬렉션이다. 하지만, D는 항상 객체컬렉션이 아닐수도있다. D또한 어떠한 노드(A,B,C)의 자식중 하나일수있기때문이다. 즉, 각 노드를 객체이자 객체컬렉션으로 생각하고 성격에 맞춰 구현해준다.

 

따라서, 각 객체의 자료구조에 들어있는 변수는 컴포지트 패턴을 구현하는 클래스가 되어야한다. (구현에 따라 달라질수도있다)


문제

애벌레는 나뭇잎을 먹으러 Java나무를 기어 올라가려고한다.

Java나무에는 무수히 많은 나뭇가지들이있고, 각 나무가지에는 나뭇잎이 자라고있다.(자라지않을수도있다) 

 

애벌레는 나무의 꼭대기(더 이상 갈길이없다면)에 도착하면, 나비가 되어 날아간다.

나뭇가지와 나뭇잎끼리는 절대 합쳐지는일이 없을때(쉽게말하면, 나무의 모양은 트리구조이다.) 지렁이가 먹을수있는 나뭇잎의 최대갯수를 구하자.

 

루트 노드는 항상 0이며, 노드들은 모두 서로 연결되어있다.

 

(현재는 ArrayList밖에없지만 계절에따라 Array에 나뭇가지와 나뭇잎이 자랄수도있다. 이 상황에 대비해 디자인하도록하자.)

 

입력

부모노드, 자식노드(나뭇잎이라면, 나뭇잎의 양으로 해석한다), 자식노드의 상태(나뭇잎일경우에는 1 아니라면 0)가 주어진다. 단, 노드들의 이름은 중복되지않는다.

입력의 끝에는 "-"이 주어진다.

ex)

1 2 1

0 1 1

-

 

출력

달팽이가 가장 많이먹은 나뭇잎의 수를 출력한다.


구현

컴포지트 패턴을 이용해서 하나의 인터페이스로 반복할수있도록 디자인 해보자.

 

우선, 인터페이스를 만들어보자. 인터페이스가 하는일은 다음과 같이 생각할수있을것이다.

1. 자식요소에 나뭇잎 혹은 나뭇가지 추가하기

2. 애벌레가 현재위치에서 나뭇잎을 먹을수있는지 알려주기(현재위치가 나뭇잎인지 확인하면된다.)

3. 자신의 다음 노드로 이동하기

package prob9_JavaTree;

public interface Tree {
    void addNode(Tree tree);
    int eatLeaf();
    int getLeaf();
    Tree getNode(int nodeName);
}

간단하다.

이제 인터페이스를 구현하는 나뭇잎과 나뭇가지들을 만들자. 
(구현상 편의를 위해 ArrayList만 있다고하자. 하지만 다른 자료구조가 추가되더라도 문제없이 동작하도록)

 

package prob9_JavaTree.Nodes;

import java.util.ArrayList;
import prob9_JavaTree.*;

public class BranchWithArrayList implements Tree{
    private int nodeName;
    private ArrayList<Tree> nodes = new ArrayList<Tree>(); 
    // 여기에 나뭇잎과 나뭇가지가 저장될것이다.
    // 나뭇잎, 나뭇가지는모두 Tree인터페이스를 구현할것이다.
    
    public BranchWithArrayList(int nodeName){
        this.nodeName = nodeName;
    }
    
    public Tree getNode(int nodeName){ // 모든자식들을 반복하며 원하는 노드를 갖고있는 Leaf를 찾는다.
        if(nodeName == this.nodeName) return this;
        Tree ret = null;
        for(int idx = 0; idx < nodes.size(); idx++) {
            Tree temp = nodes.get(idx).getNode(nodeName);
            if(temp == null) continue;
            ret = temp;
        }
        return ret;
    }
    
    public void addNode(Tree tree){
        this.nodes.add(tree);
    }
    
    public int eatLeaf(){
        return 0; // 나뭇가지라서 나뭇잎을 먹을수없음
    }
    
    public int getLeaf(){
        int ret = 0;
        for(int idx = 0; idx < nodes.size(); idx++){
            ret = Math.max(nodes.get(idx).getLeaf(), ret);
        }
        return ret;
    }
}

 

위 클래스는 나뭇가지이므로, 나뭇잎이 없다. 즉 eatLeaf메소드가 호출되었을때 동작하지않아야한다. 코드에서는 이를 0을 반환하는것으로 구현했다. (나뭇잎을 먹지않는것은 0을 더하는것과 마찬가지이므로)

 

getLeaf()메소드를 호출하면 하위노드들을 모두 탐색하며 먹을수있는 나뭇잎의 최대갯수를 구한다.

원하는 노드에 addNode를 할수있도록 getNode()메소드를 호출하면, 원하는 이름의 노드가 나오도록 만들었다.

 

이제 나뭇잎 클래스를 만들어보자.

package prob9_JavaTree.Nodes;

import prob9_JavaTree.*;

public class Leaf implements Tree{
    
    private int numberOfLeaf;
    
    public void addNode(Tree tree){}
    
    public Leaf(int numberOfLeaf){
        this.numberOfLeaf = numberOfLeaf;
    }
    
    public int eatLeaf(){
        return numberOfLeaf;
    }
    
    public int getLeaf(){
        return eatLeaf();
    }
    
    public Tree getNode(int nodeName){
        return null;
    }
    
}

나뭇잎에서는 나뭇가지가 자랄수없다. 따라서, addNode와 getNode를 아무동작도 하지않는 메소드로 만들어준다.

 

eatLeaf메소드를 호출하면 나뭇잎이기 때문에 나뭇잎의 양(numberOfLeaf)를 반환한다.

getLeaf메소드를 호출하면 eatLeaf를 호출한다.

 

이제 호출을 입력및 프로그램의 일을 관리하는 Root를 만들자.

package prob9_JavaTree;

import java.io.*;
import prob9_JavaTree.Nodes.*;

public class Root {
    
    private BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    private Tree rootOfTree = new BranchWithArrayList(0);
    
    public void run(){ // 실행을 담당하는 메소드이다. run메소드를 호출하면 답이나온다.
        input();
        System.out.println(rootOfTree.getLeaf());
    }
    
    private void input(){ // 입력을 담당하는 메소드이다.
        try{
            int input[] = new int[5];
            String[] temp = br.readLine().split(" ");
            while(!temp[0].equals("-")){
                for(int i = 0; i < 3; i++) input[i] = Integer.parseInt(temp[i]);
                Tree tree = smallFactory(input[2], input[1]); // 입력값에 따라 나뭇가지노드 혹은 나뭇잎노드를 생성한다.
                Tree par = rootOfTree.getNode(input[0]); // 나뭇잎 노드가 저장될 노드를 찾는다. Root노드의 이름은 항상 0이다.
                if(par == null) continue; // 찾은 노드가 나뭇잎이라면 저장하지않고 위로올린다.
                par.addNode(tree);
                temp = br.readLine().split(" ");
            }
        } catch (IOException ioe){}
    }
    
    private Tree smallFactory(int state, int nodeName){ // 생성을 담당하는 메소드이다.
        Tree ret = null;
        if(state == 1) ret = new Leaf(nodeName); // 나뭇잎을 만든다.
        else if(state == 2) ret = new BranchWithArrayList(nodeName); // 나뭇가지를 만든다. 이때 이름을 입력받은 nodeName으로 한다.
        return ret;
    }
    
}

Tree tree = smallFactory(input[2], input[1]) 은 입력에 따라 나뭇가지 노드 혹은 나뭇잎노드를 만든다.

Tree par = rootOfTree.getNode(input[0])을 호출하면 현재 노드가 저장될 부모노드의 위치를 찾는다.

 

main쓰레드를 만들어서 실행해보자.

package prob9_JavaTree;

public class Main {
    
    public static void main(String[] args){
        Root root = new Root();
        root.run();
    }
    
}

 


소스코드

https://github.com/devxb/DesignPatterns/tree/main/DesignPatterns/prob9_JavaTree

 

devxb/DesignPatterns

디자인 패턴 🤔. Contribute to devxb/DesignPatterns development by creating an account on GitHub.

github.com