[Design Pattern] Iterator & Composite Pattern

Loopy·2022년 3월 21일
0

디자인패턴

목록 보기
8/9
post-thumbnail

☁️ 반복자 패턴이란?

객체 내부의 저장 방식(구현 방법)을 노출하지 않으면서, 클라이언트가 집합체 내부 모든 객체에 접근할 수 있게 해주는 패턴을 의미한다.

예를 들어 아래와 같은 경우는 객체 내부의 구현 방법이 노출되는 상황이다. 이렇게 되면 내부 구현 방법이 모두 달라 사용하는 컬렉션이 달라지고, 클라이언트는 내부 요소의 속성에 맞춰 반복하는 코드를 작성해줘야 한다.

List<String> list = dinerMenu.getList();
for (int i = 0; i < list.size(); i++) {
	System.out.println(list.get(i));
}

String[] list = launchMenu.getArray();
for (int i = 0; i < list.size(); i++) {
	System.out.println(list[i]);
}

🫧 내부 구현을 외부로 노출했을 때 단점

  1. 상세 구조가 클라이언트까지 외부로 노출된다. 따라서, 클라이언트가 구상 클래스에 직접적으로 연결되어 있다.
  1. 접근 순서는 사용된 컬렉션의 특성 및 구현에 연관되어 있다.

객체에 접근해서 가져오는 부분을 인터페이스를 통해 추상화 시켜 분리한다면?

바뀌는 부분을 Iterator 라는 인터페이스를 통해 캡슐화한다. Iterator 를 지원하는 모든 컬렉션에서 사용할 수 있는 메서드를 만들 수 있기 때문에, 다형적인 코드를 생성할 수 있어 유연하다.

interface Iterator<E> () {	
	public boolean hasNext();
    public E next();
    public void remove();
}

어떤 컬렉션이 오던지, 동일한 작업으로 내부 요소를 순회할 수 있어진다.

public void test() {
  Iterator<String> dinner = dinerMenu.createIterator();
  Iterator<String> launch = launchMenu.createIterator();
  iterate(dinner);
  iterate(launch);
}

private void iterate(Iterator iter) {
	while (iter.hasNext()) {
      String s = (String)iter.next();
      System.out.println(s);
  }
}

더불어 이러한 반복자는 자바의 컬렉션 클래스에서 같은 방법을 통해 각 요소에 대한 작업을 할 수 있도록 한다. 즉, 컬렉션 클래스를 교체해도 반복자 코드는 수정안하고 사용할 수 있어 단일 책임 원칙(SRP)이 분명하게 지켜진다.

  • iterator() : 반복자에 대한 참조(Iterator 자료형)를 반환
  • hasNext() : 요소가 더 존재하는지 확인. true 또는 false 반환
  • next() : 다음 요소 가져오기. Object를 반환하므로 변환해서 사용

반복자 패턴 장점

  1. 변하는 부분인 상세 내용이 캡슐화되어 있으므로, 클라이언트는 공통화된 인터페이스만 알면 된다.

  2. 내부 구조를 노출하지 않아도 컬렉션에 들어있는 모든 객체에 접근할 수 있다.(반복 작업을 캡슐화)

  3. 집합체에서 내부 컬렉션 기능과 반복자용 메소드 관련 기능을 분리하여 하나의 책임만 가질 수 있도록 한다. (하나로 묶여있다면 클래스 뿐만 아니라 반복자 관련 기능이 수정되어도 클래스까지 변경이 전파된다)

☁️ Iterator 예시 코드

public class IntDynamicArray implements Iterable {   
    final int INCREMENT_SIZE = 10;    //증가시키려는 사이즈(dynamic array)
    int count;                     
    int size;                   
    private int[] arr;

    public IntDynamicArray() {
        arr = new int[INCREMENT_SIZE];
        size = INCREMENT_SIZE;        
        count = 0;
    }

    public void add(int n) {
        if (count >= size) {      
            int[] arr2 = new int[size + INCREMENT_SIZE];
            for (int i = 0; i < size; i++) {
                arr2[i] = arr[i];
            }
            size += INCREMENT_SIZE;
            arr = arr2;
        }
        arr[count] = n;
        count++;
    }

    public int get(int idx) throws ArrayIndexOutOfBoundsException {
        if (idx < size) {
            return arr[idx];
        }
        else {
            throw new ArrayIndexOutOfBoundsException();
        }
    }

