前言
脫離了代碼談設(shè)計模式,就像是脫離了業(yè)務(wù)談架構(gòu)一樣苟耻。很多時候,覺得別人代碼寫的好扶檐,是離不開合理的運(yùn)用設(shè)計模式的凶杖,本篇就重點(diǎn)用代碼講述,倍牛工程中用到的設(shè)計模式款筑,文中關(guān)于設(shè)計模式的定義來自《大話設(shè)計模式》一書智蝠。
從最常用的工廠模式說起:
工廠模式
工廠模式是我們最常用的實(shí)例化對象模式,是用工廠方法代替new操作的一種模式奈梳。
在倍牛工程中杈湾,用到的UPHKEmpty
類,在該類的.h中如下:
/*
* 默認(rèn)沒有數(shù)據(jù) 占位view
* */
+ (instancetype)defaultEmptyView;
/*
* 轉(zhuǎn)菊花樣式 占位view
* */
+ (instancetype)progressEmptyView;
/*
* 網(wǎng)絡(luò)異常 占位view
* */
+ (instancetype)netErrorEmptyViewWithHandler:(UPHKEmptyViewHandler)handler;
/**
* 通用的無網(wǎng)絡(luò)的"雷達(dá)"空白頁 點(diǎn)擊可從新加載
*/
+ (instancetype)reconnectEmptyViewWithHandler:(UPHKEmptyViewHandler)handler;
/*
* 自選空白 占位view
* */
+ (instancetype)optionalEmptyViewWithHandler:(UPHKEmptyViewHandler)handler;
工廠方法也可以是如下這個初始化的方法:
/**
* 工廠方法
*/
+ (instancetype) modelWithStartPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint;
這樣理解起來也并不困難攘须。
根據(jù)不同的場景毛秘,使用不同的工廠方法,即可阻课。
單一職責(zé)原則
先說個人理解叫挟,單一職責(zé)原則,我一直理解為限煞,在一個類中抹恳,要保證該類的功能是單一的,也就是專一署驻,該類不能既承擔(dān)A功能奋献,又承擔(dān)B功能,不然會造成功能耦合旺上,從而導(dǎo)致設(shè)計脆弱瓶蚂、擴(kuò)展性差,此原則的核心就是解耦和增強(qiáng)內(nèi)聚性宣吱。
單一職責(zé)原則的定義如下:
就一個類而言窃这,應(yīng)該僅有一個引起它發(fā)生變化的原因。
我并不能很好的理解“僅有一個引起它發(fā)生變化的原因”這句話征候。
所以在聯(lián)想到倍牛工程的時候杭攻,我覺得祟敛,數(shù)據(jù)庫的創(chuàng)建和讀取的設(shè)計,類UPHKDBHelper
和UPHKUserDBManager
兆解。UPHKDBHelper
是直接操作數(shù)據(jù)庫的類馆铁,負(fù)責(zé)UPHKUserSDK
中數(shù)據(jù)的IO操作,將讀取或者需要寫入的數(shù)據(jù)锅睛,回調(diào)或者寫入到數(shù)據(jù)庫中埠巨,但是這里的單一職責(zé)我就認(rèn)為它是功能單一,或者唯一现拒,是唯一直接操作數(shù)據(jù)庫的類乖订。
UPHKDBHelper.h
中的代碼如下,
// 數(shù)據(jù)表操作相關(guān)
- (id)initDBWithName:(NSString *)dbName;
- (id)initWithDBWithPath:(NSString *)dbPath;
...
///************************ 數(shù)據(jù)庫存取相關(guān) *****************************************
- (void)putObject:(id)object withId:(NSString *)objectId intoTable:(NSString *)tableName;
- (id)getObjectById:(NSString *)objectId fromTable:(NSString *)tableName;
....
其實(shí)UPHKDBHepler
理解為單一職責(zé)原則具练,我并不認(rèn)為合理,還需探討甜无。
我認(rèn)為在倍牛工程中比較符合單一職責(zé)設(shè)計模式的是UPHKCommDataClient
以及與它相似的類扛点。
UPHKCommDataClient.h
// 下載
- (UPHKResponse *)downloadFile:(NSString *)filePath;
/**
* 上傳圖片到服務(wù)器
*/
- (UPHKResponse *)uploadImageFile:(UIImage *)image;
UPHKCommDataClient
負(fù)責(zé)了整個工程的文件下載和圖片的上傳,職責(zé)單一岂丘,也不依賴上層邏輯陵究,做到“僅有一個引起它發(fā)生變化的原因”,在倍牛工程共還有許多與之相似的類奥帘,如UserSDK
中負(fù)責(zé)線程管理的UserServer
铜邮、負(fù)責(zé)組包的UserService
、負(fù)責(zé)底層網(wǎng)絡(luò)請求的UserClient
等等都有單一職責(zé)的應(yīng)用寨蹋。
開放封閉原則
是說軟件實(shí)體(類松蒜、模塊、函數(shù)等)已旧,應(yīng)該可以擴(kuò)展秸苗,但是不可修改。
開放封閉原則主要體現(xiàn)在兩個方面:對于擴(kuò)展是開放的运褪,對于更改是封閉的惊楼。
在倍牛及股票通工程中使用的UPHybridSDK,是嚴(yán)格開放封閉原則進(jìn)行設(shè)計的秸讹。
在此之前檀咙,我從未想過一個WebView要做這么深層次的封裝,也才知道璃诀,WebView的功能弧可,可以以插件的形式設(shè)計,對HybridSDK結(jié)構(gòu)理解還不夠深刻劣欢,只是拿來UPHybridPlugin
這個類侣诺,對開放封閉原則這一設(shè)計模式做深入理解用殖演。
UPHybridPlugin
(插件基類),在實(shí)現(xiàn)上只是將事件分發(fā)給UPHybridPluginManager
年鸳。在native需要與web頁面交互的時候趴久,execute
方法就是web調(diào)用終端的onJsRequest
時觸發(fā)的方法,action
和args
分別是需要終端處理的事件和終端需要的參數(shù)搔确。
-(BOOL)execute:(NSString *)callbackId action:(NSString *)action args:(NSDictionary *)args;
在創(chuàng)建功能插件時彼棍,必須繼承UPHybridPlugin
,并重載execute
方法膳算。在UPHybirdSDK的結(jié)構(gòu)設(shè)計完成后座硕,父類UPHybridPlugin
的execute
方法就不再更改,但是無論模塊多么的封閉涕蜂,都會存在一些無法對之封閉的變化华匾,我理解的變化,針對UPHybridPlugin
來說机隙,就是不斷增加的功能蜘拉,就需要各式各樣的功能插件,功能是隨需求不斷變化的有鹿,既然不可能完全封閉旭旭,就必須對設(shè)計的模塊應(yīng)該對那種變化封閉做出選擇,然后構(gòu)造抽象來隔離這些變化葱跋,execute
其實(shí)就是這樣抽象出來的方法持寄。
對UPHybridSDK的可以逐步的理解如下:最初寫UPHybird時,假設(shè)只有分享功能娱俺,就不存在當(dāng)前插件形式的結(jié)構(gòu)稍味,但是隨著拍照功能、頁面定制功能的增加荠卷,這時候仲闽,應(yīng)該對程序中呈現(xiàn)頻繁變化的那部分做出抽象,變化時什么僵朗?可以理解為具體的功能代碼的實(shí)現(xiàn)赖欣,但是不變的是什么?是底層Web
調(diào)用native
的結(jié)構(gòu)验庙,這時候需要抽象的部分就形成了顶吮,面對新功能,對程序的改動是通過增加新插件(新代碼)的形式進(jìn)行的(可擴(kuò)展)粪薛,而不是更改現(xiàn)有的代碼(封閉)悴了。
里氏代換原則
子類型必須能夠替換掉他們的父類型。
一個軟件實(shí)體如果使用的是一個父類的話,那么一定適用于其子類湃交,而且它察覺不出父類對象和子類對象的區(qū)別熟空。也就是說,在軟件里面搞莺,把父類都替換成它的子類息罗,程序的行為沒有變化。
我理解的里氏代換原則才沧,是開放封閉原則的根本迈喉,也就是說,正是由于子類型的可替換性才使得使用父類類型的模塊在無需修改的情況下就可以擴(kuò)展温圆。
依賴倒轉(zhuǎn)原則
高層模塊不應(yīng)該依賴于低層模塊挨摸。兩個都應(yīng)該依賴抽象。抽象不應(yīng)該依賴細(xì)節(jié)岁歉,細(xì)節(jié)應(yīng)該依賴抽象得运。
按照UserSDK和User模塊的關(guān)系,可以理解User是業(yè)務(wù)邏輯相關(guān)的锅移,屬于高層模塊熔掺,UserSDK是低層模塊,這樣的依賴關(guān)系是符合工程模塊化設(shè)計原理的帆啃。
但是我認(rèn)為把這種設(shè)計理解成依賴倒轉(zhuǎn)的原則,有一點(diǎn)不妥窍帝。按照依賴倒轉(zhuǎn)原則努潘,高層模塊不應(yīng)該依賴低層模塊,兩個都應(yīng)該依賴抽象(有些拗口)坤学。
在高層模塊中疯坤,多處調(diào)用了低層模塊提供的接口,在要做新項目時深浮,如果需求是不更改App的UI布局压怠,換另一個UserSDK直接提供數(shù)據(jù),也就意味著飞苇,高層模塊的業(yè)務(wù)邏輯完全可以復(fù)用菌瘫,但高層模塊多處調(diào)用的低層接口是不是要多處修改?
所以按照依賴倒轉(zhuǎn)的原則應(yīng)該是:
但實(shí)際開發(fā)考慮到實(shí)際情況布卡,很少這樣去設(shè)計雨让,也是為了便捷開發(fā),適合自己的才是最好的忿等。
代理模式
######為其他對象提供一種代理以控制對這個對象的訪問栖忠。
在iOS代理模式最常見的實(shí)現(xiàn)方式是協(xié)議(Protocol),協(xié)議定義了接口,無需關(guān)心代理是誰庵寞,只要遵循并實(shí)現(xiàn)協(xié)議狸相,就可以訪問到這個對象,在倍牛和股票通中的Router
是按照代理模式設(shè)計的捐川。
以Router
跳轉(zhuǎn)個股詳情為例
在RouterProtocol
中定義協(xié)議代碼如下:
@protocol UPHKMarketRouterDelegate <NSObject>
/*
進(jìn)入股票行情頁面
@param code 股票代碼
@param setCode 市場代碼
@param category 市場分類
*/
- (void)goMarketHQWithCode:(NSString *)code setCode:(NSInteger)setCode category:(UPMarketStockCategory)category;
@end
在個股詳情頁遵循并實(shí)現(xiàn)該協(xié)議
- (void)goMarketHQWithCode:(NSString *)code setCode:(NSInteger)setCode category:(UPMarketStockCategory)category
{
...
}
那么遵循協(xié)議者就是對代理者開放了一種對當(dāng)前對象的訪問權(quán)限脓鹃。
迪米特法則
如果兩個類不需要彼此直接通信,那么這兩個類就不應(yīng)當(dāng)發(fā)生直接的相互作用属拾。如果其中一個類需要調(diào)用另一個類的某一個方法的話将谊,可以通過第三者轉(zhuǎn)發(fā)這個調(diào)用。
先說我的理解渐白,在學(xué)習(xí)迪米特法則這個設(shè)計模式之前尊浓,我是經(jīng)常聽到大家這么說的,但是并不知道這其實(shí)是一種設(shè)計模式纯衍。按照之前的理解栋齿,每一個類在結(jié)構(gòu)設(shè)計上,都應(yīng)當(dāng)盡量降低成員的訪問權(quán)限襟诸,也就是說瓦堵,一個類包裝好了自己的private狀態(tài),不需要讓別的類知道的字段或行為就不要公開歌亲,它強(qiáng)調(diào)類之前的松耦合菇用,這就是該設(shè)計模式的根本思想。
迪米特法則在倍牛中有廣泛應(yīng)用陷揪,如UserSDK
中的UserServer
惋鸥,在.m
將用戶數(shù)據(jù)UserData
私有,對外只提供對應(yīng)的方法悍缠,滿足條件的情況下會在內(nèi)部對該屬性進(jìn)行管理卦绣,而非將該屬性暴露出去,我也認(rèn)為這一個類飞蚓,同時滿足了幾種設(shè)計模式滤港。
職責(zé)鏈模式
使多個對象都有機(jī)會處理請求,從而避免請求的發(fā)送者和接受者之間的耦合關(guān)系趴拧。將這個對象連成一條鏈溅漾,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止著榴。
image.png
-
職責(zé)鏈的好處
①當(dāng)提交一個請求時樟凄,請求沿鏈傳遞直至有一個Handler對象負(fù)責(zé)處理它。
②接受者和發(fā)送者都沒有對方的明確信息兄渺,且鏈中的對象自己也不知道鏈的結(jié)構(gòu)缝龄。結(jié)果是職責(zé)鏈可簡化對象的相互連接汰现,他們僅需保持一個指向其后繼者的引用,而不需要保持它所有候選接受者的引用叔壤。降低了耦合度瞎饲。
③隨時隨地增加或修改一個請求的結(jié)構(gòu),增強(qiáng)了給對象指派職責(zé)的靈活性炼绘。
-
職責(zé)鏈的應(yīng)用
在倍牛工程中嗅战,并沒有非常符合職責(zé)鏈設(shè)計模式的代碼示例,但是我依然覺的這個設(shè)計模式非常有意思俺亮。
在大話設(shè)計模式一書中驮捍,作者是用員工、經(jīng)理脚曾、總監(jiān)东且、總經(jīng)理的角色分工來舉例說明的。我舉一個iOS中的例子本讥,我認(rèn)為iOS中的響應(yīng)鏈?zhǔn)前凑章氊?zé)鏈模式進(jìn)行設(shè)計的珊泳。
當(dāng)用戶觸摸應(yīng)用中的一個Button時,觸摸生成的Event
會沿響應(yīng)鏈進(jìn)行傳遞拷沸,并找到最適合處理事件的對象色查。
UIApplication
~> UIWindow
~> UIViewController
~~>UIButton
在查找適合處理事件的對象時,系統(tǒng)會在不同的處理者自動調(diào)用pointInside
方法撞芍,如下
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
[super pointInside:point withEvent:event];
}
當(dāng)pointInside
方法返回YES的時候秧了,會將該視圖加入到UIApplication
的響應(yīng)者棧。
在iOS中響應(yīng)者都繼承自UIResponder
序无,查找當(dāng)前響應(yīng)者的下一個響應(yīng)者可以通過[self nextResponder]
獲取验毡,如果當(dāng)前響應(yīng)者不處理事件,可以將事件繼續(xù)傳遞給父類愉镰,父類再進(jìn)行分發(fā)米罚。
在響應(yīng)鏈中钧汹,滿足了一個請求可以被多個對象處理丈探,并且每個對象僅需保持一個指向其后繼者的引用的條件,所以我認(rèn)為響應(yīng)鏈就是按照職責(zé)鏈設(shè)計模式進(jìn)行設(shè)計的拔莱。
適配器模式
將一個類的接口轉(zhuǎn)換成需求需要的另外一個接口碗降。Adapter模式使得原本由于接口不兼容而不能一起工作的那些類可以一起工作。
-
應(yīng)用場景
當(dāng)數(shù)據(jù)和行為都正確塘秦,但接口不符時讼渊,我們應(yīng)該考慮使用適配器,目的是使控制范圍之外的一個原有對象和某個接口匹配尊剔。適配模式主要應(yīng)用于希望復(fù)用一些現(xiàn)存的類爪幻,但是接口又與復(fù)用環(huán)境要求不一致的情況。
-
理解
在看安卓代碼的時候,會發(fā)現(xiàn)有各式各樣的Adapter
挨稿,如下:
public class MarketAmountListAdapter extends MarketBaseRecyclerAdapter {
private final Context mContext;
private final ArrayList<UPMarketData> mDataList;
public MarketAmountListAdapter(Context context) {
mContext = context;
mDataList = new ArrayList<>();
}
但是在iOS中仇轻,很少看到專門的Adapter
,但并不代表沒有奶甘,多數(shù)時候數(shù)據(jù)的適配或處理是在數(shù)據(jù)模型中完成的篷店,所以并不需要創(chuàng)建單獨(dú)的Adapter
。
網(wǎng)絡(luò)請求回來的數(shù)據(jù)模型如UPHqStockHq
和UI控件需要顯示的數(shù)據(jù)往往有些差別臭家,如類型不匹配疲陕、字體顏色未知等,這時候就需要對改模型進(jìn)行適配.h
@interface UPHKMarketHSGTAHStockModel : NSObject
@property (nonatomic, strong) UPMarketAHStockData *ahStock;
@property (nonatomic, strong) UIImage *marketFlagImage;
...
@property (nonatomic, copy) NSString *aChangeRadio;
// Color
...
@property (nonatomic, strong) UIColor *aNowPriceColor;
@property (nonatomic, strong) UIColor *aChangeRadioColor;
@property (nonatomic, strong) UIColor *premiumRateColor;
@end
.m
@implementation UPHKMarketHSGTAHStockModel
- (UIImage *)marketFlagImage {
return UPHKImage(@"up_hk_market_hk");
}
...
- (UIColor *)hChangeRadioColor {
return [UPCompareTool compareWithData:self.ahStock.hItem.changeRatio baseData:0 precise:3];
}
- (UIColor *)premiumRateColor {
return [UPCompareTool compareWithData:self.ahStock.premiumRate baseData:0 precise:3];
}
@end
使用一個已經(jīng)存在的類钉赁,但如果它的接口蹄殃,也就是它的方法和你的要求不相同時,就應(yīng)該考慮用適配器模式橄霉。
-
討論
jayma(馬杰) 4-4 上午 9:25
我覺得對一個類或是一個模塊的設(shè)計窃爷,肯定不是一成不變的,隨著功能的增加姓蜂,需求的變化按厘,架構(gòu)和模式是一個持續(xù)修改和優(yōu)化的過程。
就現(xiàn)在來看倍牛里面還是有很多需要優(yōu)化的地方钱慢。比如UserSDK里面的模塊劃分逮京,分層結(jié)構(gòu),各個類的職責(zé)和功能束莫,用戶狀態(tài)機(jī)的管理等等懒棉,現(xiàn)在感覺實(shí)現(xiàn)有點(diǎn)冗余,邏輯有點(diǎn)混亂览绿,有很大的優(yōu)化空間策严。
還有TradeSDK對UserSDK的依賴調(diào)用,是否有更加合理的方式等等
jayma(馬杰) 4-4 上午 9:27
可以從代碼優(yōu)化的方式入手饿敲,對著各種設(shè)計模式妻导,多思考一下,這樣搞過幾輪后怀各,理解應(yīng)該會更加深入 [強(qiáng)]
....(單例模式倔韭、迭代器模式等待補(bǔ)充)