반복자 패턴(Iterator Pattern)

  • 반복자 패턴은 내부 구조를 외부에 노출하지 않고, 집합체(Aggregate)를 통해 원소 객체에 순차적으로 접근할 수 있습니다.
  • C++의 vector에도 사용되는 유명한 패턴입니다.
std::vector<int> nums;
nums.push_back(1);
nums.push_back(2);
nums.push_back(3);
nums.push_back(4);
nums.push_back(5);

// 반복자 패턴
std::vector<int>::iterator iter = nums.begin();
for (; iter != nums.end(); iter++)
{
    std::cout << *iter << std::endl;
}
  • 여기서 vector<T>::iterator가 반복자 패턴입니다.

반복자 패턴 구조

  • 반복자 패턴의 해심은 효율적인 집합 관리를 위해 별도의 집합체를 갖고 있습니다.
  • 따라서 반복자 패턴은 다음과 같은 구조로 구성됩니다.
    • 집합체(Aggregate)
    • 반복자(Iterator)
  • 과일 이름을 저장하고 이를 반복자 패턴으로 순차적으로 하나씩 출력하는 예제를 작성해보겠습니다.

반복자 패턴: 과일 이름 저장소

  • 과일 이름을 저장하기 위해 우선 과일 클래스를 작성합니다.
class Fruit
{
public:
    Fruit(std::string name)
    {
        m_name = name;
    }

    std::string getName()
    {
        return m_name;
    }

private:
    std::string m_name;
};

반복자 패턴: 집합체 인터페이스 클래스

  • 집합체 인터페이스 클래스는 구성이 간단합니다.
  • 반복자를 생성하는 가상 메서드를 작성합니다.
class Aggregate
{
    virtual std::shared_ptr<Iterator> createIterator() = 0;
};

반복자 패턴: 집합체 인터페이스 구현

  • 과일의 이름을 받고, 내부 컨테이너에 저장하는 클래스를 작성합니다.
  • 간단한 예제로 vector를 사용했지만, 실제로는 내부 리스트(List) 또는 스택(Stack)과 같은 컨테이너를 직접 구현합니다.
  • 자료구조를 공부하는 시간이 아니므로 간단하게 구현해보겠습니다.
  • 집합체 클래스에서 반복자 객체를 생성하는 것이 핵심입니다.
class concreteAggregate :
    public Aggregate
{
public:
    void addFruit(std::string name)
    {
        std::shared_ptr<Fruit> fruit = std::make_shared<Fruit>(name);
        m_list.push_back(fruit);
    }

    std::shared_ptr<Iterator> createIterator() override
    {
        std::shared_ptr<Iterator> iterator = std::make_shared<IteratorObject>(m_list);
        return iterator;
    }

private:
    std::vector<std::shared_ptr<Fruit>> m_list;
};

반복자 패턴: 반복자 인터페이스 클래스

  • 반복자 패턴이 객체를 순환 처리하기 위해 필요한 메서드를 작성합니다.
  • C++ STL에서 지원하는 컨테이너는 다양한 반복자 패턴의 순환 메서드가 존재합니다.
  • 여기서는 우선 필요한 것만 작성하겠습니다.
class Iterator
{
public:
    virtual bool isDone() = 0;
    virtual std::shared_ptr<Fruit> next() = 0;
};

반복자 패턴: 반복자 인터페이스 구현

  • 집합체 객체가 반복자 객체를 생성하는데, 집합체 객체가 갖고 있는 내부 데이터를 반복자 객체도 동일하게 가져야 합니다.
  • 집합체 메서드에서 생성자에 데이터를 전달하므로, 생성자를 통해 내부 컨테이너에 저장하는 방법을 채택하였습니다.
  • 내부 인덱스를 저장하는 변수가 있어, 컨테이너의 크기와 비교하여 종료 조건을 확인합니다.
    • 만약 vector가 아닌 리스트나 스택의 경우 nullptr 검사 또는 스택의 지정된 크기를 활용할 수 있습니다.
class IteratorObject :
    public Iterator
{
public:
    IteratorObject(std::vector<std::shared_ptr<Fruit>> fruits)
    {
        m_index = 0;
        for (const auto& fruit : fruits)
        {
            m_fruits.push_back(fruit);
        }
    }

    bool isDone() override
    {
        if (m_index < m_fruits.size())
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    std::shared_ptr<Fruit> next() override
    {
        std::shared_ptr<Fruit> fruit = m_fruits[m_index];
        m_index++;

        return fruit;
    }

private:
    std::vector<std::shared_ptr<Fruit>> m_fruits;
    int m_index;
};

반복자 패턴: 실행 코드

int main(const int argc, const char* argv[])
{
    std::shared_ptr<concreteAggregate> menu = std::make_shared<concreteAggregate>();
    menu->addFruit("Apple");
    menu->addFruit("Orange");
    menu->addFruit("Melon");
    menu->addFruit("Banana");

    std::shared_ptr<Iterator> iter = menu->createIterator();
    while (iter->isDone())
    {
        auto item = iter->next();
        std::cout << item->getName() << std::endl;
    }

    return 0;
}
  • 실행 결과
Apple
Orange
Melon
Banana
  • 반복자 패턴은 C++ STL 컨테이너에 완벽하게 적용되어 있습니다.
  • 컨테이너를 새로 만들 경우 이를 참고하여 반복자를 만들 수 있습니다.
  • 반복자 패턴은 순환 알고리즘이 실제 구현된 객체에 의존하지 않습니다.
  • 또한 독립적인 동작을 유지하기 위해 객체의 내부 메서드를 직접 호출하지 않습니다.
    • 반복자 패턴의 isDone()next()를 사용하여 객체 내부의 데이터를 순환합니다.
  • 예제에서 사용한 Fruit 클래스 대신 템플릿으로 구현한다면, 어떤 객체든 상관 없이 반복자 패턴을 적용할 수 있습니다.