Iterator pattern là gì

  -  
*

Iterator Pattern và Composite Pattern – Quản lý bộ sưu tập

Có rất nhiều cách để nhét object vào một collection.

Bạn đang xem: Iterator pattern là gì

Đặt chúng trong một Array, một Stack, một List, một Hashtable, tùy lựa chọn. Mỗi loại có những lợi thế và sự đánh đổi riêng. Nhưng đến một lúc nào đó, client của bạn sẽ muốn duyệt qua những đối tượng đó, và khi đó, bạn sẽ tự tay cài đặt thuật toán duyệt của mình (duyệt 1 mảng sẽ khác duyệt 1 ArrayList), bạn có muốn như vậy? Chúng tôi hy vọng không! Điều đó không chuyên nghiệp. Khi xem qua phần Iterator pattern, bạn sẽ thấy cách bạn có thể cho phép client của mình duyệt qua các đối tượng mà không cần phải xem qua cách bạn lưu trữ các đối tượng. Bạn cũng sẽ học cách tạo ra một số bộ super collection có thể bỏ qua một số cấu trúc dữ liệu trong một ràng buộc duy nhất. Và nếu điều đó chưa đủ, bạn cũng sẽ học được một hoặc hai thứ về trách nhiệm của đối tượng.

Tin tức mới nhất: Objectville Diner và Objectville Pancake sáp nhập

Tin tốt đấy! Bây giờ chúng ta có thể có được những bữa sáng và những bữa trưa ngon tuyệt ở một cùng nơi. Không phải sáng ở Objectville Pancake và trưa lại ở Objectville Diner nữa. Nhưng dường như có một vấn đề nhỏ…


*

(Trong đoạn trao đổi: Mẹ dùng Mảng và Lou ArrayList để lưu menu)

Kiểm tra qua các menu

Ít nhất là Lou và Mel đồng ý về việc triển khai MenuItems. Hãy cùng kiểm tra các mục trên mỗi menu, và hãy xem cách thực hiện.


*

*

Cài đặt menu của Lou và Mel

Bây giờ, hãy nhìn về những gì Lou và Mel đang tranh cãi. Cả hai đều tốn rất nhiều thời gian và code được đầu tư theo cách họ lưu trữ các item trong một menu, và rất nhiều code khác phụ thuộc vào nó.

Lou: Tôi đã sử dụng một ArrayList để tôi có thể dễ dàng thêm mới một menu item.

*

Mel: Haaa! Một Arraylist … Tôi đã sử dụng một array THỰC SỰ để tôi có thể kiểm soát kích thước tối đa của menu và get MenuItems của mình mà không phải sử dụng cast.

*

Vấn đề với việc có hai biểu diễn menu khác nhau là gì?

Để xem lý do tại sao việc có hai biểu diễn menu khác nhau lại làm phức tạp mọi thứ, hãy thử thực hiện một ứng dụng client sử dụng hai menu. Hãy tưởng tượng bạn đã được thuê bởi công ty mới được thành lập bởi sự hợp nhất của Diner và Pancake House để tạo ra một phục vụ Java-enabled Waitress. Mô tả cho người phục vụ để cô ấy có thể in một menu theo khách hàng theo yêu cầu và thậm chí cho bạn biết khi một menu item là món chay mà không cần phải hỏi đầu bếp – bây giờ là một sự đổi mới!

Hãy kiểm tra mô tả, và sau đó bước qua những gì có thể cần để thực hiện…

Mô tả cho Java-Enabled Waitress (client)


*

Hãy bắt đầu bằng cách xem qua cách chúng tôi thực hiện phương thức printMenu():

1. Để in tất cả các item trên mỗi menu, bạn sẽ cần gọi phương thức getMenuItem() trên PancakeHouseMenuDinerMenu để lấy các menu item tương ứng của chúng. Lưu ý rằng mỗi phương thức trả về một kiểu lưu trữ khác nhau (mảng và ArrayList):

*

2. Bây giờ, để in ra các item từ PancakeHouseMenu, chúng tôi sẽ duyệt qua các item trên ArrayList breakfastItems. Và để in ra các Diner item, chúng tôi sẽ duyệt qua một mảng.

