设计模式在倍牛工程中的应用

前言

脱离了代码谈设计模式,就像是脱离了业务谈架构一样。很多时候,觉得别人代码写的好,是离不开合理的运用设计模式的,本篇就重点用代码讲述,倍牛工程中用到的设计模式,文中关于设计模式的定义来自《大话设计模式》一书。
从最常用的工厂模式说起:

工厂模式

工厂模式是我们最常用的实例化对象模式,是用工厂方法代替new操作的一种模式。

在倍牛工程中,用到的UPHKEmpty类,在该类的.h中如下:

/*
 * 默认没有数据 占位view
 * */
+ (instancetype)defaultEmptyView;

/*
 * 转菊花样式 占位view
 * */
+ (instancetype)progressEmptyView;

/*
 * 网络异常 占位view
 * */
+ (instancetype)netErrorEmptyViewWithHandler:(UPHKEmptyViewHandler)handler;

/**
 *  通用的无网络的"雷达"空白页 点击可从新加载
 */
+ (instancetype)reconnectEmptyViewWithHandler:(UPHKEmptyViewHandler)handler;

/*
 * 自选空白 占位view
 * */
+ (instancetype)optionalEmptyViewWithHandler:(UPHKEmptyViewHandler)handler;

工厂方法也可以是如下这个初始化的方法:

/**
 *  工厂方法
 */
+ (instancetype) modelWithStartPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint;

这样理解起来也并不困难。

根据不同的场景,使用不同的工厂方法,即可。

单一职责原则

先说个人理解,单一职责原则,我一直理解为,在一个类中,要保证该类的功能是单一的,也就是专一,该类不能既承担A功能,又承担B功能,不然会造成功能耦合,从而导致设计脆弱、扩展性差,此原则的核心就是解耦和增强内聚性。
单一职责原则的定义如下:

就一个类而言,应该仅有一个引起它发生变化的原因。

我并不能很好的理解“仅有一个引起它发生变化的原因”这句话。
所以在联想到倍牛工程的时候,我觉得,数据库的创建和读取的设计,类UPHKDBHelperUPHKUserDBManagerUPHKDBHelper是直接操作数据库的类,负责UPHKUserSDK中数据的IO操作,将读取或者需要写入的数据,回调或者写入到数据库中,但是这里的单一职责我就认为它是功能单一,或者唯一,是唯一直接操作数据库的类。
UPHKDBHelper.h中的代码如下,

//  数据表操作相关
- (id)initDBWithName:(NSString *)dbName;

- (id)initWithDBWithPath:(NSString *)dbPath;
...

///************************ 数据库存取相关 *****************************************

- (void)putObject:(id)object withId:(NSString *)objectId intoTable:(NSString *)tableName;

- (id)getObjectById:(NSString *)objectId fromTable:(NSString *)tableName;
....

其实UPHKDBHepler理解为单一职责原则,我并不认为合理,还需探讨。
我认为在倍牛工程中比较符合单一职责设计模式的是UPHKCommDataClient以及与它相似的类。
UPHKCommDataClient.h

//  下载
- (UPHKResponse *)downloadFile:(NSString *)filePath;

/**
 * 上传图片到服务器
 */
- (UPHKResponse *)uploadImageFile:(UIImage *)image;

UPHKCommDataClient负责了整个工程的文件下载和图片的上传,职责单一,也不依赖上层逻辑,做到“仅有一个引起它发生变化的原因”,在倍牛工程共还有许多与之相似的类,如UserSDK中负责线程管理的UserServer、负责组包的UserService、负责底层网络请求的UserClient等等都有单一职责的应用。

开放封闭原则

是说软件实体(类、模块、函数等),应该可以扩展,但是不可修改。
开放封闭原则主要体现在两个方面:对于扩展是开放的,对于更改是封闭的。

在倍牛及股票通工程中使用的UPHybridSDK,是严格开放封闭原则进行设计的。


image.png

