Hướng dẫn Java Design Pattern – Iterator

Trong khi phát triển các ứng dụng, chúng ta làm việc với nhiều loại tập hợp như: cấu trúc cây, mảng, tập hợp, bảng băm, ngăn xếp, hàng đợi, … Cách thức mà tập hợp này lưu trữ đối tượng của nó rất khác nhau, và nếu bạn muốn truy cập dữ liệu của những đối tượng này, bạn phải học những kỹ thuật khác nhau cho từng loại tập hợp. Khi đó, mẫu Iterator là một giải pháp tốt. Chúng ta có thể sử dụng một interface được xác định phương thức cụ thể để truy cập tới từng phần tử của tập hợp. Sử dụng những phương thức này, chúng ta có thể truy xuất tới các phần tử trong tập hợp theo cùng cách dễ dàng nhất.

Trong phần tiếp theo của bài viết này chúng ta sẽ cùng tìm hiểu chi tiết về Iterator Pattern, cách cài đặt, lợi ích mà nó mang lại và khi nào chúng ta có thể áp dụng nó.

1. Iterator Pattern là gì?

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Iterator Pattern là một trong những Pattern thuộc nhóm hành vi (Behavior Pattern). Nó được sử dụng để “Cung cấp một cách thức truy cập tuần tự tới các phần tử của một đối tượng tổng hợp, mà không cần phải tạo dựng riêng các phương pháp truy cập cho đối tượng tổng hợp này”.

Nói cách khác, một Iterator được thiết kế cho phép xử lý nhiều loại tập hợp khác nhau bằng cách truy cập những phần tử của tập hợp với cùng một phương pháp, cùng một cách thức định sẵn, mà không cần phải hiểu rõ về những chi tiết bên trong của những tập hợp này.

Iterator thường được viết trong Java như là những lớp độc lập. Ý tưởng thiết kế này là một trong những kỹ thuật được gọi là “đơn trách nhiệm – Single responsibility principle (SRP)” – một lớp chỉ có duy nhất một công việc để làm. Hãy suy nghĩ rằng tập hợp duy trì các phần tử, một iterator cung cấp cách thức làm việc với các phần tử đó. Đó cũng là lý do tại sao những Iterator có thể làm việc được trong các tập hợp khác nhau.

Tách biệt trách nhiệm giữa các lớp rất hữu dụng khi một lớp bị thay đổi. Nếu có quá nhiều thứ bên trong một lớp đơn lẻ, sẽ rất khó khăn để viết lại mã nguồn. Khi diễn ra sự thay đổi, một lớp “đơn trách nhiệm” sẽ chỉ có một lý do duy nhất để thay đổi.

Chúng ta có thể thấy Interator Pattern được áp dụng trong java với Interface iterator trong gói java.util.Iterator. Interface này định nghĩa các phương thức sau:

  • Hàm next() : trả về phần tử kế tiếp trong tập hợp
  • Hàm hasNext() : trả về giá trị True nếu vẫn còn phần tử trong tập hợp và trả về false trong trường hợp ngược lại.

Đó là cách Iterator làm việc. Nó cung cấp một giao diện đơn giản, nhất quán để làm việc với các tập hợp khác nhau.

Giả sử rằng Client phải làm việc với một tập hợp phức tạp và rắc rối ( như hình sau) và không biết cách thức làm việc với nó như thế nào.

Client có thể sử dụng iterator để làm cầu nối với tập hợp, và client có thể sử dụng các phương thức cơ bản của Iterator để giao tiếp với tập hợp. Như hình sau:

2. Cài đặt Iterator Pattern như thế nào?

Các thành phần tham gia mẫu Iterator:

  • Aggregate : là một interface định nghĩa định nghĩa các phương thức để tạo Iterator object.
  • ConcreteAggregate : cài đặt các phương thức của Aggregate, nó cài đặt interface tạo Iterator để trả về một thể hiện của ConcreteIterator thích hợp.
  • Iterator : là một interface hay abstract class, định nghĩa các phương thức để truy cập và duyệt qua các phần tử.
  • ConcreteIterator : cài đặt các phương thức của Iterator, giữ index khi duyệt qua các phần tử.
  • Client : đối tượng sử dụng Iterator Pattern, nó yêu cầu một iterator từ một đối tượng collection để duyệt qua các phần tử mà nó giữ. Các phương thức của iterator được sử dụng để truy xuất các phần tử từ collection theo một trình tự thích hợp.