*

Thực hiện mọi phương thức khác trong Waitress sẽ là một biến thể của chương này. Chúng tôi luôn luôn cần có cả hai menu và sử dụng hai vòng để lặp qua các item của chúng. Nếu một nhà hàng khác có triển khai khác được gộp thì chúng tôi sẽ có ba vòng lặp.

Bài tập:

Dựa trên việc triển khai printMenu() của chúng tôi, áp dụng nào sau đây?

❏ A. Chúng tôi đang code cho các triển khai cụ thể của PancakeHouseMenuDinerMenu, chứ không phải cho một interface.❏ B. Waitress không implement Java Waitress API và vì vậy cô ấy không tuân thủ theo tiêu chuẩn.❏ C. Nếu chúng tôi quyết định chuyển từ sử dụng DinerMenu sang một loại menu khác đã triển khai danh sách các menu item của nó bằng Hashtable, chúng tôi đã phải sửa đổi rất nhiều code trong Waitress.❏ D. Waitress cần biết làm thế nào mỗi menu đại diện cho tập hợp các menu item bên trong của nó; Điều này vi phạm nguyên tắc đóng gói.❏ E. Chúng tôi có code trùng lặp: phương thức printMenu() cần hai vòng lặp riêng biệt để lặp qua hai loại menu khác nhau. Và nếu chúng ta thêm một menu thứ ba, chúng ta sẽ có một vòng lặp khác.❏ F. Việc triển khai không dựa trên MXML (Menu XML) và do đó không thể tương thích như mong muốn.

Đáp án: A, C, D, E

Bây giờ thì sao?

Mel và Lou đang đặt chúng ta vào thế khó. Họ không muốn thay đổi cách triển khai vì điều đó có nghĩa là viết lại rất nhiều code trong mỗi lớp menu tương ứng. Nhưng nếu một trong số họ không nhượng bộ, thì chúng tôi sẽ có công việc của Waitress sẽ khó duy trì và mở rộng.

Sẽ thật tuyệt nếu chúng ta có thể tìm cách cho phép chúng implements cùng một interface cho các menu. Hiện tại chúng đã đóng (không có implement), ngoại trừ kiểu trả về của phương thức getMenuItems(). Bằng cách đó, chúng ta có thể giảm thiểu các tham chiếu cụ thể trong code Waitress và cũng hy vọng thoát khỏi nhiều vòng lặp cần thiết để duyệt trên cả hai menu.

Nghe hay đấy? Chà, làm thế nào chúng ta làm được điều đó?

Chúng ta có thể gói gọn việc duyệt phần tử không?

Nếu chúng ta đã được học một điều từ cuốn sách này, đó là đóng gói những gì thay đổi. Những gì đang thay đổi ở đây đã rõ ràng: “duyệt phần tử”, gây ra bởi các kiểu tập hợp khác nhau, được return từ các menu. Nhưng chúng ta có thể đóng gói điều này không? Hãy suy nghĩ một chút…

*
*
*
*

Gặp gỡ Iterator Pattern

Chà, có vẻ như kế hoạch đóng gói của chúng ta thực sự có thể hoạt động; và bạn có thể đã biết từ đầu tiêu đề, Mẫu thiết kế này được gọi là Iterator Pattern.

Điều đầu tiên bạn cần biết về Iterator Pattern là nó dựa trên interface có tên là Iterator. Ở đây, một Iterator interface có thể có:

*

Bây giờ, khi chúng ta có giao diện này, chúng ta có thể triển khai Iterator pattern cho bất kỳ loại tập hợp đối tượng nào: mảng, danh sách, hashtables, … chọn loại tập hợp đối tượng yêu thích của bạn. Khi chúng ta muốn triển khai Iterator pattern cho Array được sử dụng trong DinerMenu. Nó sẽ trông như thế này:


*

Áp dụng Iterator Pattern vào DinerMenu

Để thêm Iterator vào DinerMenu, trước tiên chúng ta cần định nghĩa Iterator Interface:

*