在此之前,我从未想过一个WebView要做这么深层次的封装,也才知道,WebView的功能,可以以插件的形式设计,对HybridSDK结构理解还不够深刻,只是拿来UPHybridPlugin这个类,对开放封闭原则这一设计模式做深入理解用。
UPHybridPlugin(插件基类),在实现上只是将事件分发给UPHybridPluginManager。在native需要与web页面交互的时候,execute方法就是web调用终端的onJsRequest时触发的方法,actionargs分别是需要终端处理的事件和终端需要的参数。

-(BOOL)execute:(NSString *)callbackId action:(NSString *)action args:(NSDictionary *)args;

在创建功能插件时,必须继承UPHybridPlugin,并重载execute方法。在UPHybirdSDK的结构设计完成后,父类UPHybridPluginexecute方法就不再更改,但是无论模块多么的封闭,都会存在一些无法对之封闭的变化,我理解的变化,针对UPHybridPlugin来说,就是不断增加的功能,就需要各式各样的功能插件,功能是随需求不断变化的,既然不可能完全封闭,就必须对设计的模块应该对那种变化封闭做出选择,然后构造抽象来隔离这些变化,execute其实就是这样抽象出来的方法。
对UPHybridSDK的可以逐步的理解如下:最初写UPHybird时,假设只有分享功能,就不存在当前插件形式的结构,但是随着拍照功能、页面定制功能的增加,这时候,应该对程序中呈现频繁变化的那部分做出抽象,变化时什么?可以理解为具体的功能代码的实现,但是不变的是什么?是底层Web调用native的结构,这时候需要抽象的部分就形成了,面对新功能,对程序的改动是通过增加新插件(新代码)的形式进行的(可扩展),而不是更改现有的代码(封闭)。

里氏代换原则

子类型必须能够替换掉他们的父类型。

一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化。
我理解的里氏代换原则,是开放封闭原则的根本,也就是说,正是由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展。

依赖倒转原则

高层模块不应该依赖于低层模块。两个都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。

按照UserSDK和User模块的关系,可以理解User是业务逻辑相关的,属于高层模块,UserSDK是低层模块,这样的依赖关系是符合工程模块化设计原理的。


image.png

但是我认为把这种设计理解成依赖倒转的原则,有一点不妥。按照依赖倒转原则,高层模块不应该依赖低层模块,两个都应该依赖抽象(有些拗口)。
在高层模块中,多处调用了低层模块提供的接口,在要做新项目时,如果需求是不更改App的UI布局,换另一个UserSDK直接提供数据,也就意味着,高层模块的业务逻辑完全可以复用,但高层模块多处调用的低层接口是不是要多处修改?

所以按照依赖倒转的原则应该是:


image.png

但实际开发考虑到实际情况,很少这样去设计,也是为了便捷开发,适合自己的才是最好的。

代理模式

######为其他对象提供一种代理以控制对这个对象的访问。
在iOS代理模式最常见的实现方式是协议(Protocol),协议定义了接口,无需关心代理是谁,只要遵循并实现协议,就可以访问到这个对象,在倍牛和股票通中的Router是按照代理模式设计的。
Router跳转个股详情为例
RouterProtocol中定义协议代码如下:

@protocol UPHKMarketRouterDelegate <NSObject>
/*
 进入股票行情页面
 
 @param code 股票代码
 @param setCode 市场代码
 @param category 市场分类
 */
- (void)goMarketHQWithCode:(NSString *)code setCode:(NSInteger)setCode category:(UPMarketStockCategory)category;
@end

在个股详情页遵循并实现该协议

- (void)goMarketHQWithCode:(NSString *)code setCode:(NSInteger)setCode category:(UPMarketStockCategory)category
{
  ...
}

那么遵循协议者就是对代理者开放了一种对当前对象的访问权限。

迪米特法则

如果两个类不需要彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

先说我的理解,在学习迪米特法则这个设计模式之前,我是经常听到大家这么说的,但是并不知道这其实是一种设计模式。按照之前的理解,每一个类在结构设计上,都应当尽量降低成员的访问权限,也就是说,一个类包装好了自己的private状态,不需要让别的类知道的字段或行为就不要公开,它强调类之前的松耦合,这就是该设计模式的根本思想。

迪米特法则在倍牛中有广泛应用,如UserSDK中的UserServer,在.m将用户数据UserData私有,对外只提供对应的方法,满足条件的情况下会在内部对该属性进行管理,而非将该属性暴露出去,我也认为这一个类,同时满足了几种设计模式。

