提到工厂,会不会马上想到了下图呢?工厂模式的职能确实与现实中的工厂很像,都是用来生产东西的,不同的是,工厂模式是用来生产对象的。下面进入正文:
本篇内容分四部分:
1.思考问题
2.深度自省、反问
3.工厂方法模式
3.1 定义
3.2 案例
4.抽象工厂模式
4.1 定义
4.2 案例
5.结尾
1.思考问题
我们强调要面向接口编程而不是实现,这是因为面向实现的编程会让代码更脆弱,不易扩展,很难做到对扩展开放,对修改关闭。
那swift里直接调用init方法创建对象不就是面向实现编程吗?
确实是,调用init方法就会对具体的类型依赖,导致脆弱的代码。工厂模式就是用来解决这个问题的,所有的工厂模式都是用来封装对象的创建过程。
2.深度自省、反问
1 调用init方法创建对象就是面向实现编程,是不是意味着我们要尽全力在每一处避免直接创建对象?
2 面向接口编程是否意味着要彻底消除实现代码?
3 若对2的答案是否定的,实现代码都放哪了?这么费劲面向接口编程到底为了什么?
作者自己对这几个问题的感悟会在文尾给出。
3.工厂方法模式
3.1 定义
定义了创建对象的接口,但由实现类(原文为子类,文中以java抽象类为例,由于swift没有抽象类,只能用协议代替,因此不能叫子类)决定要创建哪些实例。工厂方法让类把实例化推迟到实现类。
注意:上面的定义是正规的使用方法,当然你可以变通,定义接口的也不一定必须是协议,可以是具体类,但是由于swift中具体类的方法必须要有实现,不能只是声明。所以此时你必须想办法为工厂方法提供一个空实现,子类必须override此方法提供合适的创建对象过程。
头脑风暴:(同上,将父类理解为协议,子类理解为实现类)
工厂方法模式与前面提到的策略模式、代理模式是否相似?它们之间的异同点在哪里?
策略模式、代理模式都是通过组合的方式扩展现有类的功能,它们将其他类对象用一个属性引用,然后调用其方法来为自己所用。工厂方法模式并没有使用组合,取而代之的是使用了继承。你可以看做它将一部分功能代理给了自己的子类,由子类决定到底该做什么。之所以名字里有工厂二字全因它代理出去的功能主要是创建对象。事实上,不一定非得是创建对象,其他功能也可以通过这种方式代理给子类,之所以没人提是因为太简单,因为继承机制天然支持父类将行为代理给子类,别忘了父类的行为子类可以轻松override。
注意:工厂方法模式重点不在将创建对象这个行为代理给子类,而是通过这个方式,父类中依赖具体对象类型的方法将可以使用一个接口类型代替自己依赖的对象类型(即面向接口编程),从而将这些方法从具体实现中解脱出来,以达到复用的目的。而实现该接口的具体对象只有子类才能知道,也就自然应该代理给子类创建了。
ps: 突然想起来一些话与上面说的很像:
1. 高内聚低耦合(合理的划分责任,每个类只做自己该做的事,这样才能高度内敛)
2. 小即是美。让程序只做好一件事。(unix哲学)
很多时候,高度的耦合是由于责任划分不当,把不该干的事情都干了。
3.2 案例
某一饭馆准备在全国开连锁店,现在重庆与北京已经开设了分店,由于快速扩张,决定要开发一套订单处理系统来自动化处理流程。注意:由于各地人的口味不一样,同一样菜在各地都进行了本地化,味道食材都变了。
现在就来开始设计吧:
1. 首先想到的是我们需要一个饭馆类来表示各地饭馆,每个饭馆都有许多相同点,因此它们都实现了相同的接口。对了,我们想让每个饭馆尽量复用一些基本操作,比如处理订单流程。但是因为每个菜处理不太一样,我们不想针对具体菜编写代码,因此我们需要定义一个菜品接口,然后针对这个接口编程。
接口都已经定义好了,我们不需要担心具体的菜品啦。下面看看接口的order方法如何针对抽象的菜品编写吧。
看到了吗?我们使用工厂方法createFood(name: )来创建了一个菜品,并且调用这个菜品的方法来准备订单。 这个菜品到底是什么,只有饭馆的实现类才能知道。我们做到了对接口编程,无论以后添加了什么菜品,我们的order方法都不需要修改。
2.接下来看看北京与重庆的饭馆如何实现吧
可以看到,我们具体饭馆的实现类里返回了自己风味的菜品。接下来看看我们菜品如何实现:
快来使用我们实现的订单系统吧!
看看北京和重庆的分店点同一份菜的结果:
4.抽象工厂模式
4.1 定义
提供一个接口用来创建相关或依赖对象的家族,而不需要明确指定的类型。
两个工厂模式的区别:
工厂方法模式通过继承关系决定如何创建对象,而抽象工厂模式通过组合的方式决定如何创建对象。
这个不同点导致了截然不同的效果,组合的方式可以在运行时动态修改,也即使用时决定如何创建,而继承则是静态决定的,不够灵活,复用性较差。
两种工厂模式如何选择?
当实现类不能决定唯一决定如何创建依赖对象时,采用组合的方式将能得到更大的复用,另外抽象工厂一般用来封装创建一系列对象。当子类唯一决定如何创建依赖对象且创建对象很少时,采用工厂方法更简单直接。
4.2 案例
还是用上面案例,这次饭馆为了保证菜品的质量,决定对食材采购严格把关,不能让当地的饭馆随便以次充好,败坏了招牌。饭馆决定与当地的生产商达成固定合作关系。由于地域区别,食材不可能做到完全一致。比如重庆的饭馆,鸡肉是由农户提供家养鸡,而北京由于家养鸡太少,采用跑步鸡(跑步鸡虽然是养鸡场生产的,但是为了肉质鲜美,每天要求鸡跑够一定步数)。
下面是食材生产商接口及实现类(仅以鸡肉为例):
1.首先我们要创建供应商来提供食材(鸡肉),我们希望通过不同地域的高质量供应商提供各地最高品质的食材,但是我们想在使用时不涉及具体供应商。所以我们创建了一个抽象供应商MaterialProvider,并且让各地供应商实现它返回各自能提高的最高品质食材。
在抽象工厂MaterialProvider中使用了工厂方法返回了一个抽象食材。工厂方法?没听错,抽象工厂内部确实使用了工厂方法来封装创建具体类型。
2.接下来我们修改菜品接口及具体类,将供应商组合进去。
其他菜品同理。
3.下面看每个餐馆如何使用吧:
同理其他餐馆,打印结果如下:
5.结尾
终于到了结尾,太不容易了,💪💪💪。
针对第2部分提出的几个问题,下面就一起回答了。
使用设计模式是为了让代码更富有弹性,能轻松适应变化。而一个系统一般不可能每个地方都经常变化,至少限定在某一段时空是这样的。我们一般是寻找系统中几处容易变化的部分使用设计模式进行设计以达到易于修改,避免牵一发而动全身。若是所有的地方都这样处理是一种极大的浪费。
面向接口编程并不是要将所有的代码抽象化,那是不可能的。世界是具体的,不是抽象的,抽象是概念,是对现实世界的高度概括,是人为的。无论是物理世界还是面向对象的世界(面向对象世界是对现实世界的建模)都是具体事物构成。
无论怎样抽象,终归是要创建具体的对象,否则世界将空无一物。举个不恰当的例子,接口就像现实世界的法律、道德标准,每个人(对象)遵守这些统一的规则(统一的接口)才能正常有效交往,当每个人遵守这些规则时,虽然看上去受到了约束,事实上每个人获得了更大的自由发展空间(因为交互对象之间遵守了某个接口而彼此之间可以解耦)。
设计模式的奥义:
通过接口解耦交互对象的依赖关系,通过抽取变化部分加以封装,通过接口暴露其功能,从而将易于变化的部分与系统其它部分隔离,达到降低影响范围,易于扩展的目的。