Và bây giờ chúng ta cần triển khai một Iterator con đại diện cho Diner Menu:

*

Làm lại DinerMenu với Iterator Pattern

Được rồi, chúng tôi đã có các iterator. Thời gian để làm việc với DinerMenu; tất cả những gì chúng ta cần làm là thêm một phương thức để tạo DinerMenuIterator và return lại cho client:

*

Bài tập:

Hãy tiếp tục và tự mình thực hiện PancakeHouseIterator và thực hiện các thay đổi cần thiết để kết hợp nó vào PancakeHouseMenu.

Sửa code Waitress

Bây giờ chúng ta cần tích hợp iterator pattern vào Waitress. Chúng ta sẽ có thể loại bỏ một số dư thừa trong tiến trình. Việc tích hợp khá đơn giản: đầu tiên chúng ta tạo một phương thức printMenu(), sau đó chúng ta sử dụng phương thức createIterator() trên mỗi menu để lấy Iterator và chuyển nó sang phương thức mới.

*
Testing code của bạn

Đây là thời gian để đưa mọi thứ vào một thử nghiệm. Hãy viết một số test drive và xem cách Waitress hoạt động…

*

Đây là kết quả…

*

Chúng ta đã làm gì cho tới bây giờ?

Để bắt đầu, chúng tôi đã làm cho các đầu bếp Objectville của chúng tôi rất hạnh phúc. Họ giải quyết sự khác biệt và giữ thực hiện riêng của họ. Khi chúng tôi đưa cho họ một PancakeHouseMenuIteratorDinerMenuIterator, tất cả những gì họ phải làm là thêm một phương thức createIterator() và họ đã hoàn thành.

Chúng tôi cũng đã tự giúp mình trong quá trình này. Waitress sẽ dễ dàng hơn nhiều để duy trì và mở rộng trong tương lai. Hãy đi qua chính xác những gì chúng ta đã làm và nghĩ về hậu quả:


*

Những gì chúng ta có cho đến giờ…

Trước khi chúng tôi dọn dẹp mọi thứ, hãy có một cái nhìn toàn cảnh về thiết kế hiện tại của chúng tôi.


Cải tiến menu với Iterator Pattern…của Java

Được rồi, chúng tôi biết các interface của PancakeHouseMenuDinerMenu hoàn toàn giống nhau và chúng tôi chưa định nghĩa interface chung cho chúng. Vì vậy, chúng tôi sẽ làm điều đó và thay đổi Waitress thêm một chút nữa.

Bạn có thể tự hỏi tại sao chúng tôi không sử dụng interface Iterator của Java từ đầu – chúng tôi đã làm vậy để bạn có thể thấy cách xây dựng một iterator. Bây giờ chúng tôi đã thực hiện điều đó, chúng tôi sẽ chuyển sang sử dụng interface Iterator Java, bởi vì chúng tôi sẽ nhận được rất nhiều lợi ích bằng cách implement nó thay vì interface Iterator “tự xây dựng” của chúng tôi. Những lợi ích gì? Bạn sẽ sớm thấy thôi.

Trước tiên, hãy xem qua giao diện java.util.Iterator:

*

Đây sẽ là một miếng bánh: Chúng ta chỉ cần thay đổi interface mà cả PancakeHouseMenuIteratorDinerMenuIterator extend, phải không? Hầu như … thực sự, nó thậm chí còn dễ dàng hơn thế. Java.util không chỉ có interface Iterator riêng mà ArrayList còn có phương thức iterator() trả về một iterator. Nói cách khác, chúng ta không bao giờ cần phải implement iterator riêng cho ArrayList. Tuy nhiên, chúng tôi vẫn cần triển khai cho DinerMenu vì giả sử nó phụ thuộc vào một mảng không hỗ trợ phương thức iterator() (hoặc không hỗ trợ bất kỳ cách nào khác để tạo một array iterator).

Xem thêm: Tuổi Các Nhân Vật Trong Bảy Viên Ngọc Rồng ", Cận Cảnh Các Nhân Vật Chính Của 7 Viên Ngọc Rồng

Không có câu hỏi ngớ ngẩn