Ví dụ: tạo Iterator để duyệt qua các item trong một menu.

Item.java

package com.maixuanviet.patterns.behavioral.iterator;
 
public class Item {
    private String title;
    private String url;
 
    public Item(String title, String url) {
        super();
        this.title = title;
        this.url = url;
    }
 
    @Override
    public String toString() {
        return "Item [title=" + title + ", url=" + url + "]";
    }
}

ItemIterator.java

package com.maixuanviet.patterns.behavioral.iterator;
 
public interface ItemIterator<T> {
     
    boolean hasNext();
     
    T next();
}

Menu.java

package com.maixuanviet.patterns.behavioral.iterator;
 
import java.util.ArrayList;
import java.util.List;
 
public class Menu {
    private List<Item> menuItems = new ArrayList<>();
 
    public void addItem(Item item) {
        menuItems.add(item);
    }
 
    public ItemIterator<Item> iterator() {
        return new MenuItemIterator();
    }
 
    class MenuItemIterator implements ItemIterator<Item> {
        private int currentIndex = 0;
 
        @Override
        public boolean hasNext() {
            return currentIndex < menuItems.size();
        }
 
        @Override
        public Item next() {
            return menuItems.get(currentIndex++);
        }
    }
}
&#91;/code&#93;
<!-- /wp:shortcode -->

<!-- wp:paragraph -->
<p><strong>Client.java</strong></p>
<!-- /wp:paragraph -->

<!-- wp:shortcode -->

package com.maixuanviet.patterns.behavioral.iterator;
 
public class Client {
    public static void main(String[] args) {
 
        Menu menu = new Menu();
        menu.addItem(new Item("Home", "/home"));
        menu.addItem(new Item("Java", "/java"));
        menu.addItem(new Item("Spring Boot", "/spring-boot"));
 
        ItemIterator<Item> iterator = menu.iterator();
        while (iterator.hasNext()) {
            Item item = iterator.next();
            System.out.println(item);
        }
    }
}

Output của chương trình:

Item [title=Home, url=/home]
Item [title=Java, url=/java]
Item [title=Spring Boot, url=/spring-boot]

Các bạn có thể thêm về cách implement Iterator Pattern trong JDK như:

  • Tất cả các cài đặt của java.util.Iterator 
  • Tất cả các cài đặt của java.util.Enumeration

3. Lợi ích của Iterator Pattern là gì?

Một số lợi ích khi sử dụng Iterator Pattern:

  • Đảm bảo nguyên tắc Single responsibility principle (SRP) : chúng ta có thể tách phần cài đặt các phương thức của tập hợp và phần duyệt qua các phần tử (iterator) theo từng class riêng lẻ.
  • Đảm bảo nguyên tắc Open/Closed Principle (OCP) : chúng ta có thể implement các loại collection mới và iterator mới, sau đó chuyển chúng vào code hiện có mà không vi phạm bất cứ nguyên tắc gì.
  • Chúng ta có thể truy cập song song trên cùng một tập hợp vì mỗi đối tượng iterator có chứa trạng thái riêng của nó.

Một số điểm cần xem xét khi sử dụng Iterator:

  • Sử dụng iterator có thể kém hiệu quả hơn so với việc duyệt qua các phần tử của bộ sưu tập một cách trực tiếp.
  • Có thể không cần thiết nếu ứng dụng chỉ hoạt động với các collection đơn giản.

4. Sử dụng Iterator Pattern khi nào?

  • Cần truy cập nội dung của đối tượng trong tập hợp mà không cần biết nội dung cài đặt bên trong nó.
  • Hỗ trợ truy xuất nhiều loại tập hợp khác nhau.
  • Cung cấp một interface duy nhất để duyệt qua các phần tử của một tập hợp.