    public int size() {
        return count;
    }

    public Iterator iterator() {            //Iterator 반환
        return new IntDynamicArrayIterator(this);
    }
}


객체는 arr 을 클라이언트에게 반환하는 것이 아닌, iterator 를 생성해서 반환함으로써 내부가 어떻게 구현되어 있는지 모르도록 한다.

Iterator

public class IntDynamicArrayIterator implements Iterator {    //Iterator 구현
    IntDynamicArray arr;
    int count;

    public IntDynamicArrayIterator(IntDynamicArray arr) {
        this.arr = arr;
        count = 0;
    }

    @Override
    public boolean hasNext() {
        return count < arr.size();
    }

    @Override
    public Object next() {
        Integer n = arr.get(count);
        count++;
        return n;
    }
}
public class Main {
    public static void main(String[] args) {
        IntDynamicArray arr = new IntDynamicArray();

        for (int i = 0; i < 15; i++) {
            arr.add(i);
        }

        for (Iterator itr = arr.iterator(); itr.hasNext(); ) {
            System.out.println(itr.next());
        }

        for (Object n : arr) {  // for-each 구문 사용
            System.out.println((Integer) n);
        }
    }

}

Iterable과 for-each

이미 자바에서 iterator 들을 다 구현해놓았다.

  1. 어떤 클래스에서 iterable을 구현하면, 해당 클래스는 iterator() 메소드를 구현하고 컬렉션에 있는 항목을 대상으로을 반복하는 반복자를 리턴해야 한다.

  2. 향상된 for문이나 forEach 메서드를 제공한다.

향상된 for문 성능

https://sorjfkrh5078.tistory.com/98

☁️ Composite pattern

컴포지트 패턴은, 객체를 트리와 같은 계층 구조로 구성할 수 있도록 한다. 예를 들어 메뉴, 서브메뉴, 서브의 서브메뉴까지 모두 같은 객체로 인식되고 처리될 수 있다.

전체를 대상으로 작업을 수행할 수도 있고, 부분을 대상으로 작업을 수행할 수도 있다.

단일 객체인 Leaf, 복합 객체인 Composite 가 있을 때 둘다 하나의 Componet 를 구현함으로써 동일한 객체로 간주되어 똑같은 방식으로 처리될 수 있도록 한다.

public Class Leaf extends Component {
	...
}
public Class Composite extends Component {
	List<Component> components = new ArrayList<>();   // 자식 요소 저장
 
 	public void add(Component component) {
    	components.add(leaf);
    }
    
    public void remove(Component component) {
    	components.remove(leaf);
    }
    
    public Leaf getChild(int i) {
    	components.get(i)
    }
    
   	public void print() {
    	for (Component componet: components) { // 재귀
        	component.print();
        }
    }
}

같은 방식으로 처리될 수 있도록 했을 때 장점은?

루트 메서드 하나만 호출하면, 재귀 형태로 전체 구조를 반복하는 작업을 수행할 수 있으므로 결과적으로 클라이언트 처리 방식이 굉장이 단순화된다.

public void Client() {
	Componet component;
    
    public Client(Component component) {
    	this.component = component;
    }
    
    public void print() {
    	component.print(); // 트리 전체의 계층 구조를 출력하고 싶으면 최상위 노드의 print 만 호출해도 재귀적으로 수행됌
    }
}

Component composite1 = new Composite();
Component composite2 = new Composite();
Component component = new Composite();  // 복합 객체가 될..

composite1.add(new Leaf());  // 복합 객체에 개별 객체 추가
composite2.add(new Leaf());

component.add(composite1);           // 루트 객체에 복합 객체 추가
component.add(composite2);


Client client = new Client(component);
client.print();   // 메서드 한번에 모든 객체 정보가 출력됌

물론 계층 구조 관리 역할과 Composite 본연의 역할 두 가지가 있어 단일 책임 원칙에 위배된다는 시각도 있지만, 대신 투명성의 장점을 확보할 수 있다.

투명성이란, 클라이언트가 어떤 원소가 복합이고 어떤 원소가 개별 객체인지 정확하게 보이게 함으로써 복합 객체와 잎을 똑같은 방식으로 처리할 수 있어지는 것을 의미한다.

디자인 패턴은 정답이 없고, 상황에 따라 원칙을 적절하게 사용할 수 있다.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글