你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 設計模式系列7

設計模式系列7

編輯:IOS開發基礎

場景分析

我們平時去餐廳吃飯,都會使用菜單來點餐,今天我們來實現一個超級菜單,這個一個菜單大集合,包括單一菜品和子菜單,如圖所示:

1.png

可以看到上面的菜單不但包括單個的菜品項目,還包括子菜單項目,子菜單也包含一系列菜品或者子菜單。

我們現在想實現兩個個需求:

  • 如果是菜單項目,我們需要打印菜單的名稱和描述,添加刪除子菜單或者菜品,打印所有子菜單、子菜單包括的菜品、子菜單的子菜單的名稱和描述,一直遞歸打印到最後一個菜品項目。

  • 如果是菜品項目,我們需要得到菜品的價格、描述、名稱、是否是素菜這些信息

可以發現上述兩個需求有相同和不同的地方,常規做法就是區別對待兩者各自進行操作。但是這樣以後擴展起來就非常麻煩,如果添加或者刪除兩者,那麼原有代碼就需要做相應的修改。而且兩者其實很多操作都是類似的,卻要寫兩套代碼,操作繁瑣。

分析下上面的圖,我們不難發現這是一個典型的樹形結構圖,菜品項目是葉節點,子菜單項目是子節點(子節點還可以包含子節點或者葉節點),所有菜單是根節點,這個結構可以無限延伸下去。

如果我們能統一對待葉節點和子節點,使用一致的方式在樹結構間游走處理,那就方便許多,這樣不管以後新加一個葉節點還是子節點,原有代碼都不需要修改,因為他們二者的處理方式完全一致。

下面我們就來看看具體的實現。

代碼實現

1、定義葉節點和子節點的父類

先定義一個抽象類,作為葉節點和子節點的父類,父類定義了兩者的所有操作方法,兩者可以自己選擇實現自己需要的方法。父類的每個方法默認實現都是拋出異常,等待子類覆蓋實現。如果子類沒有覆蓋,然後又調用了該方法,就會拋出異常。

#import @interface MenuComponent : NSObject
-(void)add:(MenuComponent *)component;
-(void)remove:(MenuComponent *)component;
-(MenuComponent*)getChild:(NSInteger)position;
-(NSString*)getName;
-(NSString*)getDescription;
-(CGFloat)getPrice;
-(BOOL)isVegetarian;
-(void)print;
@end
===============
#import "MenuComponent.h"
@implementation MenuComponent
-(void)add:(MenuComponent *)component{
    NSString *reason = [NSString stringWithFormat:@"【%@】沒有實現該方法",NSStringFromClass([self class])];
    @throw ([NSException exceptionWithName:@"不支持該方法" reason:reason userInfo:nil]);
}
-(void)remove:(MenuComponent *)component{
    NSString *reason = [NSString stringWithFormat:@"【%@】沒有實現該方法",NSStringFromClass([self class])];
    @throw ([NSException exceptionWithName:@"不支持該方法" reason:reason userInfo:nil]);
}
-(MenuComponent *)getChild:(NSInteger)position{
    NSString *reason = [NSString stringWithFormat:@"【%@】沒有實現該方法",NSStringFromClass([self class])];
    @throw ([NSException exceptionWithName:@"不支持該方法" reason:reason userInfo:nil]);
}
-(NSString *)getName{
    NSString *reason = [NSString stringWithFormat:@"【%@】沒有實現該方法",NSStringFromClass([self class])];
    @throw ([NSException exceptionWithName:@"不支持該方法" reason:reason userInfo:nil]);
}
-(NSString *)getDescription{
    NSString *reason = [NSString stringWithFormat:@"【%@】沒有實現該方法",NSStringFromClass([self class])];
    @throw ([NSException exceptionWithName:@"不支持該方法" reason:reason userInfo:nil]);
}
-(CGFloat)getPrice{
    NSString *reason = [NSString stringWithFormat:@"【%@】沒有實現該方法",NSStringFromClass([self class])];
    @throw ([NSException exceptionWithName:@"不支持該方法" reason:reason userInfo:nil]);
}
-(BOOL)isVegetarian{
    NSString *reason = [NSString stringWithFormat:@"【%@】沒有實現該方法",NSStringFromClass([self class])];
    @throw ([NSException exceptionWithName:@"不支持該方法" reason:reason userInfo:nil]);
}
-(void)print{
    NSString *reason = [NSString stringWithFormat:@"%@沒有實現該方法",NSStringFromClass([self class])];
    @throw ([NSException exceptionWithName:@"不支持該方法" reason:reason userInfo:nil]);
}
@end