Hỏi: Điều gì sẽ xảy ra nếu tôi không muốn cung cấp khả năng remove thứ gì đó khỏi tập hợp các đối tượng cơ bản?

Trả lời: Phương thức remove() được coi là tùy chọn. Bạn không phải cung cấp chức năng remove. Nhưng, rõ ràng là bạn cần phải cung cấp phương thức này vì nó là một phần của interface Iterator. Nếu bạn không cho phép remove() trong iterator của mình, bạn sẽ ném runtime exception java.lang.UnsupportedOperationException.

Tài liệu API của Iterator chỉ định rằng exception này có thể được ném từ remove() và bất kỳ thiết kế nào tốt đều sẽ kiểm tra exception này khi gọi phương thức remove().

Hỏi: Làm thế nào để remove() hoạt động trong đa luồng (multiple thread) có thể sử dụng các iterator khác nhau trên cùng một tập hợp đối tượng?

Trả lời: Hành vi của remove() là không xác định nếu tập hợp bị thay đổi trong khi bạn đang duyệt qua nó. Vì vậy, bạn nên cẩn thận trong việc thiết kế code multiple thread của riêng bạn khi truy cập đồng thời một tập hợp.

Dọn dẹp mọi thứ với java.util.Iterator

Hãy bắt đầu với PancakeHouseMenu, thay đổi nó thành java.util.Iterator sẽ trở nên dễ dàng. Chúng tôi chỉ cần xóa lớp PancakeHouseMenuIterator, thêm java.util.Iterator lên đầu class PancakeHouseMenu và thay đổi một dòng của PancakeHouseMenu:

*

Và đó là nó, PancakeHouseMenu đã hoàn thành.

Bây giờ chúng ta cần thực hiện các thay đổi để cho phép DinerMenu hoạt động với java.util.Iterator.

*

Chúng ta gần hoàn thành…

Chúng ta chỉ cần cung cấp cho các Menu một interface chung và làm lại Waitress một chút. Giao diện Menu khá đơn giản: cuối cùng chúng tôi có thể muốn thêm một vài phương thức nữa, như addItem(), nhưng bây giờ chúng tôi sẽ cho phép các đầu bếp kiểm soát các menu của họ bằng cách đưa phương thức đó ra khỏi public interface:

*

Bây giờ chúng ta cần thêm implements Menu vào cả định nghĩa lớp PancakeHouseMenuDinerMenu, sau đó cập nhật Waitress:

*

Iterator Pattern cho chúng ta những gì?

Các lớp PancakeHouseMenuDinerMenu implement cùng một interface, Menu. Waitress có thể tham chiếu từng đối tượng menu bằng interface chứ không phải lớp cụ thể. Vì vậy, chúng tôi giảm bớt sự phụ thuộc giữa Waitress và các lớp cụ thể bằng cách lập trình vào một giao diện, chứ không phải là một triển khai (programming to an interface, not an implementation) => Điều này giải quyết vấn đề của Waitress phụ thuộc vào concrete Menu (menu con).

Interface Menu mới có một phương thức, createIterator(), được triển khai bởi PancakeHouseMenuDinerMenu. Mỗi lớp menu đảm nhận trách nhiệm tạo ra một Iterator cụ thể phù hợp cho việc thực hiện bên trong các menu item => Điều này giải quyết vấn đề của Waitress phụ thuộc vào việc triển khai MenuItem.


Định nghĩa Iterator Pattern

Bạn đã thấy cách triển khai Iterator Pattern với iterator riêng của mình. Bạn cũng đã thấy cách Java hỗ trợ các iterator trong một số lớp collection (ArrayList). Bây giờ đã đến lúc để kiểm tra định nghĩa chính thức của mẫu:


(Iterator Pattern cung cấp một cách để truy cập các phần tử của một tập hợp đối tượng một cách tuần tự mà không làm lộ đại diện bên dưới của nó)