职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
image.png
  • 职责链的好处

①当提交一个请求时,请求沿链传递直至有一个Handler对象负责处理它。
②接受者和发送者都没有对方的明确信息,且链中的对象自己也不知道链的结构。结果是职责链可简化对象的相互连接,他们仅需保持一个指向其后继者的引用,而不需要保持它所有候选接受者的引用。降低了耦合度。
③随时随地增加或修改一个请求的结构,增强了给对象指派职责的灵活性。

  • 职责链的应用

在倍牛工程中,并没有非常符合职责链设计模式的代码示例,但是我依然觉的这个设计模式非常有意思。
在大话设计模式一书中,作者是用员工、经理、总监、总经理的角色分工来举例说明的。我举一个iOS中的例子,我认为iOS中的响应链是按照职责链模式进行设计的。
当用户触摸应用中的一个Button时,触摸生成的Event会沿响应链进行传递,并找到最适合处理事件的对象。
UIApplication ~> UIWindow ~> UIViewController~~>UIButton
在查找适合处理事件的对象时,系统会在不同的处理者自动调用pointInside方法,如下

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    [super pointInside:point withEvent:event];
}

pointInside方法返回YES的时候,会将该视图加入到UIApplication的响应者栈。
在iOS中响应者都继承自UIResponder,查找当前响应者的下一个响应者可以通过[self nextResponder]获取,如果当前响应者不处理事件,可以将事件继续传递给父类,父类再进行分发。
在响应链中,满足了一个请求可以被多个对象处理,并且每个对象仅需保持一个指向其后继者的引用的条件,所以我认为响应链就是按照职责链设计模式进行设计的。

适配器模式

将一个类的接口转换成需求需要的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
  • 应用场景

当数据和行为都正确,但接口不符时,我们应该考虑使用适配器,目的是使控制范围之外的一个原有对象和某个接口匹配。适配模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况。

  • 理解

在看安卓代码的时候,会发现有各式各样的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,但并不代表没有,多数时候数据的适配或处理是在数据模型中完成的,所以并不需要创建单独的Adapter
网络请求回来的数据模型如UPHqStockHq和UI控件需要显示的数据往往有些差别,如类型不匹配、字体颜色未知等,这时候就需要对改模型进行适配.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
使用一个已经存在的类,但如果它的接口,也就是它的方法和你的要求不相同时,就应该考虑用适配器模式。
  • 讨论

jayma(马杰) 4-4 上午 9:25
我觉得对一个类或是一个模块的设计,肯定不是一成不变的,随着功能的增加,需求的变化,架构和模式是一个持续修改和优化的过程。
就现在来看倍牛里面还是有很多需要优化的地方。比如UserSDK里面的模块划分,分层结构,各个类的职责和功能,用户状态机的管理等等,现在感觉实现有点冗余,逻辑有点混乱,有很大的优化空间。
还有TradeSDK对UserSDK的依赖调用,是否有更加合理的方式等等

jayma(马杰) 4-4 上午 9:27
可以从代码优化的方式入手,对着各种设计模式,多思考一下,这样搞过几轮后,理解应该会更加深入 [强]

....(单例模式、迭代器模式等待补充)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 本文首发于个人博客:Lam's Blog - 谈谈23种设计模式在Android源码及项目中的应用,文章由Mark...
    格子林ll阅读 4,638评论 1 105
  • 设计模式基本原则 开放-封闭原则(OCP),是说软件实体(类、模块、函数等等)应该可以拓展,但是不可修改。开-闭原...
    西山薄凉阅读 3,779评论 3 14
  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,929评论 1 15
  • 大家好,给大家介绍一下: 我的大名:苹果 我的品种:红富士 来自陕北红色革命圣地延安 我的特点及优点:从小套袋、不...
    采蘑菇的佳怡07阅读 422评论 1 1
  • 走着走着, 天就亮了。 走着走着, 花就开了。 走着走着, 就回到了童年。 走着走着, 就遇到了自己。 走着走着,...
    Bernardxiao阅读 241评论 1 4