2、實現菜單項目

#import "MenuComponent.h"
@interface Menu : MenuComponent
@property(copy ,nonatomic)NSString *name;
@property(copy ,nonatomic)NSString *desc;
@property(strong,nonatomic)NSMutableArray* menuComponentArr;
-(instancetype)initMenuItemWithName:(NSString*)name withDesc:(NSString*)desc;
@end
========================================
#import "Menu.h"
@implementation Menu
-(instancetype)initMenuItemWithName:(NSString *)name withDesc:(NSString *)desc{
    if (self == [super init]) {
        self.name = name;
        self.desc = desc;
        self.menuComponentArr = [NSMutableArray array];
    }
    return self;
}
-(NSString *)getDescription{
    return self.desc;
}
-(NSString *)getName{
    return self.name;
}
-(void)add:(MenuComponent *)component{
    [self.menuComponentArr addObject:component];
}
-(void)remove:(MenuComponent *)component{
    [self.menuComponentArr enumerateObjectsUsingBlock:^(MenuComponent * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if(obj == component){
            [self.menuComponentArr removeObject:component];
        }else{
            if ([obj isKindOfClass:[Menu class]]) {
                if ([((Menu *)obj).menuComponentArr containsObject:component]) {
                    [obj remove:component];
                    }
                }
            }
    }];
}
-(MenuComponent*)getChild:(NSInteger)position{
    return self.menuComponentArr[position];
}
-(void)print{
    NSLog(@"菜單名稱:%@ | 菜單描述:%@ " ,self.name, self.desc);
    if(self.menuComponentArr.count){
        for (MenuComponent * component in self.menuComponentArr) {
            [component print];
        }
    }
}
@end

3、實現菜品項目

#import "MenuComponent.h"
@interface menuItem : MenuComponent
@property(copy ,nonatomic)NSString *name;
@property(copy ,nonatomic)NSString *desc;
@property(assign,nonatomic)NSInteger isVegetarain;
@property(assign,nonatomic)CGFloat price;
-(instancetype)initMenuItemWithName:(NSString*)name withDesc:(NSString*)desc withVegetarain:(NSInteger)isVege withPrice:(CGFloat)price;
@end
=====================================================
#import "menuItem.h"
@implementation menuItem
-(instancetype)initMenuItemWithName:(NSString *)name withDesc:(NSString *)desc withVegetarain:(NSInteger)isVege withPrice:(CGFloat)price{
    if (self == [super init]) {
        self.name = name;
        self.desc = desc;
        _isVegetarain = isVege;
        self.price = price;
    }
    return self;
}
-(CGFloat)getPrice{
    return self.price;
}
-(NSString *)getDescription{
    return self.desc;
}
-(NSString *)getName{
    return self.name;
}
-(BOOL)isIsVegetarain{
    return self.isVegetarain;
}
-(void)print{
    NSLog(@"菜品名稱:%@ | 菜品價格:%f | 菜品描述:%@ | 是否是素菜:%@" ,self.name, self.price, self.desc, self.isVegetarain ? @"是":@"不是");
}
@end

4、客戶端調試

我們先按照文章開頭的圖完成菜單的構建

