设计模式:工厂方法--Objective-C实现

前言

工厂模式应用非常广泛,经常可以在一些复杂的应用或框架上看见其影子.正所谓

没见过猪跑,但我吃过猪肉.

你可以在NSString看到工厂方法.尝试执行以下代码:

NSString *str = @"string 1";
NSLog(@"string 1 class : %@", [str class]);
NSString *str2 = [NSString stringWithUTF8String:"string 2"];
NSLog(@"string 2 class :%@", [str2 class]);

可以在控制台中看到以下日志:

string 1 class : __NSCFConstantString
string 2 class :NSTaggedPointerString

很明显,在分别调用不同的类方法生成了两个继承NSString的子类.在OC中,如果你看到一个类有着丰富多样的类方法来生成对象,那么它就有可能使用工厂方法.在上一次面试中,面试官问到了这个问题.我发现自己表达得很烂.as we know:

如果你不能将知识通过简洁的语言表达出来,那说明你还没掌握这个知识.

分类

一般来说,分为三个类:简单工厂,工厂方法,抽象工厂.由于简单工厂和工厂方法类似,就放在本篇讲,而工厂方法在下一篇介绍.
他们在四人帮中(Gang of four)的定义如下:

  • 简单工厂 -- 定义一个用于创建对象的没有暴露的接口,让子类决定哪一个类.
  • 工厂方法 -- 定义一个用于创建对象的接口,让子类决定哪一个类.Factory Method使一个类实例化延迟到其子类.
  • 抽象工厂 -- 提供一个创建一系列相关或相互依赖对象的接口,而无需指定他们具体的类.

注:'让子类决定'的意思是在编码时无法确定具体实例化哪一个类,需在运行时(runtime)通过交互或者通过参数决定实例化哪一个类.

一个例子

假如现在我们有这么一个需求:
公司正在做一个商城APP叫Bao Si APP,简称BS.在BS中,当用户未完善个人信息的时候,下单需要跳转到完善个人信息界面中完善个人信息.
这个信息界面是一个表单,包括姓名地址,性别,手机号码,当前银行卡之类.这个应该是比较简单的,所以你就开始动手写了.当你写到一半的时候,PM跑过来跟你说:"之后我们继续讨论了一下,你把这个表单改为动态的.现在的表单比较简单,以后我们需要添加其他的表单信息,比如说生日.有些信息可能变得很重要,所以这个表单是由后台的运营人员控制的.只要后台一更改,前台就得跟着改变."
这时候,你应该放下你手中的刀,并坐下来听一首<The Wolven Storm>,慢慢思考人生.
我们可以将上述的需求总结:

  1. 动态性.客户端的配置应该由后台配置,我们通过这个配置来生成对应的表单.每一个表单的控件类型都由一个唯一标识符type控制.让type决定创建对应的控件对象.即我们不知道我们要创建的具体的类.
  2. 多态性.每一个表单控件中都有相同的标题,以及类似的方法:getValue,isValidate(为了简单起见,假设这里就只有两个方法).每个控件的getValue方法又都不同.对于地址,需要返回Label.text中的值.对于日期,则需要textfield.text. 将一些责任(方法)委托给子类实现.

基本上符合这两个点的时候需求的时候,就能使用简单工厂模式了.

简单工厂模式

按照工厂模式,因为我们不知道所要创建的具体的类,所以希望通过一个工厂类来帮我们生成对应的产品类,所以在客户端中调用工厂方法中的类方法controlWithType:并传入一个参数.这时候就需要知道具体什么方法,只关心从控件中取出来的值.
于是可以得到以下的UML结构图.

simple factory
simple factory

注:在当前的需求中,每一个控件都有一个公共属性titleLabel,这个属性明显不属于抽象产品类.所以为了灵活起见,定义了一个基类继承抽象产品类ControlProtocol.这样,子类也就有了titleLabel,子类也能继承对应的方法.

简单工厂有两种方法实现:

  1. 参数化方法. 即在工厂类里面写一个带type类型的生产方法,在这个方法中写判断逻辑.

    @interface AKFactory : NSObject
    - (AKBasicControl *)controlWithType:(NSString *)type;
    @end
    
  2. 为每一个产品类写一个生产方法.这种方法将原本的判断逻辑延迟到了客户端实现.

    @interface AKFactory : NSObject
    - (AKBasicControl *)TextField;
    - (AKBasicControl *)Button;
    @end
    

