猫粮公司的诞生
陀螺是个程序喵,另起炉灶自己开了公司,为了纪念曾经码梦为生的岁月,公司起名为“跑码场”,主要业务是生产猫粮。
一个喵兼顾着研发和运营,终究不是长久之计。于是雇了一个菜喵做学徒,技术怎么样并不在意,陀螺最看重的是菜喵的名字—招财。
很快,第一款产品「鱼香猫粮」上线,陀螺让招财写个线上订单系统,方便顾客网上下单
招财很快写出了代码
测试之后上线,一直运行正常。
过了一段时间,陀螺对招财说:“公司目前正在研发一款牛肉猫粮,并且预计在接下来一段时间会上线「薄荷猫粮」、「鸡肉猫粮」等多款新品,你升级一下订单系统应对一下未来可能发生的改变。”
招财接到任务,重构了原来的代码,首先创建了抽象的CatFood
,之后所有具体口味的猫粮必须继承该类
接下来依次是各种口味的猫粮对象
最后是下单的逻辑
招财迫不及待地向陀螺展示自己的代码,并介绍到:“老板,我的代码已经能够满足未来的动态变化了,如果再有新口味的产品,只需要创建该产品的对象,然后修改一下order()
方法就好了!”
陀螺赞赏地点点头,“看得出来你经过了自己认真的思考,这一点非常好!但是别着急,你有没有听说过开闭原则?”
“开闭原则?听说过,但是仅仅停留在概念上,我记得好像是‘对修改关闭,对扩展开放’,当时为了面试背的还是挺熟的,哈哈哈”
“那你对照开闭原则再看一下你的代码,你觉得你的代码有什么问题?”,陀螺问道。
招财赶紧仔细审视了一下自己的代码,"我知道了,现在的问题是一旦有新产品上线,就需要改动orde()
方法,这就是所谓的没有对修改关闭吧,但是有了新的产品你总得有个地方把他new
出来啊,这一步是无论如何都无法省略的,我觉得目前的代码是能够满足需求的。"
“你说的没错,设计原则并不是金科玉律,比如未来如果只有零星几个的新口味产品上线的话,你确实没有必要改变现在的代码结构,简单的修改一下order()
就可以了,根本不用在意对修改关闭的这种约束。但是你有必要思考一下,如果后期我们研发了数十种乃至上百种产品,这种情况下你该怎么做?”
“除了修改order()
方法,我实在没有想出其他的办法...”,招财挠着脑袋回答道。
陀螺不急不慢地解释说:“这种时候,我们可以先识别出代码中哪些是经常变化的部分,然后考虑使用封装
,很明显,order()
方法中创建对象的部分就是经常需要变化的,我们可以将封装,使其专门用于创造对象。”
陀螺解释说:“如此一来,我们完成了封装的操作,把生成对象的操作集中在了SimpleCatFoodFactory
中。”
招财立即提出了自己的疑问:“我不理解这样做有什么好处,在我看来这只是把一个问题搬到了一个对象里罢了,问题本身依然存在!”
“就创建的过程而言,你说的确实没错。”,陀螺点点头,“但是,我们仍然得到了很多益处,现在我们的SimpleCatFoodFactory
不仅仅可以被order()
方法使用了,之后的任何相关逻辑都可以调用我们写的这个类,而且如果后续需要改变,我们也仅仅需要改变这个单独的类就可以了”。
招财无奈地回应说,“好吧,你的话确实很有道理,把经常变动的部分提取出来是个不错的代码优化习惯。对了,刚才这种优化技巧有名字吗?”
“这种叫简单工厂,很多开发人员都误以为它是一种设计模式了,但是它其实并不属于GoF23种设计模式,但是由于用的人太多,经常把它和工厂模式一起介绍。至于是不是设计模式,对我们而言并不重要。”
简单工厂并不是一种设计模式,更像是一种编程的优化习惯,用来将对象的创建过程和客户端程序进行解耦
招财并不放弃,继续追问,“那能不能有个办法再优化一下创建对象的过程呢,它现在依然没有满足开闭原则!而且客户端的调用方式非常不优雅,万一参数不小心拼错了,直接就崩了,这种麻烦不应该转嫁到客户端不是吗?”
陀螺愣了愣,久久盯着招财,仿佛看到了当年自己刚学习编程的样子,对一切充满好奇,对代码又有点洁癖,欣慰地说道:“说得好啊,那我们尝试利用反射继续优化一下吧。”
客户端的代码优化如下
“到此SimpleCatFoodFactoryV2
就符合了开闭原则,但是这里利用反射的一个基本原则是所有对象的构造方法必须保持一致,如果对象创建的过程比较复杂而且各有特点,那么优化到这一步或许并不是最好的选择,记住优化的原则——合适就好”,陀螺补充道。
招财对陀螺的这一番优化和解说佩服不已,心想实习遇到这么个好老板好师傅,平时还能试吃自己最爱的猫粮,这简直就是在天堂啊。
猫粮公司的扩张
日子一天天过去,公司在陀螺的运营下经营有成,计划在全国各地建立分公司。为了保证服务质量,陀螺希望各个分公司能够使用他们经过时间考验的代码。
但是不同的分公司需要根据当地特色生产不同口味的产品,比如山东生产「葱香猫粮」、「大酱猫粮」,湖南生产「辣子猫粮」、「剁椒猫粮」...
招财心想,这不简单嘛!继续利用SimpleCatFoodFactoryV2
,让各个公司的新款猫粮继承CatFood
不就可以了嘛!
但是转念一想,随着每个分公司的产品链的丰富,获取产品的创建过程会有差异,那么SimpleCatFoodFactoryV2
的职责会变得越来越多,像一个万能的类,不方便维护。
招财想到可以为每个分公司创建独立的简单工厂,然后将具体的简单工厂对象绑定到PaoMaChang
对象中,顾客下单的时候只要指定对应的分公司的工厂和口味就可以进行下单了。
PaoMaChangV3
重构如下
将工厂本身也做了个抽象,创建ICatFoodFactory
接口
各分公司的工厂代码
各种口味的猫粮代码如下
产品类对应的UML图为
顾客下单「湖南分公司」的「剁椒猫粮」的代码就变成了这样
到此,招财重构完了代码,经过细心检查系统终于上线了,各地分公司使用这套系统有条不紊地开展起自己的业务,形势一片大好!
之后的某一天,招财接到陀螺的电话,让他火速前往陀螺的办公室,招财一路战战兢兢,一直在想是不是自己的代码出了问题。来到办公室,陀螺招呼招财来到他旁边坐着,指着满屏的代码说道:“别害怕,你的代码到目前为止没有出什么bug。你为每一个分公司单独创建自己的简单工厂,又把简单工厂对象作为参数注入到了PaoMaChang
类中,能看得出来你最近没少在代码上下功夫。只是我在审查各分公司代码的时候发现一个潜在的隐患。”说罢,打开了某分公司的代码给招财看。
招才看到,湖南分公司的技术人员在order()
方法中擅自添加了一个pack()
打包的方法,陀螺继续说道:“先不管这个逻辑加的对不对,光是分公司能够改动我们的核心代码这一点就是有风险的,你需要想个办法,既能让每个分公司*创建产品,又能保证我们的核心功能不被改变,核心逻辑只能由我们来定。”
“确实是个问题,目前各个分公司的下单逻辑都是自己定义的,我们需要提供一个真正的“框架”,让他们按照我们的标准来进行业务逻辑。”
“没错!”,陀螺欣慰地看着招财。
“既然如此,我可以把我们的PaoMaChangV3
改成抽象的,命名为PaoMaChangV4
吧,让各个子公司继承这个类,然后为order()
添加final
关键字,禁止子类进行覆写,这样他们便只能用我们的下单逻辑了”,招财一遍思考一边说。
“那你打算怎么让子公司能*控制各种产品呢?”,陀螺问道。
招财不慌不忙地回答:“我最近又研究了一下多态和继承,order()
方法中的create()
方法不做具体操作,将该方法延迟到子类中进行执行。”说罢,招财立刻写了如下代码。
"order()
方法只是调用了create()
方法而已,是由子公司创建的子类负责具体实现create()
方法,湖南分公司和山东分公司对应的代码如下",招财接着解释道。
对应的UML图为
最终顾客的下单方式变成了
“看来真是要对你刮目相看了,你刚刚总结出来的这种思想其实就是大名鼎鼎的工厂方法模式”,陀螺满意地笑了,“工厂方法模式通过让子类决定该创建的对象是什么,来达到将对象创建的过程封装的目的。”
工厂方法模式:定义一个创建对象的接口,担忧子类决定要实例化的类是哪一个,将类的实例化推迟到了子类。
“啊!”,招财大惊,没想到自己误打误撞研究出了工厂方法模式,“我其实并没有想这么多,只是单纯想解决当下的问题,适应未来的变化而已。”
“我知道,恐怕现在让你总结什么时候该用简单工厂模式,什么时候该用工厂方法模式你也未必说的准确。设计模式也不过是前人不断优化自己的代码总结出来的方法论。不必拘泥于你的优化方式叫什么名字,或者干脆忘掉我刚才说的术语吧,在合适的时机运用合适的方法来解决问题才是最重要的!不要学习了设计模式,就觉得自己手上握着锤子,然后看什么都是钉子。”
“我明白了师傅!但是我听说还有一种关于工厂的设计模式,你要不顺便给我讲讲吧。”
猫粮原材料的工厂
“还有一种叫抽象工厂模式,如果你明白了我们系统的一步步优化,这个模式对你来说就太简单了。还是用我们公司的场景给你举例子吧。”
“假如我们想进一步控制分公司生产猫粮的原料,避免每个分公司的原料质量参差不齐。制作猫粮的主要原料都是一样的,都需要肉、燕麦、果蔬、牛磺酸等,但是不同的分公司又有不同的原料生产工艺,抽象工厂就适合于这种场景。”
“那该怎么进行设计呢?”
“这个简单啊,我们可以为每一个分公司创建一个原料工厂,这个原料工厂必须符合我们制定的标准,像这样”,招财写下了伪代码。
"各分公司自己的原料厂必须实现CatFoodIngredientFactory
来实现每一个创造方法,以山东分公司为例。"
注:代码中有很多类未给出实现,大家只需理解其中的含义即可
招财继续问道:“现在怎么把各个分公司的原料工厂和猫粮联系起来呢?”
“别急,为了更好的解释抽象工厂,我们需要先改变一下我们的CatFood
类。这里只是为了单纯讲解抽象工厂模式而进行的更改,和我们自身的业务逻辑已经没有关系了。”
“接下来的重点就是如何创建具体口味的猫粮了。你觉得怎么让猫粮和原料厂关联起来呢?”
“可以在子类中添加一个原料工厂的对象,猫粮产品对象的时候可以选择某个原料厂进行初始化,这样就实现了猫粮和具体原料之间的解耦,猫粮类只需要知道怎么制作就可以了,比如像这个样子。”
“孺子可教”,陀螺欣慰地说道,“你已经掌握的面向对象的精髓了,那么分公司的代码你也可以写出来了,试试看吧。”
招财很快写出了代码。
“到此为止,我们就用抽象工厂模式完成了业务的改造,顾客下单的逻辑并没有发生变化。为了完整性,我们给出抽象工厂的定义”,陀螺说道。
抽象工厂模式:提供接口,用来创建相关或依赖对象的家族,而不需要明确制定具体类。
招财郁闷地说:“你让我自己写我觉得自己能写出来,你解释这么多,我反而头大了!”
“哈哈哈哈哈哈,学习有三种境界,第一种:看山是山,看水是水;第二种:看山不是山,看水不是水;第三种:看山依然山,看水依然水。你现在就处于第一种向第二种过度的阶段”,陀螺打趣道。
“我们从头捋一遍我们系统升级的过程,帮助你理解。”
总结
“刚开始我们公司只生产一种产品——鱼香猫粮,这时你直接针对该产品创建类FishCatFood
进行业务逻辑编写即可,不需要进行任何优化。”
“后来公司相继生产了其他两种产品,鉴于每种产品产品的相关性,你创建了CatFood
抽象类,之后生产的每种产品都需要继承这个类,然后在order()
方法中根据用户传入的口味制作相应的产品。但是随着公司的发展,产品可能会一改再改(急剧增加或下架),order()
方法不再满足开闭原则,因此我们将创建对象的代码抽离到SimpleCatFoodFactory
中进行统一管理,这就是简单工厂。”
“后来公司相继在其他省份创建了子公司,每个子公司都有自己的产品,为了避免SimpleCatFoodFactory
成为万能工厂,我们为每个分公司创建了独立的简单工厂,按照我们的要求来创建产品对象。”
“我们并不想让子公司能够修改order()
的中的逻辑,因此我们试图创建一个‘框架’,强制让子公司使用我们的下单逻辑,同时又保证子公司*创建产品的灵活性。于是我们在PaoMaChangV4
抽象类中使用了抽象的create()
方法,我们将实现create()
的行为延迟到子类中,父类中制定了基本框架。这一步使得order()
不依赖于具体类,换句话说,这就是解耦。当order()
方法调用create()
方法是,PaoMaChangV4
的子类(子公司对象)将负责创建真正的产品。这就是工厂方法模式。”
“最后我们想确保对每个子公司每个产品原料的控制,定义了原料族。这里有一个隐含的假设,每个产品所使用的原料都是相同的,区别是生产方式不同。”
“我们创建了原料工厂CatFoodIngredientAbstractFactory
接口,该接口定义了创建所有原料的接口,再看一下代码。”
"接下来我们为每个分公司创建了实现了CatFoodIngredientAbstractFactory
接口的子类来实现每一个创建方法。为了更恰当地解释抽象工厂模式,我们又稍微改造了一下猫粮类,得到了CatFoodV2
,所有的具体产品依然继承自CatFoodV2
,不同的每个产品都需要从构造器中得到一个原料工厂,注入到对象中的catFoodIngredientFactory
变量,CatFoodV2
中的make()
方法会使用到该工厂创建的原料。"
“最后总结一下抽象工厂模式的使用场景,当你需要使用原料家族来创建想要制造的产品的时候,你就可以考虑使用抽象工厂模式了。”
我是蝉沐风,一个让你沉迷于技术的讲述者,欢迎大家留言!