MenuComponent *pancakeHouseMenu = [[Menu alloc]initMenuItemWithName:@"博餅屋菜單" withDesc:@"早餐"];
        MenuComponent *dinnerMenu = [[Menu alloc]initMenuItemWithName:@"正餐菜單" withDesc:@"午餐"];
        MenuComponent *cafeMenu = [[Menu alloc]initMenuItemWithName:@"咖啡菜單" withDesc:@"晚餐"];
        MenuComponent *dessertMenu = [[Menu alloc]initMenuItemWithName:@"甜點菜單" withDesc:@"飯後甜點"];
        MenuComponent *allMenu = [[Menu alloc]initMenuItemWithName:@"所有菜單" withDesc:@"所有菜單的組合"];
        [allMenu add:pancakeHouseMenu];
        [allMenu add:dinnerMenu];
        [allMenu add:cafeMenu];
        menuItem *meatItem = [[menuItem alloc]initMenuItemWithName:@"紅燒肉" withDesc:@"祖傳紅燒肉,肥而不膩" withVegetarain:0 withPrice:177.2f];
        menuItem *fishItem = [[menuItem alloc]initMenuItemWithName:@"清蒸鲈魚" withDesc:@"新鮮味美,回味無窮" withVegetarain:0 withPrice:2332.0f];
        [dinnerMenu add:meatItem];
        [dinnerMenu add:fishItem];
        menuItem *dessertItem1 = [[menuItem alloc]initMenuItemWithName:@"清炒小白菜" withDesc:@"味美而鮮,有機綠色無污染" withVegetarain:1 withPrice:17.3f];
        menuItem *dessertItem2 = [[menuItem alloc]initMenuItemWithName:@"玉米排骨湯" withDesc:@"飯後一口湯,快樂似神仙" withVegetarain:1 withPrice:243.3f];
        [dessertMenu add:dessertItem1];
        [dessertMenu add:dessertItem2];
        [dinnerMenu add:dessertMenu];

此時我們打印一下所有菜單,只需要一句命令就可以打印出所有的菜品項目和子菜單項目

[allMenu print];

輸出如下:

2016-12-04 19:22:12.569 組合模式[39987:657657] 菜單名稱:所有菜單 | 菜單描述:所有菜單的組合 
2016-12-04 19:22:12.570 組合模式[39987:657657] 菜單名稱:博餅屋菜單 | 菜單描述:早餐 
2016-12-04 19:22:12.570 組合模式[39987:657657] 菜單名稱:正餐菜單 | 菜單描述:午餐 
2016-12-04 19:22:12.570 組合模式[39987:657657] 菜品名稱:紅燒肉 | 菜品價格:177.199997 | 菜品描述:祖傳紅燒肉,肥而不膩 | 是否是素菜:不是
2016-12-04 19:22:12.570 組合模式[39987:657657] 菜品名稱:清蒸鲈魚 | 菜品價格:2332.000000 | 菜品描述:新鮮味美,回味無窮 | 是否是素菜:不是
2016-12-04 19:22:12.570 組合模式[39987:657657] 菜單名稱:甜點菜單 | 菜單描述:飯後甜點 
2016-12-04 19:22:12.571 組合模式[39987:657657] 菜品名稱:清炒小白菜 | 菜品價格:17.299999 | 菜品描述:味美而鮮,有機綠色無污染 | 是否是素菜:是
2016-12-04 19:22:12.571 組合模式[39987:657657] 菜品名稱:玉米排骨湯 | 菜品價格:243.300003 | 菜品描述:飯後一口湯,快樂似神仙 | 是否是素菜:是
2016-12-04 19:22:12.571 組合模式[39987:657657] 菜單名稱:咖啡菜單 | 菜單描述:晚餐

此時我們試著刪除dessertMenu,然後再次打印菜單

2016-12-04 19:22:12.571 組合模式[39987:657657] 菜單名稱:所有菜單 | 菜單描述:所有菜單的組合 
2016-12-04 19:22:12.571 組合模式[39987:657657] 菜單名稱:博餅屋菜單 | 菜單描述:早餐 
2016-12-04 19:22:12.571 組合模式[39987:657657] 菜單名稱:正餐菜單 | 菜單描述:午餐 
2016-12-04 19:22:12.571 組合模式[39987:657657] 菜品名稱:紅燒肉 | 菜品價格:177.199997 | 菜品描述:祖傳紅燒肉,肥而不膩 | 是否是素菜:不是
2016-12-04 19:22:12.571 組合模式[39987:657657] 菜品名稱:清蒸鲈魚 | 菜品價格:2332.000000 | 菜品描述:新鮮味美,回味無窮 | 是否是素菜:不是
2016-12-04 19:22:12.571 組合模式[39987:657657] 菜單名稱:咖啡菜單 | 菜單描述:晚餐