当我们从后台获取配置的时候,就可以使用工厂方法生成我们所需要的控件.在这个例子中,我们使用的是静态配置来生成一个动态表单.客户端通过调用工厂方法生成相应的控件并展示在动态表单上面.这里我们不需要知道他们具体的类型是什么,只需要按照要求检查数据的合法性与获取数据即可.
详细的代码我已经托管到了github上面:FactoryMethodExample.

工厂方法模式

当我们写好并投入使用时.果然,PM又跑过来说:"这个表单的控件种类太少了吧.再添加个开关控件,就加一个而已,很简单的".你听了之后内心毫无波动,甚至还想笑.好吧,看个萝莉思考一下.


the loli
the loli

首先,很容易想到一个解决方案,那就是修改AKFactory类的类方法controlWithType:, 在这个方法中添加如类似一下的代码:

if ([type isEqualToString:@"radio"]) {
  return [[AKRadio alloc] init];
}

这样子确实可以快速解决问题.不过这也太low了,我们学习设计模式除了要解决一些复杂问题之外,还有一个目的就是装逼.这样子写会被高手鄙视的.但是话说回来.这样子写其实是有问题的,因为当一个类完成之后,在不到万不得已的情况下,我们不应该去修改它.这就是开闭原则(Open/closed principle).

Software entities should be open for extension,but closed for modification. -- Object Oriented Software Construction

翻译过来就是:“软件实体应当对扩展开放,对修改关闭."
对运行良好的类,我们不应该去修改它.在改变代码之后,不仅维护变得更复杂了,而且无法知道之前的功能是否会失效.所以干脆禁止修改,以免触发隐藏Bug.所以我们需要将原来的简单工厂模式代码,并使其容易扩展.
由于需要扩展,那我们将不能委托AKFactory生成产品,而是通过继承抽象(协议)工厂中的构造方法来为每一个产品类定制单独的构造方法.
可以得到以下UML图:

Factory method
Factory method

继承AKFactory并重载+ makeControl得到AKDateFactoryAKTextFieldFactory.在客户端调用其具体工厂类生成对应的对象.例如:在客户端调用AKTextFieldFactory类中的+ makeControl生成AKLabel类.但是,不像简单工厂,判断控件类型的逻辑需要我们在客户端实现.
现在,我们要扩展一个新的控件就简单多了.只不要继承AKFactoryAKSwitchFactory,并重载+ makeControl.然后,只需要在客户端中使用就行了.这样既不修改原来的类,也能扩展新功能.终于可以好好装逼了.
具体代码在FactoryMethodExamplefactory method分支下.

总结

当无法确定需要创建的具体的类以及这些类具有相似的方法时,就能使用简单工厂模式或工厂方法模式.工厂方法定义了一个接口,并让子类决定生成哪个子类,将类的实例化延迟到子类中,解决编程中无法确定具体类的问题.
简单工厂与工厂方法之间的区别是将原来的判断逻辑转移到转移到其他文件实现. 而产品类却都是一样.在实际的项目中,假设只需要一些静态的几个产品类,如VIP与非VIP两种情况,使用简单工厂即可.但假如遇到当前这个例子这用需求,就应该使用工厂方法模式.

此外,如果需求更加复杂,可能需要用到抽象工厂,请看我下一篇文章设计模式:抽象工厂--Objective-C实现

引用文章

Factory method pattern

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • 设计模式基本原则 开放-封闭原则(OCP),是说软件实体(类、模块、函数等等)应该可以拓展,但是不可修改。开-闭原...
    西山薄凉阅读 3,748评论 3 13
  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,894评论 1 15
  • Iterator模式 (迭代器) 一个一个遍历 一个集合类可以遵守 Iterator 协议,并实现一个 Itera...
    SSBun阅读 1,824评论 0 15
  • 01 林梦突然在朋友圈更新了一条动态:“我们的五年,敌不过你和她的五个月。”文字后面加上了3个心碎的表情,表明自己...
    洋芋丝丝阅读 4,063评论 71 73