Điều này rất có ý nghĩa: mẫu cung cấp cho bạn một cách để duyệt qua các phần tử của tập hợp mà không cần phải biết cách mọi thứ được thể hiện bên trong. Bạn đã thấy điều đó với hai cài đặt của Menu. Nhưng hiệu quả của việc sử dụng các Iterator Pattern trong thiết kế của bạn là: một khi bạn có cách truy cập thống nhất các phần tử của tất cả các tập hợp của mình, bạn có thể viết code đa hình hoạt động với bất kỳ tập hợp nào – giống như phương thức printMenu(), nó không quan tâm các menu item được lưu trong một Array hay ArrayList (hoặc bất cứ thứ gì khác có thể tạo Iterator), miễn là nó có thể tạo được Iterator.

Tác động quan trọng khác đối với thiết kế của bạn là Iterator Pattern có trách nhiệm di chuyển các phần tử và giao trách nhiệm đó cho iterator object chứ không phải collection object. Điều này không chỉ giữ cho collection interface và collection implement (các đối tượng Array, ArrayList) đơn giản hơn, nó loại bỏ trách nhiệm duyệt phần tử khỏi tập hợp và giữ cho tập hợp tập trung vào những thứ cần tập trung (quản lý tập hợp đối tượng), không phải quản lý vòng lặp.

Iterator Pattern: Sơ đồ lớp

*

Sử dụng sức mạnh bộ não

Sơ đồ lớp cho Iterator Pattern trông rất giống với Mẫu nào khác mà bạn đã nghiên cứu? Gợi ý: Một lớp con quyết định đối tượng nào được tạo.

Không có câu hỏi ngớ ngẩn

Hỏi: Tôi đã thấy các cuốn sách khác biểu diển sơ đồ lớp Iterator với các phương thức first(), next(), isDone()currentItem(). Tại sao chúng không giống các phương thức của Iterator Pattern trong chương này?

Trả lời: Đó là những tên phương thức cũ đã được sử dụng. Các tên này đã thay đổi theo thời gian và bây giờ chúng ta có next(), hasNext() và thậm chí remove() trong java.util.Iterator.

Hãy nhìn vào các phương thức cũ next()currentItem() đã được hợp nhất thành một phương thức trong java.util. Phương thức isDone() rõ ràng đã trở thành hasNext(); nhưng chúng ta không có phương thức tương ứng với First(). Điều đó bởi vì trong Java, chúng ta có xu hướng chỉ cần có một vòng lặp mới bất cứ khi nào chúng ta cần bắt đầu duyệt phần tử. Tuy nhiên, bạn có thể thấy có rất ít sự khác biệt trong các giao diện này. Trong thực tế, có một loạt các hành vi bạn có thể đưa ra cho các iterator của mình. Phương thức remove() là một ví dụ về phần mở rộng trong java.util.Iterator.

Hỏi: Tôi đã nghe nói về các “internal” iterator và “external” iterator. Chúng là gì? Chúng ta đã thực hiện trong ví dụ trên loại nào?

Trả lời: Chúng ta đã triển khai một external iterator, có nghĩa là client điều khiển phép lặp bằng cách gọi next() để lấy phần tử tiếp theo. Một internal iterator được điều khiển bởi chính iterator đó. Trong trường hợp đó, bởi vì nó là bộ lặp iterator mà duyệt qua các phần tử, bạn phải nói cho iterator biết phải làm gì với các phần tử đó khi nó duyệt qua chúng. Điều đó có nghĩa là bạn cần một cách để chuyển một hoạt động đến một iterator. Các internal iterator kém linh hoạt hơn các external iterator vì client không có quyền kiểm soát vòng lặp. Tuy nhiên, một số người có thể lập luận rằng chúng dễ sử dụng hơn vì bạn chỉ cần đưa cho chúng một thao tác và bảo chúng lặp lại, và chúng làm tất cả công việc cho bạn.

Hỏi: Tôi có thể triển khai một Iterator có thể duyệt lùi giống như duyệt tới không?

Trả lời: Chắc chắn rồi. Trong trường hợp đó, bạn có thể muốn thêm hai phương thức, một phương thức để duyệt đến phần tử phía sau và một phương thức để duyệt đến phần tử phía trước. Java’s Collection Framework cung cấp một loại giao diện lặp khác gọi là ListIterator. Trình lặp này thêm previous() và một vài phương thức khác vào giao diện Iterator tiêu chuẩn. Nó được hỗ trợ bởi bất kỳ tập hợp nào implement List interface.

Hỏi: Ai định nghĩa thứ tự duyệt phần tử trong tập hợp như Hashtable, chúng vốn không có thứ tự?

Trả lời: Iterator ngầm định là không có thứ tự. Các tập hợp cơ bản có thể không được sắp xếp; chúng thậm chí có thể chứa các phần tử trùng lặp. Vì vậy, thứ tự có liên quan đến cả các thuộc tính của tập hợp bên trong các các implement của nó. Nói chung, bạn không nên đưa ra giả định nào về thứ tự trừ khi Collection chỉ ra.

Hỏi: Bạn nói rằng chúng ta có thể viết code đa hình (polymorphic code) bằng cách sử dụng một trình iterator; bạn có thể giải thích thêm không?

Trả lời: Khi chúng ta viết các phương thức lấy Iterator làm tham số, chúng ta đang sử dụng phép lặp đa hình. Điều đó có nghĩa là chúng tôi đang tạo code có thể duyệt trên bất kỳ tập hợp nào miễn là nó hỗ trợ Iterator. Chúng tôi không quan tâm đến cách tập hợp được implement, chúng tôi vẫn có thể viết code để duyệt nó.

Hỏi: Nếu tôi sử dụng Java, không phải lúc nào tôi cũng muốn sử dụng giao diện java.util.Iterator, vì vậy tôi có thể tạo ra các iterator của riêng mình cho các lớp đã sử dụng Java iterator không?

Trả lời: Có lẽ. Nếu bạn có một Iterator interface chung, chắc chắn nó sẽ giúp bạn dễ dàng trộn và kết nối các tập hợp của riêng bạn với các tập hợp Java như ArrayList và Vector. Nhưng hãy nhớ, nếu bạn cần thêm chức năng vào giao diện Iterator cho tập hợp của bạn, bạn luôn có thể extend Iterator interface.

Xem thêm: Hỏi Tí Về Cách Reset Giờ Chơi Cf Vn, Reset Giờ Chơi Cf

Hỏi: Tôi đã thấy một Enumeration interface trong Java; Nó có cài đặt Iterator Pattern không?

Trả lời: Chúng tôi đã nói về điều này trong Chương Adaptor Pattern. Nhớ lại xem? Các java.util.Enumeration là một triển khai cũ hơn của Iterator đã được thay thế bởi java.util.Iterator. Enumeration có hai phương thức, hasMoreElements(), tương ứng với hasNex()nextElement() tương ứng với next(). Tuy nhiên, bạn có thể muốn sử dụng Iterator trên Enumeration vì nhiều lớp Java hỗ trợ nó. Nếu bạn cần chuyển đổi từ cái này sang cái khác, hãy xem lại Chương Adaptor nơi bạn đã triển khai bộ chuyển đổi Enumeration và Iterator.

Single Responsibility và Iterator Pattern

Điều gì xảy ra nếu chúng tôi cho phép tập hợp của chúng tôi implement các “internal collection” và các chức năng liên quan VÀ các iteration method của chúng? Chà, chúng ta đã biết rằng sẽ mở rộng số lượng phương thức trong tập hợp, nhưng vậy thì sao? Tại sao điều đó sẽ rất tệ?

Chà, để xem tại sao, trước tiên bạn cần nhận ra rằng khi chúng tôi cho phép một lớp không chỉ quan tâm nghiệp vụ của riêng mình (quản lý một số loại tập hợp) mà còn đảm nhận nhiều trách nhiệm hơn (như vòng lặp) thì chúng tôi đã giao cho lớp hai lý do để thay đổi. Hai ư? vâng, hai: nó có thể thay đổi nếu bộ sưu tập thay đổi theo một cách nào đó và nó có thể thay đổi nếu cách chúng ta duyệt vòng lặp thay đổi. Vì vậy, một lần nữa người bạn THAY ĐỔI của chúng tôi là trung tâm của một nguyên tắc thiết kế khác: