工厂设计模式分为三类:简单工厂模式,工厂方法模式和抽象工厂模式。本文借助于代码 来聊聊模式设计背后的motivation和intuition。首先,给出一个简单工厂模式的代码(关于目标函数类的设计,在xgboost和lightGBM中都有一个文件objective.h):

上述代码中LossSet类是一个工厂类,负责产生误差函数LossFunc,LossFunc类是一个包含纯虚函数的抽象类,定义了所有误差函数都应该实现的接口,或者说该类用来定义机器学习模型中目标函数的骨架。L1和L2是具体的两个误差函数,例如平方根和0-1误差等。

在上述设计中,假设要引入一种新的误差函数L3,则首先要实现一个L3类继承自抽象类LossFunc,L3类的出现符合我们面向对象设计的思想。然而,此时工厂类LossSet中,由此采用了枚举判断的方式,需要修改LossSet工厂类,增加L3的枚举类型。

在设计模式中,设计要符合开放封闭原则。软件实体(类、模块、函数)可以扩展,但是不可修改。。当我们增加新的误差函数的时候,扩展了L3类,但是同时修改了LossSet类。细究其关键,因为工厂类控制了具体实例化哪个类(如实例化L1还是L2?),当增加了新的可实例化的类如L3的时候,工厂类的控制权也要相应修改。找到了问题的关键,那就把控制权从工厂类中转移出去?

这就出现了工厂方法模式。所谓工厂方法模式,是指定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。也就是说将简单工厂模式中的工厂类变成一个接口(啥事儿也不干,就订标准),控制权转移到他的子类中。这样,当新的误差函数出现的时候,实现两个扩展。第一个是误差函数类的扩展,如实现L3类,和简单工厂模式相同。第二个扩展是实现一个新的工厂子类,负责具体实例化哪个类,简单工厂方法中要实现同样的目的可是要修改的哦。这样的想法同时也符合开闭原则。

简单的说:工厂方法模式就是将简单工厂模式中的工厂类接口化

改写之后,代码如下:

在上述代码中,main函数给出了使用方法。由于在上文中,我们分析到控制权在工厂类的子类中,每个子类负责生产一种产品。那么,当我需要某种产品的时候,我向工厂要就OK啦!由于我们将简单工厂方法中的工厂类接口化,将控制权分散到子类中去,也就意味着工厂方法模式需要更多的类定义!(面向对象偏执狂不仅喜欢什么都是类,而且喜欢方法调用不嵌套到1000级不罢休,黑我某搭档,虽然他确实是个优秀的程序员吧。)

关于抽象工厂模式,思想是类的组合,这种组合是控制权的组合。此处给出一个应用场景(我杜撰的):给定条件假设误差函数,二者可共同确定一个学习模型(不严格的表述)。通常我们可以设定的条件假设有很多种,比如样本符合高斯分布,噪声符合瑞利分布,属性独立不相关。误差函数也有多种类型,如上文我们提到的0-1误差,平方根误差。假设我现在需要一个特定的模型,这个模型样本符合高斯分布,使用0-1误差,怎么去设计类呢?

没有答案。

没有答案。

真的没有答案。

我说过了,真的没有。

没有答案。

还往下看?没有。

真···没···有···答···案···

(结尾还有一丢丢)

可以设计一个条件假设接口,三个子类实现三种假设。设计一个误差函数接口,两个子类实现两个误差函数。设计一个工厂类,定义六个虚函数,六个子类实现六种模型。这种情况下,和工厂方法模式相同,需要某个特定模型,向工厂要。

总结:受papi酱影响,中毒太深。设计模式围绕扩展工作很多,面向对象使得组件复用性大大提高,或者说可扩展性大大提高。做算法,可能某种意义上离业务开发较远,看了很多同学们写的算法实现,都是面向过程的高手,可能对于算法实现来说,对于扩展的需求并不是特别强烈。有些轮子,造了就可以不用改了,因为有很多方式让这个轮子直接拿来使用。但是,有设计的代码,总是显得格外优雅。