可以看到移除成功。

如果我們試著對dinnerMenu這個子菜單項目調用如下方法

[dinnerMenu isVegetarian];

會發現直接崩潰報錯如下:

2016-12-04 20:40:44.049 組合模式[40371:710191] *** Terminating app due to uncaught exception '不支持該方法', reason: '【Menu】沒有實現該方法'

此處就涉及到一個取捨問題:透明性和安全性誰更重要?

上面的例子就是保證了透明性,讓子節點和葉節點被統一對待,如果調用了二者不支持的方法就直接拋出異常。安全性就需要對調用者做一個判斷,如果調用者調用了錯誤的方法就不執行,這樣保證不會拋出異常,但是需要區別調用者。

我們使用組合模式的意圖就是為了保持葉節點和子節點的一致性,所以一般更偏重於透明性而不是安全性。

通過上面的例子大家應該對組合模式有了一個感性的認識,那麼現在我們來具體看看組合模式的定義

定義

將 對 象 組 合 成 樹 形 結 構 以 表 示 “ 部 分 -整 體 ” 的 層 次 結 構 。 組合模式 使 得 用 戶 對 單 個 對 象 和組合對象的使用具有一致性。

組合模式的目的就是讓客戶端不用區分操作的對象是子節點還是葉節點,而是用一種統一的方式來操作。

實現這個目標的關鍵在於,設計一個抽象的組件類,讓它可以代碼子節點和葉節點,這樣客戶端就不需要區分二者,統一操作它們即可。

通常,組合模式都是用樹形結構來表示的,通過根節點、子節點、葉節點組合成一顆對象樹,這也意味著任何可以使用對象樹來描述或者操作的功能,都可以使用組合模式來進行,比如XML解析,層次結構的菜單,iOS中由多個子視圖構成的復雜視圖,這些都可以使用組合模式來實現。

同時要注意,因為要讓客戶端統一操作子節點和葉節點,那麼他們的抽象類就必須定義包含二者的所有方法,如果調用者調用了它們不支持的方法,可以拋出異常警告。這雖然違反了單一原則,但是在實際開發中是合理的。

好處

  • 定義了包含基本對象和組合對象的類層次結構

        基本對象可以被組合成更復雜的組合對

        象,而這個組合對象又可以被組合,這樣不斷的遞歸下去。客戶代碼中,任何用到基本

        對象的地方都可以使用組合對象。

  • 簡化客戶代碼

        客戶可以一致地使用組合結構和單個對象。通常用戶不知道 (也不關心)處理的是一個葉節點還是一個組合組件。這就簡化了客戶代碼 , 因為在定義組合的那些類中不需要寫一些充斥著選擇語句的函數。

  • 使 得 更 容 易 增 加 新 類 型 的 組 件

    新定義的 C o m p o s i t e 或 L e a f 子 類 自 動 地 與 已 有 的 結 構 和 客

    戶代碼一起工作,客戶程序不需因新的 C o m p o n e n t類而改變。

  • 使你的設計變得更加一般化

        容易增加新組件也會產生一些問題,那就是很難限制組合

        中的組件。有時你希望一個組合只能有某些特定的組件。使用 C o m p o s i t e 時,你不能依賴類型系統施加這些約束,而必須在運行時刻進行檢查。

  • 使用時機

        如果你想表示對象的部分--整體層次結構。可以選用組合模式把整體和部分統一起來,使得層次結構實現更簡單,從外部使用這個層次結構也更容易。

        如果你希望統一的使用組合結構中的所有對象,這正是組合模式提供的主要功能

Demo下載

組合模式Demo

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved