十一、迭代器模式與命令模式詳解
18.迭代器模式
18.1.課程目標
1、 瞭解迭代器模式和命令的應用場景。
2、 自己手寫迭代器
3、 掌握迭代器模式和命令模式在源碼中的應用,知其所以然。
18.2.內容定位
聽說過迭代器模式和命令模式,但並不知其所以然的人群。
18.3.迭代器模式
迭代器模式( Iterator Pattern ) 又稱為遊標模式(Cursor Pattern), 它提供一種順序訪問集合/ 容器對象元素的方法,而又無須暴露集合內部表示。迭代器模式可以為不同的容器提供一致的 遍歷行為,而不用關心容器內容元素組成結構,屬於行為型模式。
原文 : Provide a way to access the elements of an aggregate object sequentially without exposing its under lying representation.
解釋:提供一種順序訪問集合/容器對象元素的方法,而又無須暴露集合內部表示。
迭代器模式的本質是抽離集合對象迭代行為到迭代器中,提供一致訪問接口。
18.4.迭代器模式的應用場景
迭代器模式在我們生活中應用的得也比較廣泛,比如物流系統中的傳送帶,不管傳送的是什 麼物品,都被打包成一個一個的箱子並且有一個統一的二維碼。這樣我們不需要關心箱子裡面 是啥,我們在分發時只需要一個一個檢查發送的目的地即可。再比如,我們平時乘坐交通工具, 都是統一刷卡或者刷臉進站,而不需要關心是男性還是女性、是殘疾人還是正常人等個性化的信息。
我們把多個對象聚在一起形成的總體稱之為集合(Aggregate), 集合對象是能夠包容一組對 象的容器對象。不同的集合其內部元素的聚合結構可能不同,而迭代器模式屏蔽了內部元素獲 取細節,為外部提供一致的元素訪問行為,解耦了元素迭代與集合對象間的耦合,並且通過提 供不同的迭代器,可以為同個集合對象提供不同順序的元素訪問行為,擴展了集合對象元素迭 代功能,符合開閉原則。迭代器模式適用於以下場景:
1、 訪問一個集合對象的內容而無需暴露它的內部表示;
2、 為遍歷不同的集合結構提供一個統一的訪問接口。
首先來看下迭代器模式的通用UML類圖:
從 UML類圖中,我們可以看到,迭代器模式主要包含三種角色:
抽象迭代器( Iterator) : 抽象迭代器負責定義訪問和遍歷元素的接口 ;
具體迭代器( Concreteiterator) :提供具體的元素遍歷行為;
抽象容器(Aggregate ) : 負責定義提供具體迭代器的接口 ;
具體容器(ConcreteAggregate ) :創建具體迭代器。
18.5.手寫字定義的送代器
總體來說,迭代器模式還是非常簡單的。我們還是以課程為例,下面我們自己創建一個課程的集合,集合中的每一個元素就是課程對象,然後自己手寫一個迭代器,將每一個課程對象的信息讀出來。
首先創建集合元素課程Course類 :
<code> public class Course {
private String name;
public Course(String name) {
this.name = name;
}
public String getName() {
return name;
}
}/<code>
然後創建自定義迭代器Iterator接口 :
<code> public interface Iterator{ /<code>
E next();
boolean hasNext();
}
然後創建自定義的課程的集合ICourseAggregate接口 :
<code> public interface ICourseAggregate {
void add(Course course);
void remove(Course course);
Iterator<course> iterator();
}/<course>/<code>
然後,分別實現迭代器接口和集合接口,創建Iteratorlmpl實現類:
<code> public class IteratorImplimplements Iterator /<code>{
private Listlist;
private int cursor;
private E element;
public IteratorImpl(Listlist) {
this.list = list;
}
public E next() {
System.out.print("當前位置 " + cursor + " : ");
element = list.get(cursor);
cursor ++;
return element;
}
public boolean hasNext() {
if(cursor > list.size() - 1){
return false;
}
return true;
}
}
創建課程集合CourseAggregatelmpI 實現類:
<code> public class CourseAggregateImpl implements ICourseAggregate {
private List courseList;
public CourseAggregateImpl() {
this.courseList = new ArrayList();
}
public void add(Course course) {
courseList.add(course);
}
public void remove(Course course) {
courseList.remove(course);
}
public Iterator<course> iterator() {
return new IteratorImpl<course>(courseList);
}
}/<course>/<course>/<code>
然後,編寫客戶端代碼:
<code> public class Test {
public static void main(String[] args) {
Course java = new Course("Java架構");
Course javaBase = new Course("Java基礎");
Course design = new Course("設計模式");
Course ai = new Course("人工智能");
ICourseAggregate aggregate = new CourseAggregateImpl();
aggregate.add(java);
aggregate.add(javaBase);
aggregate.add(design);
aggregate.add(ai);
System.out.println("===========課程列表==========");
printCourse(aggregate);
aggregate.remove(ai);
System.out.println("===========刪除操作之後的課程列表==========");
printCourse(aggregate);
}
private static void printCourse(ICourseAggregate aggregate) {
Iterator<course> i = aggregate.iterator();
while (i.hasNext()){
Course course = i.next();
System.out.println("《" + course.getName() + "》");
}
}
}/<course>/<code>
運行結果如下:
看到這裡,小夥伴們肯定會有一種似曾相識的感覺,讓人不禁想起我們每天都在用的JDK 自帶的結合迭代器。下面我們就來看看源碼中是如何運用迭代器的。
18.6.迭代器模式在源碼中的體現
先來看JDK中大家非常熟悉的Iterator源碼 :
<code> public interface Iterator{ /<code>
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
從上面代碼中,我們看到兩個主要的方法定義hasNext()和 next()方 法 ,和我們自己寫的完 全一致。
另外,從上面的代碼中,我們看到removeO方法實現似曾相識。其實是在組合模式中我們 見到過。迭代器模式和組合模式,兩者似乎存在一定的相似性。組合模式解決的是統一樹形結 構各層次訪問接口,迭代器模式解決的是統一各集合對象元素遍歷接口。雖然他們的適配場景 不同,但核心理念是相通的。
下面接看來看Iterator的實現類,其實在我們常用的ArrayList中有一個內部實現類Itr ,它 就實現了 Iterator接口 :
<code> public class ArrayListextends AbstractList /<code>
implements List, RandomAccess, Cloneable, java.io.Serializable
{
private class Itr implements Iterator{
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
...
}
}
其中hasNextO方法和next()方法實現也非常簡單,我們繼續往下看在ArrayList內部還有 幾個迭代器對Itr進行了進一步擴展,首先看Listltr :
<code>private class ListItr extends Itr implements ListIterator{ /<code>
ListItr(int index) {
super();
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
...
}
它增加了 hasPreviousQ方法是否還有上一個等這樣的判斷。另外還有SubList對子集合的迭代處理。
當然,迭代器模式在MyBatis中也是必不可少的,來看一個Defaultcursor類 :
<code>public class DefaultCursorimplements Cursor /<code>{
\t...
private final CursorIterator cursorIterator = new CursorIterator();
}
首先它實現了 Cursor接 口 ,而且定義了一個成員變量cursoriterator , 我繼續查看 Cursoriterator的源代碼發現, 它 是 Defaultcursor的一個內部類,並且實現了 JDK中的 Iterater 接口。
18.7.迭代器模式的優缺點
優點:
1、 多態迭代:為不同的聚合結構提供一致的遍歷接口,即一個迭代接口可以訪問不同的集合對 象 ;
2、 簡化集合對象接口 :迭代器模式將集合對象本身應該提供的元素迭代接口抽取到了迭代器中 ,使集合對象無須關心具體迭代行為;
3、 元素迭代功能多樣化:每個集合對象都可以提供一個或多個不同的迭代器,使的同種元素聚合結構可以有不同的迭代行為;
4、解耦迭代與集合:迭代器模式 封裝了具體的迭代算法,迭代算法的變化,不會影響到集合對象的架構。
缺點:
1、對於比較簡單的遍歷(像數組或者有序列表) ,使用迭代器方式遍歷較為繁瑣。 在日常開發當中,我們幾乎不會自己寫迭代器。除非我們需要定製一個自己實現的數據結構 對應的迭代器,否則,開源框架提供給我們的API完全夠用。
18.8.思維導圖
19.命令模式
19.1.定義
命令模式(Command Pattern )是對命令的封裝,每一個命令都是一個操作:請求的一方發出請求要求執行一個操作;接收的一方收到請求,並執行操作。命令模式解耦了請求方和接收方,請求方只需請求執行命令,不用關心命令是怎樣被接收,怎樣被操作以及是否被執行…等。 命令模式屬於行為型模式。
原 文 : Encapsulate a request as an object, there by letting you parameterize clients with different requests, queue or log requests,and support undoable operations.
解釋:將一個請求封裝成一個對象,從而讓你使用不同的請求把客戶端參數化,對請求排隊或者記錄請求日誌,可以提供命令的撤銷和恢復功能。
在軟件系統中,行為請求者與行為實現者通常是一種緊耦合關係,因為這樣的實現簡單明瞭。 但緊耦合關係缺乏擴展性,在某些場合中,當需要為行為進行記錄,撤銷或重做等處理時,只 能修改源碼。而命令模式通過為請求與實現間引入一個抽象命令接口,解耦了請求與實現,並 且中間件是抽象的,它可以有不同的子類實現,因此其具備擴展性。所以,命令模式的本質是 解耦命令請求與處理。
19.2.命令模式的應用場景
當系統的某項操作具備命令語義時,且命令實現不穩定(變化),那麼可以通過命令模式解耦請求與實現,利用抽象命令接口使請求方代碼架構穩定,封裝接收方具體命令實現細節。接收方與抽象命令接口呈現弱耦合(內部方法無需一致),具備良好的擴展性。
命令模式適用於 以下應用場景:
1、現實語義中具備"命令"的操作(如命令菜單,shell命令…);
2、請求調用者和請求的接收者需要解耦,使得調用者和接收者不直接交互;
3、需要抽象出等待執行的行為,比如撤銷(Undo)操作和恢復(Redo)等操作;
4、需要支持命令宏(即命令組合操作)。
首先看下命令模式的通用UML類圖:
從 UML類圖中,我們可以看到,命令模式 主要包含四種角色:
接收者角色(Receiver):該類負責具體實施或執行一個請求;
命令角色(Command ):定義需要執行的所有命令行為;
具體命令角色(Concrete Command )該類內部維護一個接收者(Receiver ),在其execute() 方法中調用Receiver的相關方法;
請求者角色(Invoker):接收客戶端的命令,並執行命令。
從命令模式的UML類圖中,其實可以很清晰地看出:Command的出現就是作為Receiver 和 Invoker的中間件,解耦了彼此。而之所以引入Command中間件,我覺得是以下兩方面原因 :
解耦請求與實現:即解耦了 Invoker和 Receiver , 因為在UML類圖中,Invoker是一個具 體的實現,等待接收客戶端傳入命令(即 Invoker與客戶端耦合),Invoker處於業務邏輯區域, 應當是一個穩定的結構。而 Receiver是屬於業務功能模塊 是經常變動的 如果沒有Command z 則 Invoker緊耦合Receiver , 一個穩定的結構依賴了一個不穩定的結構,就會導致整個結構都不穩定了。這也就是Command引入的原因:不僅僅是解耦請求與實現,同時穩定( Invoker) 依賴穩 定 (Command ),結構還是穩定的。
擴展性增強:擴展性體現在兩個方面:
1、 Receiver屬於底層細節,可以通過更換不同的Receiver達到不同的細節實現;
2、 Command接口本身就是抽象的,本身就具備擴展性;而且由於命令對象本身就具備抽 象 ,如果結合裝飾器模式,功能擴展簡直如魚得水。
注:在一個系統中,不同的命令對應不同的請求,也就是說無法把請求抽象化,因此命令模式中的Receiver是具體實 現;但是如果在某一個模塊中,可以對Receiver進行抽象,其實這就變相使用到了橋接模式(Command類具備兩個變 化的維度:Command和 Receiver), 這樣子的擴展性會更加優秀。
舉個生活中的例子,相信80後的小夥伴應該都經歷過普及黑白電視機的那個年代。黑白電 視機要換臺那簡直不容易,需要人跑上前去用力掰動電視機上那個切換頻道的旋鈕,一 頓 〃啪 啪啪〃折騰下來才能完成一次換臺。如今時代好了,我們只需躺沙發上按一下遙控器就完成了 換臺。這就是用到了命令模式,將換臺命令和換臺處理進行了分離。
另外,就是餐廳的點菜單,一般是後廚先把所有的原材料組合配置好了,客戶用餐前只需要 點菜即可,將需求和處理進行了解耦。
19.3.命令模式在業務場景中的應用
假如我們自己開發一個播放器,播放器有播放功能、有拖動進度條功能、停止播放功能、暫 停功能,我們自己去操作播放器的時候並不是直接調用播放器的方法,而是通過一個控制條去傳達指令給播放器內核,那麼具體傳達什麼指令,會被封裝為一個一個的按鈕。那麼每個按鈕 的就相當於是對一條命令的封裝。用控制條實現了用戶發送指令與播放器內核接收指令的解耦。
下面來看代碼,首先創建播放器內核GPlayer類:
<code>public class GPlayer {
public void play(){
System.out.println("正常播放");
}
public void speed(){
System.out.println("拖動進度條");
}
public void stop(){
System.out.println("停止播放");
}
public void pause(){
System.out.println("暫停播放");
}
}/<code>
創建命令接口 IAction類:
<code>public interface IAction {
void execute();
}/<code>
然後分別創建操作播放器可以接受的指令,播放指令PlayAction類 :
<code>public class PlayAction implements IAction {
private GPlayer gplayer;
public PlayAction(GPlayer gplayer) {
this.gplayer = gplayer;
}
public void execute() {
gplayer.play();
}
}/<code>
暫停指令PauseAction類 :
<code>public class PauseAction implements IAction {
private GPlayer gplayer;
public PauseAction(GPlayer gplayer) {
this.gplayer = gplayer;
}
public void execute() {
gplayer.pause();
}
}/<code>
拖動進度條指令SpeedAction類 :
<code>public class SpeedAction implements IAction {
private GPlayer gplayer;
public SpeedAction(GPlayer gplayer) {
this.gplayer = gplayer;
}
public void execute() {
gplayer.speed();
}
}/<code>
停止播放指令StopAction類 :
<code>public class StopAction implements IAction {
private GPlayer gplayer;
public StopAction(GPlayer gplayer) {
this.gplayer = gplayer;
}
public void execute() {
gplayer.stop();
}
}/<code>
最後 ,創建控制條Controller類 :
<code>public class Controller {
private List<iaction> actions = new ArrayList<iaction>();
public void addAction(IAction action){
actions.add(action);
}
public void execute(IAction action){
action.execute();
}
public void executes(){
for (IAction action:actions) {
action.execute();
}
actions.clear();
}
}/<iaction>/<iaction>/<code>
從上面代碼來看,控制條可以執行單條命令,也可以批量執行多條命令。下面來看客戶端測試代碼:
<code>public class Test {
public static void main(String[] args) {
GPlayer player = new GPlayer();
Controller controller = new Controller();
controller.execute(new PlayAction(player));
controller.addAction(new PauseAction(player));
controller.addAction(new PlayAction(player));
controller.addAction(new StopAction(player));
controller.addAction(new SpeedAction(player));
controller.executes();
}
}/<code>
運行效果如下:
<code>正常播放
暫停播放
正常播放
停止播放
拖動進度條/<code>
由於控制條已經與播放器內核解耦了,以後如果想擴展新命令,只需增加命令即可,控制條 的結構無需改動。
19.4.命令模式在源碼中的體現
首先來看JDK中的Runnable接口 ,實際上Runnable就相當於是命令的抽象,只要是實現 了 Runnable接口的類都被認為是一個線程。
<code>public interface Runnable {
public abstract void run();
}/<code>
實際上調用線程的start()方法之後,就有資格去搶CPU資源,而不需要我們自己編寫獲得 CPU資源的邏輯。而線程搶到CPU資源後,就會執行run()方法中的內容,用 Runnable接口 把用戶請求和CPU執行進行了解耦。
然 後 ,再看一個大家非常孰悉的junit.framework.Test接口 :
<code>package junit.framework;
public interface Test {
public abstract int countTestCases();
public abstract void run(TestResult result);
}/<code>
Test接口中有兩個方法,第一個是countTestCases()方法用來統計當前需要執行的測試用例 總數。第二個是run()方法就是用來執行具體的測試邏輯,其參數TestResult是用來返回測試結果的。 實際上我們在平時編寫測試用例的時候,只需要實現Test接口即便認為就是一個測試用例,那 麼在執行的時候就會自動識別。實際上我們平時通常做法都是繼承TestCase類 ,我們不妨來看 —下 TestCase的源碼:
<code>public abstract class TestCase extends Assert implements Test {
...
public void run(TestResult result) {
result.run(this);
\t}
...
}/<code>
實際上TestCase類它也實現了 Test接口。我們繼承TestCase類 ,相當於也實現了 Test 接口,自然也就會被掃描成為一個測試用例。
19.5.命令模式的優缺點
優點:
1、通過引入中間件(抽象接口),解耦了命令請求與實現;
2、 擴展性良好,可以很容易地增加新命令;
3、 支持組合命令,支持命令隊列;
4、可以在現有命令的基礎上,增加額外功能(比如日誌記錄…,結合裝飾器模式更酸爽)。
缺點:
1、 具體命令類可能過多;
2、 命令模式的結果其實就是接收方的執行結果,但是為了以命令的形式進行架構,解耦請求與 實現,引入了額外類型結構(引入了請求方與抽象命令接口),增加了理解上的困難(不過這 也是設計模式帶來的一個通病,抽象必然會引入額外類型;抽象肯定比緊密難理解)。
19.6.思維導圖
19.7.作業
1、為什麼需要迭代器,說說你的看法。
- 當集合背後為複雜的數據結構, 且你希望對客戶端隱藏其複雜性時 (出於使用便利性或安全性的考慮), 可以使用迭代器模式。迭代器封裝了與複雜數據結構進行交互的細節, 為客戶端提供多個訪問集合元素的簡單方法。 這種方式不僅對客戶端來說非常方便, 而且能避免客戶端在直接與集合交互時執行錯誤或有害的操作, 從而起到保護集合的作用。
- 使用該模式可以減少程序中重複的遍歷代碼。重要迭代算法的代碼往往體積非常龐大。 當這些代碼被放置在程序業務邏輯中時, 它會讓原始代碼的職責模糊不清, 降低其可維護性。 因此, 將遍歷代碼移到特定的迭代器中可使程序代碼更加精煉和簡潔。
- 如果你希望代碼能夠遍歷不同的甚至是無法預知的數據結構, 可以使用迭代器模式。該模式為集合和迭代器提供了一些通用接口。 如果你在代碼中使用了這些接口, 那麼將其他實現了這些接口的集合和迭代器傳遞給它時, 它仍將可以正常運行。
2、用命令模式實現一個自定義的FTP服務器架構(不需要完成具體功能,但命令可以無限擴展)。
首先創建播放器內核FtpServer類:
<code>public class FtpServer {
public void login(){
System.out.println("登錄");
}
public void exit(){
System.out.println("註銷");
}
public void help(){
System.out.println("幫助");
}
}/<code>
創建命令接口 IAction類:
<code>public interface IAction {
void execute();
}/<code>
然後分別創建指令,幫助指令HelpAction類 :
<code>public class HelpAction implements IAction {
private FtpServer ftpServer;
public HelpAction(FtpServer ftpServer) {
this.ftpServer = ftpServer;
}
public void execute() {
ftpServer.help();
}
}/<code>
登錄指令LoginAction類 :
<code>public class LoginAction implements IAction {
private FtpServer ftpServer;
public LoginAction(FtpServer ftpServer) {
this.ftpServer = ftpServer;
}
public void execute() {
ftpServer.login();
}
}/<code>
註銷指令ExitAction類 :
<code>public class ExitAction implements IAction {
private FtpServer ftpServer;
public ExitAction(FtpServer ftpServer) {
this.ftpServer = ftpServer;
}
public void execute() {
ftpServer.exit();
}
}/<code>
最後 ,創建控制條Controller類 :
<code>public class Controller {
private List<iaction> actions = new ArrayList<iaction>();
public void addAction(IAction action){
actions.add(action);
}
public void execute(IAction action){
action.execute();
}
public void executes(){
for (IAction action:actions) {
action.execute();
}
actions.clear();
}
}/<iaction>/<iaction>/<code>
下面來看客戶端測試代碼:
<code>public class Test {
public static void main(String[] args) {
FtpServer server = new FtpServer();
Controller controller = new Controller();
controller.addAction(new HelpAction(server));
controller.addAction(new LoginAction(server));
controller.addAction(new ExitAction(server));
controller.executes();
}
}/<code>
運行效果如下:
<code>幫助
登錄
註銷/<code>
閱讀更多 我是阿喵醬 的文章